Skip to content

Commit 4c38558

Browse files
committed
feat(xai): add mcpServer tool for MCP server support
- Adds xai.tools.mcpServer() for connecting to remote MCP servers via xAI's Responses API - Includes MCP type definition in XaiResponsesTool with all xAI API fields - Adds args validation and camelCase to snake_case transformation - Includes streaming event schemas for MCP calls (mcp_call, response.mcp_call.* events) - Supports all MCP configuration options: serverUrl, serverLabel, serverDescription, allowedTools, headers, authorization
1 parent 817e601 commit 4c38558

File tree

9 files changed

+318
-85
lines changed

9 files changed

+318
-85
lines changed

.changeset/mcp-server-tool.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@ai-sdk/xai": patch
3+
---
4+
5+
Add `mcpServer` tool factory for xAI Responses API Remote MCP support
6+
7+
- Adds `xai.tools.mcpServer()` for connecting to remote MCP servers via xAI's Responses API
8+
- Includes streaming event schemas for MCP calls (`mcp_call`, `response.mcp_call.*` events)
9+
- Supports all MCP configuration options: serverUrl, serverLabel, serverDescription, allowedTools, headers, authorization

packages/xai/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { createXai, xai } from './xai-provider';
55
export type { XaiProvider, XaiProviderSettings } from './xai-provider';
66
export {
77
codeExecution,
8+
mcpServer,
89
viewImage,
910
viewXVideo,
1011
webSearch,

packages/xai/src/responses/__snapshots__/xai-responses-language-model.test.ts.snap

Lines changed: 0 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3734,28 +3734,6 @@ exports[`XaiResponsesLanguageModel > doStream > text streaming > should stream t
37343734
"id": "text-msg_769f3302-64f9-4c72-2b48-860c87fd9b2a",
37353735
"type": "text-delta",
37363736
},
3737-
{
3738-
"delta": "### Overview of Sonoran Cuisine
3739-
Sonoran cuisine originates from the state of Sonora in northwest Mexico, blending indigenous, Spanish, and American influences due to its proximity to the U.S. border and arid desert environment. It's notably rustic, flavorful, and fusion-oriented, often featuring grilled meats, fresh produce, and bold spices adapted to the region's hot, dry climate. Unlike coastal Mexican cuisines, it emphasizes hearty, protein-heavy dishes with influences from the American Southwest, such as flour tortillas and cheese.
3740-
3741-
### Key Ingredients and Techniques
3742-
- **Meats and Proteins**: Grilled or roasted meats like beef (carne asada), pork, and chicken are central, often seasoned with simple marinades of lime, garlic, and chili. Machaca (dried, shredded beef) is a hallmark preservation technique for the desert.
3743-
- **Chiles and Spices**: Local chiles (e.g., Sonora's chiltepin, a tiny, potent pepper) add heat, while cumin, oregano, and cilantro provide earthy flavors. Beans (frijoles) and corn are staples, often prepared simply to highlight natural tastes.
3744-
- **Tortillas and Breads**: Flour tortillas dominate, reflecting American influence, unlike corn tortillas in many other Mexican regions. Baking and grilling are common, with an emphasis on fresh, unprocessed ingredients.
3745-
- **Regional Staples**: Desert elements like nopales (cactus pads), nopalitos, and wild herbs are incorporated for texture and nutrition, showcasing indigenous roots.
3746-
3747-
### Signature Dishes
3748-
- **Carne Asada Tacos**: Thinly sliced, grilled skirt steak served on flour tortillas with salsa, onions, and cilantrooften eaten with beans and rice.
3749-
- **Sonoran Hot Dogs**: A fusion dish where a hot dog is wrapped in bacon, grilled, and topped with pinto beans, onions, tomatoes, mayo, mustard, and cheese in a sliced bolillo roll. This originated in border towns and highlights Mexican-American crossover.
3750-
- **Chimichangas**: Deep-fried burritos filled with meat, cheese, and beans, served with guacamole and sour creama nod to Tex-Mex influences.
3751-
- **Ceviche and Seafood**: Coastal Sonoran areas feature fresh ceviche (citrus-marinated fish) and shrimp dishes, contrasting the land-based inland cuisine.
3752-
- **Other Notables**: Guaymas-style tamales (with meat and sauce) and caldillo (a beef stew with potatoes) emphasize slow-cooked comfort foods.
3753-
3754-
### Cultural and Historical Influences
3755-
Sonora's style emerged from indigenous Yaqui and Mayo tribes, who used desert foraging, combined with Spanish colonial methods like cattle ranching. Post-Mexican-American War migration boosted American elements, creating dishes like flour-based tacos. Today, it's celebrated in cities like Hermosillo and Tucson (Arizona), influencing Southwestern U.S. cuisine. Festivals like the Sonora International Gastronomic Festival highlight its evolution, and it's known for its focus on quality ingredients over elaborate preparations, making it accessible and flavorful for everyday meals. If you're exploring, local markets (tianguis) are the best places to experience authentic flavors.",
3756-
"id": "text-msg_769f3302-64f9-4c72-2b48-860c87fd9b2a",
3757-
"type": "text-delta",
3758-
},
37593737
{
37603738
"id": "text-msg_769f3302-64f9-4c72-2b48-860c87fd9b2a",
37613739
"type": "text-end",
@@ -7051,29 +7029,6 @@ exports[`XaiResponsesLanguageModel > doStream > text streaming > should stream t
70517029
"id": "text-msg_0b824fe9-3250-2588-0bbf-0810402fc822",
70527030
"type": "text-delta",
70537031
},
7054-
{
7055-
"delta": "### Overview of Sonoran Cuisine
7056-
Sonoran cuisine originates from the Sonora region in northwestern Mexico, characterized by its simplicity, fresh ingredients, and emphasis on grilling and bold flavors. Unlike spicier Mexican cuisines from other regions, it's notably milder, relying on the region's abundant desert produce and livestock. This style blends indigenous influences (e.g., from Yaqui and Mayo peoples) with Spanish colonial elements, resulting in hearty, straightforward dishes that highlight natural tastes over complex preparations.
7057-
7058-
### Key Ingredients and Flavors
7059-
- **Mild Chilies and Heat**: The signature is the Mexican Sonora chile (a mild, fruity pepper), often used fresh or roasted. Unlike hotter varieties in Yucatán or Oaxaca, Sonoran food avoids excessive spice, focusing on subtle sweetness and smokiness.
7060-
- **Desert and Local Produce**: Staples include nopales (cactus pads), beans, corn, tomatoes, and onions. The arid climate favors grilled or fresh preparations, with minimal sauces.
7061-
- **Proteins**: Beef, especially in carne asada (grilled skirt steak), is dominant, reflecting Sonora's cattle ranching history. Other meats like pork or chicken are common, often seasoned simply with garlic, lime, and herbs.
7062-
- **Influence of the Sea and Land**: Coastal areas incorporate seafood (e.g., shrimp or fish), while inland focuses on ranch-raised meats. Dairy is rare, emphasizing Mexican crema or cheese sparingly.
7063-
7064-
### Notable Dishes and Preparation Styles
7065-
- **Grilling as a Core Technique**: Many dishes are cooked over open flames or on a comal, imparting a charred, smoky flavor. This is seen in carne asada, served with tortillas, guacamole, and pico de gallo.
7066-
- **Iconic Foods**:
7067-
- **Machaca**: Shredded, dried beef slow-cooked with eggs, onions, and chilies— a breakfast staple that's simple yet flavorful.
7068-
- **Tamales**: Steamed corn masa filled with meat or cheese, often wrapped in corn husks; Sonoran versions are less sauced than those in central Mexico.
7069-
- **Chimichangas**: Fried burritos, a Sonora-influenced Tex-Mex creation, highlighting the region's border proximity to Arizona.
7070-
- **Beverages**: Agua fresca (fruit waters) from local fruits like watermelon or hibiscus, and horchata, a rice-based drink, complement meals.
7071-
7072-
### Cultural and Regional Context
7073-
Sonoran's style is practical for its hot, dry climate—dishes are quick to prepare and preserve well. It's less ornate than Yucatecan or Jaliscan cuisines, prioritizing sustenance over elaboration. In the U.S., especially Arizona, Sonoran influences appear in fusion foods like Mexican hot dogs (bacon-wrapped with beans and salsa). This cuisine's notable restraint in spice and focus on grilling make it distinct, emphasizing the desert's bounty for authentic, approachable meals.",
7074-
"id": "text-msg_0b824fe9-3250-2588-0bbf-0810402fc822",
7075-
"type": "text-delta",
7076-
},
70777032
{
70787033
"id": "text-msg_0b824fe9-3250-2588-0bbf-0810402fc822",
70797034
"type": "text-end",
@@ -10489,26 +10444,6 @@ exports[`XaiResponsesLanguageModel > doStream > text streaming > should stream t
1048910444
"id": "text-msg_bf3b2b34-79d4-a45c-7be8-d1e5f96386c2",
1049010445
"type": "text-delta",
1049110446
},
10492-
{
10493-
"delta": "### Overview of Sonoran Cuisine
10494-
Sonoran food originates from the Sonoran Desert region, spanning parts of northern Mexico (especially Sonora state) and southern Arizona. It's a rustic, resourceful style shaped by the arid environment, blending Indigenous, Spanish, and Mexican influences. Unlike the more complex, mole-heavy cuisines of central Mexico, Sonoran fare emphasizes simplicity, fresh local ingredients, and bold, smoky flavors from grilling and chiles. It's practical for desert living, focusing on sustenance with minimal processing.
10495-
10496-
### Key Ingredients and Characteristics
10497-
- **Local Produce and Staples**: Heavily reliant on desert-sourced items like nopales (prickly pear cactus pads), which are grilled or sautéed for their tangy, crisp texture. Beans (pinto or black), corn, and chiles (e.g., chili pequin or jalapeños) are ubiquitous, often used fresh or dried. Tomatoes, onions, and herbs like cilantro add freshness.
10498-
- **Proteins**: Beef, pork, and chicken dominate, with grilling (asado) as a core technique, infusing meats with smoky char. Seafood appears in coastal Sonoran areas, like shrimp or fish tacos.
10499-
- **Bold Flavors and Simplicity**: Dishes avoid heavy sauces; instead, they rely on spices, lime, and chiles for heat and acidity. Tortillas (corn or flour) serve as the base, with minimal elaboration—think of it as "desert pragmatism" where food is hearty yet straightforward to prepare in hot climates.
10500-
10501-
### Signature Dishes
10502-
- **Carne Asada**: Grilled skirt or flank steak marinated in lime, garlic, and cumin, served with tortillas, salsa, and beans. It's the region's barbecue staple, often enjoyed at family gatherings.
10503-
- **Sonoran Hot Dogs**: A fusion delight where a bacon-wrapped hot dog is topped with pinto beans, salsa, onions, and mustard in a bun. Originating in Hermosillo, Mexico, it's a street food icon, blending American and Mexican influences.
10504-
- **Tacos and Tamales**: Tacos are filled with grilled meats, nopales, or cheese, while tamales use corn masa wrapped in corn husks and steamed, often stuffed with pork or chicken.
10505-
- **Chimichangas**: Deep-fried burritos, more associated with Arizona's Mexican-American adaptations, filled with meat, cheese, and beans.
10506-
10507-
### Cultural and Regional Notes
10508-
Sonoran cuisine reflects Indigenous roots from tribes like the Tohono O'odham and Yaqui, who taught Europeans to use desert plants for survival. Spanish colonialists added wheat flour and livestock. In Mexico's Sonora, it's more traditional and seafood-influenced near the Gulf of California, while in Arizona, it incorporates Tex-Mex elements like flour tortillas and cheese. This style stands out for its resilience and adaptability, making it distinct from coastal Mexican seafood or Yucatecan spice profiles. For authentic experiences, try spots like food trucks in Tucson or markets in Hermosillo.",
10509-
"id": "text-msg_bf3b2b34-79d4-a45c-7be8-d1e5f96386c2",
10510-
"type": "text-delta",
10511-
},
1051210447
{
1051310448
"id": "text-msg_bf3b2b34-79d4-a45c-7be8-d1e5f96386c2",
1051410449
"type": "text-end",
@@ -11921,19 +11856,6 @@ exports[`XaiResponsesLanguageModel > doStream > text streaming > should stream w
1192111856
"type": "source",
1192211857
"url": "https://x.ai/",
1192311858
},
11924-
{
11925-
"delta": "xAI is an American artificial intelligence company founded by Elon Musk in July 2023. Its mission is to "understand the true nature of the universe" by advancing scientific discovery through AI. The company is based in the San Francisco Bay Area and focuses on developing advanced AI models, including the Grok chatbot (integrated with the X platform, formerly Twitter).
11926-
11927-
Key points:
11928-
- **Founders and Team**: Led by Elon Musk, with a team of engineers and researchers from companies like OpenAI, Google DeepMind, and Tesla.
11929-
- **Products**: xAI has released Grok-1 (an open-source large language model) and subsequent versions like Grok-1.5 and Grok-2, which power conversational AI tools.
11930-
- **Funding**: Raised significant venture capital, including a $6 billion Series B round in 2024, valuing the company at around $24 billion.
11931-
- **Philosophy**: Emphasizes curiosity-driven AI development, with a focus on truth-seeking and avoiding overly restrictive safety measures compared to competitors.
11932-
11933-
Note: "XAI" can also refer to "Explainable AI," a field in machine learning that makes AI decisions transparent and interpretable to humans. If that's what you meant, let me know for more details! For the latest updates, check x.ai.",
11934-
"id": "text-msg_98a8d4aa-fc8b-fd93-e673-d5a8f1c9cee8",
11935-
"type": "text-delta",
11936-
},
1193711859
{
1193811860
"id": "id-5",
1193911861
"sourceType": "url",

packages/xai/src/responses/xai-responses-api.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,15 @@ export type XaiResponsesTool =
7979
| { type: 'view_image' }
8080
| { type: 'view_x_video' }
8181
| { type: 'file_search' }
82-
| { type: 'mcp' }
82+
| {
83+
type: 'mcp';
84+
server_url: string;
85+
server_label: string;
86+
server_description?: string;
87+
allowed_tools?: string[];
88+
headers?: Record<string, string>;
89+
authorization?: string;
90+
}
8391
| {
8492
type: 'function';
8593
function: {
@@ -122,6 +130,16 @@ const toolCallSchema = z.object({
122130
action: z.any().optional(),
123131
});
124132

133+
const mcpCallSchema = z.object({
134+
name: z.string().optional(),
135+
arguments: z.string().optional(),
136+
output: z.string().optional(),
137+
error: z.string().optional(),
138+
id: z.string(),
139+
status: z.string(),
140+
server_label: z.string().optional(),
141+
});
142+
125143
const outputItemSchema = z.discriminatedUnion('type', [
126144
z.object({
127145
type: z.literal('web_search_call'),
@@ -151,6 +169,10 @@ const outputItemSchema = z.discriminatedUnion('type', [
151169
type: z.literal('custom_tool_call'),
152170
...toolCallSchema.shape,
153171
}),
172+
z.object({
173+
type: z.literal('mcp_call'),
174+
...mcpCallSchema.shape,
175+
}),
154176
z.object({
155177
type: z.literal('message'),
156178
role: z.string(),
@@ -347,6 +369,50 @@ export const xaiResponsesChunkSchema = z.union([
347369
item_id: z.string(),
348370
output_index: z.number(),
349371
}),
372+
z.object({
373+
type: z.literal('response.mcp_call.in_progress'),
374+
item_id: z.string(),
375+
output_index: z.number(),
376+
}),
377+
z.object({
378+
type: z.literal('response.mcp_call.executing'),
379+
item_id: z.string(),
380+
output_index: z.number(),
381+
}),
382+
z.object({
383+
type: z.literal('response.mcp_call.completed'),
384+
item_id: z.string(),
385+
output_index: z.number(),
386+
}),
387+
z.object({
388+
type: z.literal('response.mcp_call.failed'),
389+
item_id: z.string(),
390+
output_index: z.number(),
391+
}),
392+
z.object({
393+
type: z.literal('response.mcp_call_arguments.delta'),
394+
item_id: z.string(),
395+
output_index: z.number(),
396+
delta: z.string(),
397+
}),
398+
z.object({
399+
type: z.literal('response.mcp_call_arguments.done'),
400+
item_id: z.string(),
401+
output_index: z.number(),
402+
arguments: z.string().optional(),
403+
}),
404+
z.object({
405+
type: z.literal('response.mcp_call_output.delta'),
406+
item_id: z.string(),
407+
output_index: z.number(),
408+
delta: z.string(),
409+
}),
410+
z.object({
411+
type: z.literal('response.mcp_call_output.done'),
412+
item_id: z.string(),
413+
output_index: z.number(),
414+
output: z.string().optional(),
415+
}),
350416
z.object({
351417
type: z.literal('response.done'),
352418
response: xaiResponsesResponseSchema,

packages/xai/src/responses/xai-responses-language-model.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 {
6666
topP,
6767
stopSequences,
6868
seed,
69+
responseFormat,
6970
providerOptions,
7071
tools,
7172
toolChoice,
@@ -111,13 +112,29 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 {
111112
});
112113
warnings.push(...toolWarnings);
113114

115+
// Build text.format for structured output
116+
const textFormat =
117+
responseFormat?.type === 'json'
118+
? responseFormat.schema != null
119+
? {
120+
type: 'json_schema' as const,
121+
schema: responseFormat.schema,
122+
name: responseFormat.name ?? 'response',
123+
strict: true,
124+
}
125+
: { type: 'json_object' as const }
126+
: undefined;
127+
114128
const baseArgs: Record<string, unknown> = {
115129
model: this.modelId,
116130
input,
117131
max_tokens: maxOutputTokens,
118132
temperature,
119133
top_p: topP,
120134
seed,
135+
...(textFormat != null && {
136+
text: { format: textFormat },
137+
}),
121138
...(options.reasoningEffort != null && {
122139
reasoning: { effort: options.reasoningEffort },
123140
}),
@@ -196,7 +213,8 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 {
196213
part.type === 'code_execution_call' ||
197214
part.type === 'view_image_call' ||
198215
part.type === 'view_x_video_call' ||
199-
part.type === 'custom_tool_call'
216+
part.type === 'custom_tool_call' ||
217+
part.type === 'mcp_call'
200218
) {
201219
let toolName = part.name ?? '';
202220
if (webSearchSubTools.includes(part.name ?? '')) {
@@ -211,7 +229,9 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 {
211229
const toolInput =
212230
part.type === 'custom_tool_call'
213231
? (part.input ?? '')
214-
: (part.arguments ?? '');
232+
: part.type === 'mcp_call'
233+
? (part.arguments ?? '')
234+
: (part.arguments ?? '');
215235

216236
content.push({
217237
type: 'tool-call',
@@ -314,6 +334,7 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 {
314334
let isFirstChunk = true;
315335
const contentBlocks: Record<string, { type: 'text' }> = {};
316336
const seenToolCalls = new Set<string>();
337+
const streamedTextItemIds = new Set<string>();
317338

318339
const self = this;
319340

@@ -386,6 +407,9 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 {
386407
if (event.type === 'response.output_text.delta') {
387408
const blockId = `text-${event.item_id}`;
388409

410+
// Track that this item_id received streaming text
411+
streamedTextItemIds.add(event.item_id);
412+
389413
if (contentBlocks[blockId] == null) {
390414
contentBlocks[blockId] = { type: 'text' };
391415
controller.enqueue({
@@ -468,7 +492,8 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 {
468492
part.type === 'code_execution_call' ||
469493
part.type === 'view_image_call' ||
470494
part.type === 'view_x_video_call' ||
471-
part.type === 'custom_tool_call'
495+
part.type === 'custom_tool_call' ||
496+
part.type === 'mcp_call'
472497
) {
473498
if (!seenToolCalls.has(part.id)) {
474499
seenToolCalls.add(part.id);
@@ -498,7 +523,9 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 {
498523
const toolInput =
499524
part.type === 'custom_tool_call'
500525
? (part.input ?? '')
501-
: (part.arguments ?? '');
526+
: part.type === 'mcp_call'
527+
? (part.arguments ?? '')
528+
: (part.arguments ?? '');
502529

503530
controller.enqueue({
504531
type: 'tool-input-start',
@@ -530,8 +557,12 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 {
530557
}
531558

532559
if (part.type === 'message') {
560+
// Skip text emission if already streamed via response.output_text.delta
561+
const wasStreamed = streamedTextItemIds.has(part.id);
562+
533563
for (const contentPart of part.content) {
534-
if (contentPart.text && contentPart.text.length > 0) {
564+
// Only emit text if it wasn't already streamed
565+
if (!wasStreamed && contentPart.text && contentPart.text.length > 0) {
535566
const blockId = `text-${part.id}`;
536567

537568
if (contentBlocks[blockId] == null) {

0 commit comments

Comments
 (0)