@ai-sdk/mcp 2.0.0-beta.5 → 2.0.0-beta.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,19 @@
1
- import { JSONSchema7, JSONValue } from '@ai-sdk/provider';
1
+ import type { JSONObject, JSONSchema7, JSONValue } from '@ai-sdk/provider';
2
2
  import {
3
3
  asSchema,
4
4
  dynamicTool,
5
- FlexibleSchema,
6
5
  jsonSchema,
7
6
  safeParseJSON,
8
7
  safeValidateTypes,
9
- Tool,
10
8
  tool,
11
- ToolExecutionOptions,
12
- ToolResultOutput,
9
+ type FlexibleSchema,
10
+ type Tool,
11
+ type ToolExecutionOptions,
12
+ type ToolResultOutput,
13
13
  } from '@ai-sdk/provider-utils';
14
- import { z } from 'zod/v4';
14
+ import type { z } from 'zod/v4';
15
15
  import { MCPClientError } from '../error/mcp-client-error';
16
- import {
16
+ import type {
17
17
  JSONRPCError,
18
18
  JSONRPCNotification,
19
19
  JSONRPCRequest,
@@ -22,43 +22,45 @@ import {
22
22
  import {
23
23
  createMcpTransport,
24
24
  isCustomMcpTransport,
25
- MCPTransport,
26
- MCPTransportConfig,
25
+ type MCPTransport,
26
+ type MCPTransportConfig,
27
27
  } from './mcp-transport';
28
+ import { getMCPAppToolMeta, MCP_APP_MIME_TYPE } from './mcp-apps';
28
29
  import {
29
- CallToolResult,
30
30
  CallToolResultSchema,
31
- ClientCapabilities,
32
- Configuration as ClientConfiguration,
33
- ElicitationRequest,
34
31
  ElicitationRequestSchema,
35
- ElicitResult,
36
32
  ElicitResultSchema,
37
33
  InitializeResultSchema,
38
34
  LATEST_PROTOCOL_VERSION,
39
- ListResourceTemplatesResult,
40
35
  ListResourceTemplatesResultSchema,
41
- ListResourcesResult,
42
36
  ListResourcesResultSchema,
43
- ListPromptsResult,
44
37
  ListPromptsResultSchema,
45
- ListToolsResult,
46
38
  ListToolsResultSchema,
47
- McpToolSet,
48
- Notification,
49
- PaginatedRequest,
50
- ReadResourceResult,
51
39
  ReadResourceResultSchema,
52
- GetPromptResult,
53
40
  GetPromptResultSchema,
54
- Request,
55
- RequestOptions,
56
- ServerCapabilities,
57
41
  SUPPORTED_PROTOCOL_VERSIONS,
58
- ToolSchemas,
59
- ToolMeta,
42
+ type CallToolResult,
43
+ type ClientCapabilities,
44
+ type Configuration,
45
+ type Configuration as ClientConfiguration,
46
+ type ElicitationRequest,
47
+ type ElicitResult,
48
+ type ListResourceTemplatesResult,
49
+ type ListResourcesResult,
50
+ type ListPromptsResult,
51
+ type ListToolsResult,
52
+ type McpToolSet,
53
+ type Notification,
54
+ type PaginatedRequest,
55
+ type ReadResourceResult,
56
+ type GetPromptResult,
57
+ type Request,
58
+ type RequestOptions,
59
+ type ServerCapabilities,
60
+ type ToolSchemas,
61
+ type ToolMeta,
62
+ type McpProviderMetadata,
60
63
  } from './types';
61
-
62
64
  const CLIENT_VERSION = '1.0.0';
63
65
 
64
66
  function mcpToModelOutput({
@@ -81,9 +83,9 @@ function mcpToModelOutput({
81
83
  }
82
84
  if (part.type === 'image' && 'data' in part && 'mimeType' in part) {
83
85
  return {
84
- type: 'image-data' as const,
85
- data: part.data as string,
86
+ type: 'file' as const,
86
87
  mediaType: part.mimeType as string,
88
+ data: { type: 'data' as const, data: part.data as string },
87
89
  };
88
90
  }
89
91
  return { type: 'text' as const, text: JSON.stringify(part) };
@@ -99,6 +101,12 @@ export interface MCPClientConfig {
99
101
  /** Optional callback for uncaught errors */
100
102
  onUncaughtError?: (error: unknown) => void;
101
103
  /** Optional client name, defaults to 'ai-sdk-mcp-client' */
104
+ clientName?: string;
105
+ /**
106
+ * Optional client name, defaults to 'ai-sdk-mcp-client'
107
+ *
108
+ * @deprecated Use `clientName` instead.
109
+ */
102
110
  name?: string;
103
111
  /** Optional client version, defaults to '1.0.0' */
104
112
  version?: string;
@@ -119,6 +127,22 @@ export async function createMCPClient(
119
127
  }
120
128
 
121
129
  export interface MCPClient {
130
+ /**
131
+ * Information about the connected MCP server, as reported during initialization.
132
+ * @see https://modelcontextprotocol.io/specification/2025-11-25/schema#implementation
133
+ */
134
+ readonly serverInfo: Configuration;
135
+
136
+ /**
137
+ * Optional instructions provided by the server during the initialize handshake.
138
+ *
139
+ * These describe how to use the server and its features, and can be used by clients
140
+ * to improve LLM interactions (e.g. by including them in the system prompt).
141
+ *
142
+ * @see https://modelcontextprotocol.io/specification/2025-11-25/schema#initializeresult
143
+ */
144
+ readonly instructions?: string;
145
+
122
146
  tools<TOOL_SCHEMAS extends ToolSchemas = 'automatic'>(options?: {
123
147
  schemas?: TOOL_SCHEMAS;
124
148
  }): Promise<McpToolSet<TOOL_SCHEMAS>>;
@@ -131,6 +155,15 @@ export interface MCPClient {
131
155
  options?: RequestOptions;
132
156
  }): Promise<ListToolsResult>;
133
157
 
158
+ /**
159
+ * Calls a tool on the MCP server.
160
+ */
161
+ callTool(args: {
162
+ name: string;
163
+ arguments?: Record<string, unknown>;
164
+ options?: RequestOptions;
165
+ }): Promise<CallToolResult>;
166
+
134
167
  /**
135
168
  * Creates AI SDK tools from tool definitions.
136
169
  */
@@ -201,6 +234,8 @@ class DefaultMCPClient implements MCPClient {
201
234
  (response: JSONRPCResponse | Error) => void
202
235
  > = new Map();
203
236
  private serverCapabilities: ServerCapabilities = {};
237
+ private _serverInfo: Configuration = { name: '', version: '' };
238
+ private _serverInstructions?: string;
204
239
  private isClosed = true;
205
240
  private elicitationRequestHandler?: (
206
241
  request: ElicitationRequest,
@@ -208,7 +243,8 @@ class DefaultMCPClient implements MCPClient {
208
243
 
209
244
  constructor({
210
245
  transport: transportConfig,
211
- name = 'ai-sdk-mcp-client',
246
+ name,
247
+ clientName = name ?? 'ai-sdk-mcp-client',
212
248
  version = CLIENT_VERSION,
213
249
  onUncaughtError,
214
250
  capabilities,
@@ -242,11 +278,19 @@ class DefaultMCPClient implements MCPClient {
242
278
  };
243
279
 
244
280
  this.clientInfo = {
245
- name,
281
+ name: clientName,
246
282
  version,
247
283
  };
248
284
  }
249
285
 
286
+ get serverInfo(): Configuration {
287
+ return this._serverInfo;
288
+ }
289
+
290
+ get instructions(): string | undefined {
291
+ return this._serverInstructions;
292
+ }
293
+
250
294
  async init(): Promise<this> {
251
295
  try {
252
296
  await this.transport.start();
@@ -277,6 +321,13 @@ class DefaultMCPClient implements MCPClient {
277
321
  }
278
322
 
279
323
  this.serverCapabilities = result.capabilities;
324
+ this._serverInfo = result.serverInfo;
325
+ if (this.transport.setProtocolVersion) {
326
+ this.transport.setProtocolVersion(result.protocolVersion);
327
+ } else {
328
+ this.transport.protocolVersion = result.protocolVersion;
329
+ }
330
+ this._serverInstructions = result.instructions;
280
331
 
281
332
  // Complete initialization handshake:
282
333
  await this.notification({
@@ -413,22 +464,20 @@ class DefaultMCPClient implements MCPClient {
413
464
  });
414
465
  }
415
466
 
416
- private async callTool({
467
+ async callTool({
417
468
  name,
418
- args,
469
+ arguments: args = {},
419
470
  options,
420
471
  }: {
421
472
  name: string;
422
- args: Record<string, unknown>;
423
- options?: ToolExecutionOptions;
473
+ arguments?: Record<string, unknown>;
474
+ options?: RequestOptions;
424
475
  }): Promise<CallToolResult> {
425
476
  try {
426
477
  return this.request({
427
478
  request: { method: 'tools/call', params: { name, arguments: args } },
428
479
  resultSchema: CallToolResultSchema,
429
- options: {
430
- signal: options?.abortSignal,
431
- },
480
+ options,
432
481
  });
433
482
  } catch (error) {
434
483
  throw error;
@@ -569,20 +618,45 @@ class DefaultMCPClient implements MCPClient {
569
618
  _meta,
570
619
  } of definitions.tools) {
571
620
  const resolvedTitle = title ?? annotations?.title;
572
- if (schemas !== 'automatic' && !(name in schemas)) {
621
+ if (
622
+ schemas !== 'automatic' &&
623
+ !Object.prototype.hasOwnProperty.call(schemas, name)
624
+ ) {
573
625
  continue;
574
626
  }
575
627
 
576
628
  const self = this;
577
629
  const outputSchema =
578
630
  schemas !== 'automatic' ? schemas[name]?.outputSchema : undefined;
631
+ const appMeta = getMCPAppToolMeta({ _meta });
632
+ const metadata = {
633
+ clientName: this.clientInfo.name,
634
+ toolName: name,
635
+ ...(resolvedTitle != null ? { title: resolvedTitle } : {}),
636
+ ...(appMeta?.resourceUri != null
637
+ ? {
638
+ app: {
639
+ ...appMeta,
640
+ mimeType: MCP_APP_MIME_TYPE,
641
+ } as JSONObject,
642
+ }
643
+ : {}),
644
+ } satisfies McpProviderMetadata;
579
645
 
580
646
  const execute = async (
581
647
  args: any,
582
- options: ToolExecutionOptions,
648
+ options: ToolExecutionOptions<{}>,
583
649
  ): Promise<unknown> => {
584
650
  options?.abortSignal?.throwIfAborted();
585
- const result = await self.callTool({ name, args, options });
651
+ const result = await self.callTool({
652
+ name,
653
+ arguments: args,
654
+ options: { signal: options?.abortSignal },
655
+ });
656
+
657
+ if (result.isError) {
658
+ return result;
659
+ }
586
660
 
587
661
  if (outputSchema != null) {
588
662
  return self.extractStructuredContent(result, outputSchema, name);
@@ -596,6 +670,7 @@ class DefaultMCPClient implements MCPClient {
596
670
  ? dynamicTool({
597
671
  description,
598
672
  title: resolvedTitle,
673
+ metadata,
599
674
  inputSchema: jsonSchema({
600
675
  ...inputSchema,
601
676
  properties: inputSchema.properties ?? {},
@@ -607,6 +682,7 @@ class DefaultMCPClient implements MCPClient {
607
682
  : tool({
608
683
  description,
609
684
  title: resolvedTitle,
685
+ metadata,
610
686
  inputSchema: schemas[name].inputSchema,
611
687
  ...(outputSchema != null ? { outputSchema } : {}),
612
688
  execute,
@@ -736,6 +812,15 @@ class DefaultMCPClient implements MCPClient {
736
812
 
737
813
  private async onRequestMessage(request: JSONRPCRequest): Promise<void> {
738
814
  try {
815
+ if (request.method === 'ping') {
816
+ await this.transport.send({
817
+ jsonrpc: '2.0',
818
+ id: request.id,
819
+ result: {},
820
+ });
821
+ return;
822
+ }
823
+
739
824
  if (request.method !== 'elicitation/create') {
740
825
  await this.transport.send({
741
826
  jsonrpc: '2.0',
@@ -2,19 +2,29 @@ import {
2
2
  EventSourceParserStream,
3
3
  withUserAgentSuffix,
4
4
  getRuntimeEnvironmentUserAgent,
5
+ type FetchFunction,
5
6
  } from '@ai-sdk/provider-utils';
6
7
  import { MCPClientError } from '../error/mcp-client-error';
7
- import { JSONRPCMessage, JSONRPCMessageSchema } from './json-rpc-message';
8
- import { MCPTransport } from './mcp-transport';
8
+ import {
9
+ JSONRPCMessageSchema,
10
+ parseJSONRPCMessage,
11
+ type JSONRPCMessage,
12
+ } from './json-rpc-message';
13
+ import type { MCPTransport } from './mcp-transport';
9
14
  import { VERSION } from '../version';
10
15
  import {
11
- OAuthClientProvider,
12
16
  extractResourceMetadataUrl,
13
17
  UnauthorizedError,
14
18
  auth,
19
+ type AuthResult,
20
+ type OAuthClientProvider,
15
21
  } from './oauth';
16
22
  import { LATEST_PROTOCOL_VERSION } from './types';
17
23
 
24
+ function isMessageEvent(event: string | undefined): boolean {
25
+ return event === undefined || event === 'message';
26
+ }
27
+
18
28
  /**
19
29
  * HTTP MCP transport implementing the Streamable HTTP style.
20
30
  *
@@ -30,6 +40,9 @@ export class HttpMCPTransport implements MCPTransport {
30
40
  private resourceMetadataUrl?: URL;
31
41
  private sessionId?: string;
32
42
  private inboundSseConnection?: { close: () => void };
43
+ private redirectMode: RequestRedirect;
44
+ private fetchFn: FetchFunction;
45
+ private authPromise?: Promise<AuthResult>;
33
46
 
34
47
  // Inbound SSE resumption and reconnection state
35
48
  private lastInboundEventId?: string;
@@ -44,19 +57,30 @@ export class HttpMCPTransport implements MCPTransport {
44
57
  onclose?: () => void;
45
58
  onerror?: (error: unknown) => void;
46
59
  onmessage?: (message: JSONRPCMessage) => void;
60
+ protocolVersion?: string;
47
61
 
48
62
  constructor({
49
63
  url,
50
64
  headers,
51
65
  authProvider,
66
+ redirect = 'error',
67
+ fetch: fetchFn,
52
68
  }: {
53
69
  url: string;
54
70
  headers?: Record<string, string>;
55
71
  authProvider?: OAuthClientProvider;
72
+ redirect?: 'follow' | 'error';
73
+ fetch?: FetchFunction;
56
74
  }) {
57
75
  this.url = new URL(url);
58
76
  this.headers = headers;
59
77
  this.authProvider = authProvider;
78
+ this.redirectMode = redirect;
79
+ this.fetchFn = fetchFn ?? globalThis.fetch;
80
+ }
81
+
82
+ setProtocolVersion(version: string): void {
83
+ this.protocolVersion = version;
60
84
  }
61
85
 
62
86
  private async commonHeaders(
@@ -65,7 +89,7 @@ export class HttpMCPTransport implements MCPTransport {
65
89
  const headers: Record<string, string> = {
66
90
  ...this.headers,
67
91
  ...base,
68
- 'mcp-protocol-version': LATEST_PROTOCOL_VERSION,
92
+ 'mcp-protocol-version': this.protocolVersion ?? LATEST_PROTOCOL_VERSION,
69
93
  };
70
94
 
71
95
  if (this.sessionId) {
@@ -86,6 +110,27 @@ export class HttpMCPTransport implements MCPTransport {
86
110
  );
87
111
  }
88
112
 
113
+ /**
114
+ * Runs a single OAuth recovery flow for concurrent 401 responses.
115
+ */
116
+ private authorizeOnce(resourceMetadataUrl?: URL): Promise<AuthResult> {
117
+ if (!this.authProvider) {
118
+ return Promise.resolve('REDIRECT');
119
+ }
120
+
121
+ if (!this.authPromise) {
122
+ this.authPromise = auth(this.authProvider, {
123
+ serverUrl: this.url,
124
+ resourceMetadataUrl,
125
+ fetchFn: this.fetchFn,
126
+ }).finally(() => {
127
+ this.authPromise = undefined;
128
+ });
129
+ }
130
+
131
+ return this.authPromise;
132
+ }
133
+
89
134
  async start(): Promise<void> {
90
135
  if (this.abortController) {
91
136
  throw new MCPClientError({
@@ -107,10 +152,11 @@ export class HttpMCPTransport implements MCPTransport {
107
152
  !this.abortController.signal.aborted
108
153
  ) {
109
154
  const headers = await this.commonHeaders({});
110
- await fetch(this.url, {
155
+ await this.fetchFn(this.url.href, {
111
156
  method: 'DELETE',
112
157
  headers,
113
158
  signal: this.abortController.signal,
159
+ redirect: this.redirectMode,
114
160
  }).catch(() => undefined);
115
161
  }
116
162
  } catch {}
@@ -132,9 +178,10 @@ export class HttpMCPTransport implements MCPTransport {
132
178
  headers,
133
179
  body: JSON.stringify(message),
134
180
  signal: this.abortController?.signal,
181
+ redirect: this.redirectMode,
135
182
  } satisfies RequestInit;
136
183
 
137
- const response = await fetch(this.url, init);
184
+ const response = await this.fetchFn(this.url.href, init);
138
185
 
139
186
  const sessionId = response.headers.get('mcp-session-id');
140
187
  if (sessionId) {
@@ -144,10 +191,7 @@ export class HttpMCPTransport implements MCPTransport {
144
191
  if (response.status === 401 && this.authProvider && !triedAuth) {
145
192
  this.resourceMetadataUrl = extractResourceMetadataUrl(response);
146
193
  try {
147
- const result = await auth(this.authProvider, {
148
- serverUrl: this.url,
149
- resourceMetadataUrl: this.resourceMetadataUrl,
150
- });
194
+ const result = await this.authorizeOnce(this.resourceMetadataUrl);
151
195
  if (result !== 'AUTHORIZED') {
152
196
  const error = new UnauthorizedError();
153
197
  throw error;
@@ -181,6 +225,9 @@ export class HttpMCPTransport implements MCPTransport {
181
225
 
182
226
  const error = new MCPClientError({
183
227
  message: errorMessage,
228
+ statusCode: response.status,
229
+ url: this.url.href,
230
+ responseBody: text ?? undefined,
184
231
  });
185
232
  this.onerror?.(error);
186
233
  throw error;
@@ -197,9 +244,13 @@ export class HttpMCPTransport implements MCPTransport {
197
244
  if (contentType.includes('application/json')) {
198
245
  const data = await response.json();
199
246
  const messages: JSONRPCMessage[] = Array.isArray(data)
200
- ? data.map((m: unknown) => JSONRPCMessageSchema.parse(m))
247
+ ? data.map((message: unknown) =>
248
+ JSONRPCMessageSchema.parse(message),
249
+ )
201
250
  : [JSONRPCMessageSchema.parse(data)];
202
- for (const m of messages) this.onmessage?.(m);
251
+ for (const jsonRpcMessage of messages) {
252
+ this.onmessage?.(jsonRpcMessage);
253
+ }
203
254
  return;
204
255
  }
205
256
 
@@ -208,6 +259,8 @@ export class HttpMCPTransport implements MCPTransport {
208
259
  const error = new MCPClientError({
209
260
  message:
210
261
  'MCP HTTP Transport Error: text/event-stream response without body',
262
+ statusCode: response.status,
263
+ url: this.url.href,
211
264
  });
212
265
  this.onerror?.(error);
213
266
  throw error;
@@ -224,10 +277,10 @@ export class HttpMCPTransport implements MCPTransport {
224
277
  const { done, value } = await reader.read();
225
278
  if (done) return;
226
279
  const { event, data } = value;
227
- if (event === 'message') {
280
+ if (isMessageEvent(event)) {
228
281
  try {
229
- const msg = JSONRPCMessageSchema.parse(JSON.parse(data));
230
- this.onmessage?.(msg);
282
+ const jsonRpcMessage = await parseJSONRPCMessage(data);
283
+ this.onmessage?.(jsonRpcMessage);
231
284
  } catch (error) {
232
285
  const e = new MCPClientError({
233
286
  message:
@@ -252,6 +305,8 @@ export class HttpMCPTransport implements MCPTransport {
252
305
 
253
306
  const error = new MCPClientError({
254
307
  message: `MCP HTTP Transport Error: Unexpected content type: ${contentType}`,
308
+ statusCode: response.status,
309
+ url: this.url.href,
255
310
  });
256
311
  this.onerror?.(error);
257
312
  throw error;
@@ -308,10 +363,11 @@ export class HttpMCPTransport implements MCPTransport {
308
363
  headers['last-event-id'] = resumeToken;
309
364
  }
310
365
 
311
- const response = await fetch(this.url.href, {
366
+ const response = await this.fetchFn(this.url.href, {
312
367
  method: 'GET',
313
368
  headers,
314
369
  signal: this.abortController?.signal,
370
+ redirect: this.redirectMode,
315
371
  });
316
372
 
317
373
  const sessionId = response.headers.get('mcp-session-id');
@@ -322,10 +378,7 @@ export class HttpMCPTransport implements MCPTransport {
322
378
  if (response.status === 401 && this.authProvider && !triedAuth) {
323
379
  this.resourceMetadataUrl = extractResourceMetadataUrl(response);
324
380
  try {
325
- const result = await auth(this.authProvider, {
326
- serverUrl: this.url,
327
- resourceMetadataUrl: this.resourceMetadataUrl,
328
- });
381
+ const result = await this.authorizeOnce(this.resourceMetadataUrl);
329
382
  if (result !== 'AUTHORIZED') {
330
383
  const error = new UnauthorizedError();
331
384
  this.onerror?.(error);
@@ -345,6 +398,8 @@ export class HttpMCPTransport implements MCPTransport {
345
398
  if (!response.ok || !response.body) {
346
399
  const error = new MCPClientError({
347
400
  message: `MCP HTTP Transport Error: GET SSE failed: ${response.status} ${response.statusText}`,
401
+ statusCode: response.status,
402
+ url: this.url.href,
348
403
  });
349
404
  this.onerror?.(error);
350
405
  return;
@@ -370,10 +425,10 @@ export class HttpMCPTransport implements MCPTransport {
370
425
  this.lastInboundEventId = id;
371
426
  }
372
427
 
373
- if (event === 'message') {
428
+ if (isMessageEvent(event)) {
374
429
  try {
375
- const msg = JSONRPCMessageSchema.parse(JSON.parse(data));
376
- this.onmessage?.(msg);
430
+ const jsonRpcMessage = await parseJSONRPCMessage(data);
431
+ this.onmessage?.(jsonRpcMessage);
377
432
  } catch (error) {
378
433
  const e = new MCPClientError({
379
434
  message: 'MCP HTTP Transport Error: Failed to parse message',