@ai-sdk/mcp 2.0.0-beta.6 → 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
  *
@@ -31,6 +41,8 @@ export class HttpMCPTransport implements MCPTransport {
31
41
  private sessionId?: string;
32
42
  private inboundSseConnection?: { close: () => void };
33
43
  private redirectMode: RequestRedirect;
44
+ private fetchFn: FetchFunction;
45
+ private authPromise?: Promise<AuthResult>;
34
46
 
35
47
  // Inbound SSE resumption and reconnection state
36
48
  private lastInboundEventId?: string;
@@ -45,22 +57,30 @@ export class HttpMCPTransport implements MCPTransport {
45
57
  onclose?: () => void;
46
58
  onerror?: (error: unknown) => void;
47
59
  onmessage?: (message: JSONRPCMessage) => void;
60
+ protocolVersion?: string;
48
61
 
49
62
  constructor({
50
63
  url,
51
64
  headers,
52
65
  authProvider,
53
- redirect = 'follow',
66
+ redirect = 'error',
67
+ fetch: fetchFn,
54
68
  }: {
55
69
  url: string;
56
70
  headers?: Record<string, string>;
57
71
  authProvider?: OAuthClientProvider;
58
72
  redirect?: 'follow' | 'error';
73
+ fetch?: FetchFunction;
59
74
  }) {
60
75
  this.url = new URL(url);
61
76
  this.headers = headers;
62
77
  this.authProvider = authProvider;
63
78
  this.redirectMode = redirect;
79
+ this.fetchFn = fetchFn ?? globalThis.fetch;
80
+ }
81
+
82
+ setProtocolVersion(version: string): void {
83
+ this.protocolVersion = version;
64
84
  }
65
85
 
66
86
  private async commonHeaders(
@@ -69,7 +89,7 @@ export class HttpMCPTransport implements MCPTransport {
69
89
  const headers: Record<string, string> = {
70
90
  ...this.headers,
71
91
  ...base,
72
- 'mcp-protocol-version': LATEST_PROTOCOL_VERSION,
92
+ 'mcp-protocol-version': this.protocolVersion ?? LATEST_PROTOCOL_VERSION,
73
93
  };
74
94
 
75
95
  if (this.sessionId) {
@@ -90,6 +110,27 @@ export class HttpMCPTransport implements MCPTransport {
90
110
  );
91
111
  }
92
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
+
93
134
  async start(): Promise<void> {
94
135
  if (this.abortController) {
95
136
  throw new MCPClientError({
@@ -111,7 +152,7 @@ export class HttpMCPTransport implements MCPTransport {
111
152
  !this.abortController.signal.aborted
112
153
  ) {
113
154
  const headers = await this.commonHeaders({});
114
- await fetch(this.url, {
155
+ await this.fetchFn(this.url.href, {
115
156
  method: 'DELETE',
116
157
  headers,
117
158
  signal: this.abortController.signal,
@@ -140,7 +181,7 @@ export class HttpMCPTransport implements MCPTransport {
140
181
  redirect: this.redirectMode,
141
182
  } satisfies RequestInit;
142
183
 
143
- const response = await fetch(this.url, init);
184
+ const response = await this.fetchFn(this.url.href, init);
144
185
 
145
186
  const sessionId = response.headers.get('mcp-session-id');
146
187
  if (sessionId) {
@@ -150,10 +191,7 @@ export class HttpMCPTransport implements MCPTransport {
150
191
  if (response.status === 401 && this.authProvider && !triedAuth) {
151
192
  this.resourceMetadataUrl = extractResourceMetadataUrl(response);
152
193
  try {
153
- const result = await auth(this.authProvider, {
154
- serverUrl: this.url,
155
- resourceMetadataUrl: this.resourceMetadataUrl,
156
- });
194
+ const result = await this.authorizeOnce(this.resourceMetadataUrl);
157
195
  if (result !== 'AUTHORIZED') {
158
196
  const error = new UnauthorizedError();
159
197
  throw error;
@@ -187,6 +225,9 @@ export class HttpMCPTransport implements MCPTransport {
187
225
 
188
226
  const error = new MCPClientError({
189
227
  message: errorMessage,
228
+ statusCode: response.status,
229
+ url: this.url.href,
230
+ responseBody: text ?? undefined,
190
231
  });
191
232
  this.onerror?.(error);
192
233
  throw error;
@@ -203,9 +244,13 @@ export class HttpMCPTransport implements MCPTransport {
203
244
  if (contentType.includes('application/json')) {
204
245
  const data = await response.json();
205
246
  const messages: JSONRPCMessage[] = Array.isArray(data)
206
- ? data.map((m: unknown) => JSONRPCMessageSchema.parse(m))
247
+ ? data.map((message: unknown) =>
248
+ JSONRPCMessageSchema.parse(message),
249
+ )
207
250
  : [JSONRPCMessageSchema.parse(data)];
208
- for (const m of messages) this.onmessage?.(m);
251
+ for (const jsonRpcMessage of messages) {
252
+ this.onmessage?.(jsonRpcMessage);
253
+ }
209
254
  return;
210
255
  }
211
256
 
@@ -214,6 +259,8 @@ export class HttpMCPTransport implements MCPTransport {
214
259
  const error = new MCPClientError({
215
260
  message:
216
261
  'MCP HTTP Transport Error: text/event-stream response without body',
262
+ statusCode: response.status,
263
+ url: this.url.href,
217
264
  });
218
265
  this.onerror?.(error);
219
266
  throw error;
@@ -230,10 +277,10 @@ export class HttpMCPTransport implements MCPTransport {
230
277
  const { done, value } = await reader.read();
231
278
  if (done) return;
232
279
  const { event, data } = value;
233
- if (event === 'message') {
280
+ if (isMessageEvent(event)) {
234
281
  try {
235
- const msg = JSONRPCMessageSchema.parse(JSON.parse(data));
236
- this.onmessage?.(msg);
282
+ const jsonRpcMessage = await parseJSONRPCMessage(data);
283
+ this.onmessage?.(jsonRpcMessage);
237
284
  } catch (error) {
238
285
  const e = new MCPClientError({
239
286
  message:
@@ -258,6 +305,8 @@ export class HttpMCPTransport implements MCPTransport {
258
305
 
259
306
  const error = new MCPClientError({
260
307
  message: `MCP HTTP Transport Error: Unexpected content type: ${contentType}`,
308
+ statusCode: response.status,
309
+ url: this.url.href,
261
310
  });
262
311
  this.onerror?.(error);
263
312
  throw error;
@@ -314,7 +363,7 @@ export class HttpMCPTransport implements MCPTransport {
314
363
  headers['last-event-id'] = resumeToken;
315
364
  }
316
365
 
317
- const response = await fetch(this.url.href, {
366
+ const response = await this.fetchFn(this.url.href, {
318
367
  method: 'GET',
319
368
  headers,
320
369
  signal: this.abortController?.signal,
@@ -329,10 +378,7 @@ export class HttpMCPTransport implements MCPTransport {
329
378
  if (response.status === 401 && this.authProvider && !triedAuth) {
330
379
  this.resourceMetadataUrl = extractResourceMetadataUrl(response);
331
380
  try {
332
- const result = await auth(this.authProvider, {
333
- serverUrl: this.url,
334
- resourceMetadataUrl: this.resourceMetadataUrl,
335
- });
381
+ const result = await this.authorizeOnce(this.resourceMetadataUrl);
336
382
  if (result !== 'AUTHORIZED') {
337
383
  const error = new UnauthorizedError();
338
384
  this.onerror?.(error);
@@ -352,6 +398,8 @@ export class HttpMCPTransport implements MCPTransport {
352
398
  if (!response.ok || !response.body) {
353
399
  const error = new MCPClientError({
354
400
  message: `MCP HTTP Transport Error: GET SSE failed: ${response.status} ${response.statusText}`,
401
+ statusCode: response.status,
402
+ url: this.url.href,
355
403
  });
356
404
  this.onerror?.(error);
357
405
  return;
@@ -377,10 +425,10 @@ export class HttpMCPTransport implements MCPTransport {
377
425
  this.lastInboundEventId = id;
378
426
  }
379
427
 
380
- if (event === 'message') {
428
+ if (isMessageEvent(event)) {
381
429
  try {
382
- const msg = JSONRPCMessageSchema.parse(JSON.parse(data));
383
- this.onmessage?.(msg);
430
+ const jsonRpcMessage = await parseJSONRPCMessage(data);
431
+ this.onmessage?.(jsonRpcMessage);
384
432
  } catch (error) {
385
433
  const e = new MCPClientError({
386
434
  message: 'MCP HTTP Transport Error: Failed to parse message',