@ai-sdk/mcp 2.0.0-beta.3 → 2.0.0-beta.30

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.
@@ -29,6 +29,7 @@ import {
29
29
  CallToolResult,
30
30
  CallToolResultSchema,
31
31
  ClientCapabilities,
32
+ Configuration,
32
33
  Configuration as ClientConfiguration,
33
34
  ElicitationRequest,
34
35
  ElicitationRequestSchema,
@@ -81,7 +82,7 @@ function mcpToModelOutput({
81
82
  }
82
83
  if (part.type === 'image' && 'data' in part && 'mimeType' in part) {
83
84
  return {
84
- type: 'image-data' as const,
85
+ type: 'file-data' as const,
85
86
  data: part.data as string,
86
87
  mediaType: part.mimeType as string,
87
88
  };
@@ -119,6 +120,12 @@ export async function createMCPClient(
119
120
  }
120
121
 
121
122
  export interface MCPClient {
123
+ /**
124
+ * Information about the connected MCP server, as reported during initialization.
125
+ * @see https://modelcontextprotocol.io/specification/2025-11-25/schema#implementation
126
+ */
127
+ readonly serverInfo: Configuration;
128
+
122
129
  tools<TOOL_SCHEMAS extends ToolSchemas = 'automatic'>(options?: {
123
130
  schemas?: TOOL_SCHEMAS;
124
131
  }): Promise<McpToolSet<TOOL_SCHEMAS>>;
@@ -201,6 +208,7 @@ class DefaultMCPClient implements MCPClient {
201
208
  (response: JSONRPCResponse | Error) => void
202
209
  > = new Map();
203
210
  private serverCapabilities: ServerCapabilities = {};
211
+ private _serverInfo: Configuration = { name: '', version: '' };
204
212
  private isClosed = true;
205
213
  private elicitationRequestHandler?: (
206
214
  request: ElicitationRequest,
@@ -247,6 +255,10 @@ class DefaultMCPClient implements MCPClient {
247
255
  };
248
256
  }
249
257
 
258
+ get serverInfo(): Configuration {
259
+ return this._serverInfo;
260
+ }
261
+
250
262
  async init(): Promise<this> {
251
263
  try {
252
264
  await this.transport.start();
@@ -277,6 +289,7 @@ class DefaultMCPClient implements MCPClient {
277
289
  }
278
290
 
279
291
  this.serverCapabilities = result.capabilities;
292
+ this._serverInfo = result.serverInfo;
280
293
 
281
294
  // Complete initialization handshake:
282
295
  await this.notification({
@@ -420,7 +433,7 @@ class DefaultMCPClient implements MCPClient {
420
433
  }: {
421
434
  name: string;
422
435
  args: Record<string, unknown>;
423
- options?: ToolExecutionOptions;
436
+ options?: ToolExecutionOptions<{}>;
424
437
  }): Promise<CallToolResult> {
425
438
  try {
426
439
  return this.request({
@@ -579,11 +592,15 @@ class DefaultMCPClient implements MCPClient {
579
592
 
580
593
  const execute = async (
581
594
  args: any,
582
- options: ToolExecutionOptions,
595
+ options: ToolExecutionOptions<{}>,
583
596
  ): Promise<unknown> => {
584
597
  options?.abortSignal?.throwIfAborted();
585
598
  const result = await self.callTool({ name, args, options });
586
599
 
600
+ if (result.isError) {
601
+ return result;
602
+ }
603
+
587
604
  if (outputSchema != null) {
588
605
  return self.extractStructuredContent(result, outputSchema, name);
589
606
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  EventSourceParserStream,
3
+ FetchFunction,
3
4
  withUserAgentSuffix,
4
5
  getRuntimeEnvironmentUserAgent,
5
6
  } from '@ai-sdk/provider-utils';
@@ -30,6 +31,8 @@ export class HttpMCPTransport implements MCPTransport {
30
31
  private resourceMetadataUrl?: URL;
31
32
  private sessionId?: string;
32
33
  private inboundSseConnection?: { close: () => void };
34
+ private redirectMode: RequestRedirect;
35
+ private fetchFn: FetchFunction;
33
36
 
34
37
  // Inbound SSE resumption and reconnection state
35
38
  private lastInboundEventId?: string;
@@ -49,14 +52,20 @@ export class HttpMCPTransport implements MCPTransport {
49
52
  url,
50
53
  headers,
51
54
  authProvider,
55
+ redirect = 'error',
56
+ fetch: fetchFn,
52
57
  }: {
53
58
  url: string;
54
59
  headers?: Record<string, string>;
55
60
  authProvider?: OAuthClientProvider;
61
+ redirect?: 'follow' | 'error';
62
+ fetch?: FetchFunction;
56
63
  }) {
57
64
  this.url = new URL(url);
58
65
  this.headers = headers;
59
66
  this.authProvider = authProvider;
67
+ this.redirectMode = redirect;
68
+ this.fetchFn = fetchFn ?? globalThis.fetch;
60
69
  }
61
70
 
62
71
  private async commonHeaders(
@@ -107,10 +116,11 @@ export class HttpMCPTransport implements MCPTransport {
107
116
  !this.abortController.signal.aborted
108
117
  ) {
109
118
  const headers = await this.commonHeaders({});
110
- await fetch(this.url, {
119
+ await this.fetchFn(this.url.href, {
111
120
  method: 'DELETE',
112
121
  headers,
113
122
  signal: this.abortController.signal,
123
+ redirect: this.redirectMode,
114
124
  }).catch(() => undefined);
115
125
  }
116
126
  } catch {}
@@ -132,9 +142,10 @@ export class HttpMCPTransport implements MCPTransport {
132
142
  headers,
133
143
  body: JSON.stringify(message),
134
144
  signal: this.abortController?.signal,
145
+ redirect: this.redirectMode,
135
146
  } satisfies RequestInit;
136
147
 
137
- const response = await fetch(this.url, init);
148
+ const response = await this.fetchFn(this.url.href, init);
138
149
 
139
150
  const sessionId = response.headers.get('mcp-session-id');
140
151
  if (sessionId) {
@@ -147,6 +158,7 @@ export class HttpMCPTransport implements MCPTransport {
147
158
  const result = await auth(this.authProvider, {
148
159
  serverUrl: this.url,
149
160
  resourceMetadataUrl: this.resourceMetadataUrl,
161
+ fetchFn: this.fetchFn,
150
162
  });
151
163
  if (result !== 'AUTHORIZED') {
152
164
  const error = new UnauthorizedError();
@@ -308,10 +320,11 @@ export class HttpMCPTransport implements MCPTransport {
308
320
  headers['last-event-id'] = resumeToken;
309
321
  }
310
322
 
311
- const response = await fetch(this.url.href, {
323
+ const response = await this.fetchFn(this.url.href, {
312
324
  method: 'GET',
313
325
  headers,
314
326
  signal: this.abortController?.signal,
327
+ redirect: this.redirectMode,
315
328
  });
316
329
 
317
330
  const sessionId = response.headers.get('mcp-session-id');
@@ -325,6 +338,7 @@ export class HttpMCPTransport implements MCPTransport {
325
338
  const result = await auth(this.authProvider, {
326
339
  serverUrl: this.url,
327
340
  resourceMetadataUrl: this.resourceMetadataUrl,
341
+ fetchFn: this.fetchFn,
328
342
  });
329
343
  if (result !== 'AUTHORIZED') {
330
344
  const error = new UnauthorizedError();
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  EventSourceParserStream,
3
+ FetchFunction,
3
4
  withUserAgentSuffix,
4
5
  getRuntimeEnvironmentUserAgent,
5
6
  } from '@ai-sdk/provider-utils';
@@ -26,6 +27,8 @@ export class SseMCPTransport implements MCPTransport {
26
27
  private headers?: Record<string, string>;
27
28
  private authProvider?: OAuthClientProvider;
28
29
  private resourceMetadataUrl?: URL;
30
+ private redirectMode: RequestRedirect;
31
+ private fetchFn: FetchFunction;
29
32
 
30
33
  onclose?: () => void;
31
34
  onerror?: (error: unknown) => void;
@@ -35,14 +38,20 @@ export class SseMCPTransport implements MCPTransport {
35
38
  url,
36
39
  headers,
37
40
  authProvider,
41
+ redirect = 'error',
42
+ fetch: fetchFn,
38
43
  }: {
39
44
  url: string;
40
45
  headers?: Record<string, string>;
41
46
  authProvider?: OAuthClientProvider;
47
+ redirect?: 'follow' | 'error';
48
+ fetch?: FetchFunction;
42
49
  }) {
43
50
  this.url = new URL(url);
44
51
  this.headers = headers;
45
52
  this.authProvider = authProvider;
53
+ this.redirectMode = redirect;
54
+ this.fetchFn = fetchFn ?? globalThis.fetch;
46
55
  }
47
56
 
48
57
  private async commonHeaders(
@@ -81,9 +90,10 @@ export class SseMCPTransport implements MCPTransport {
81
90
  const headers = await this.commonHeaders({
82
91
  Accept: 'text/event-stream',
83
92
  });
84
- const response = await fetch(this.url.href, {
93
+ const response = await this.fetchFn(this.url.href, {
85
94
  headers,
86
95
  signal: this.abortController?.signal,
96
+ redirect: this.redirectMode,
87
97
  });
88
98
 
89
99
  if (response.status === 401 && this.authProvider && !triedAuth) {
@@ -92,6 +102,7 @@ export class SseMCPTransport implements MCPTransport {
92
102
  const result = await auth(this.authProvider, {
93
103
  serverUrl: this.url,
94
104
  resourceMetadataUrl: this.resourceMetadataUrl,
105
+ fetchFn: this.fetchFn,
95
106
  });
96
107
  if (result !== 'AUTHORIZED') {
97
108
  const error = new UnauthorizedError();
@@ -227,9 +238,10 @@ export class SseMCPTransport implements MCPTransport {
227
238
  headers,
228
239
  body: JSON.stringify(message),
229
240
  signal: this.abortController?.signal,
241
+ redirect: this.redirectMode,
230
242
  };
231
243
 
232
- const response = await fetch(endpoint, init);
244
+ const response = await this.fetchFn(endpoint.href, init);
233
245
 
234
246
  if (response.status === 401 && this.authProvider && !triedAuth) {
235
247
  this.resourceMetadataUrl = extractResourceMetadataUrl(response);
@@ -237,6 +249,7 @@ export class SseMCPTransport implements MCPTransport {
237
249
  const result = await auth(this.authProvider, {
238
250
  serverUrl: this.url,
239
251
  resourceMetadataUrl: this.resourceMetadataUrl,
252
+ fetchFn: this.fetchFn,
240
253
  });
241
254
  if (result !== 'AUTHORIZED') {
242
255
  const error = new UnauthorizedError();
@@ -1,3 +1,4 @@
1
+ import { FetchFunction } from '@ai-sdk/provider-utils';
1
2
  import { MCPClientError } from '../error/mcp-client-error';
2
3
  import { JSONRPCMessage } from './json-rpc-message';
3
4
  import { SseMCPTransport } from './mcp-sse-transport';
@@ -58,6 +59,21 @@ export type MCPTransportConfig = {
58
59
  * An optional OAuth client provider to use for authentication for MCP servers.
59
60
  */
60
61
  authProvider?: OAuthClientProvider;
62
+
63
+ /**
64
+ * Controls how HTTP redirects are handled for transport requests.
65
+ * - `'follow'`: Follow redirects automatically (standard fetch behavior).
66
+ * - `'error'`: Reject any redirect response with an error.
67
+ * @default 'error'
68
+ */
69
+ redirect?: 'follow' | 'error';
70
+
71
+ /**
72
+ * Optional custom fetch implementation to use for HTTP requests.
73
+ * Useful for runtimes that need a request-local fetch.
74
+ * @default globalThis.fetch
75
+ */
76
+ fetch?: FetchFunction;
61
77
  };
62
78
 
63
79
  export function createMcpTransport(config: MCPTransportConfig): MCPTransport {
@@ -5,7 +5,6 @@ import {
5
5
  MCPTool,
6
6
  MCPResource,
7
7
  MCPPrompt,
8
- GetPromptResult,
9
8
  CallToolResult,
10
9
  LATEST_PROTOCOL_VERSION,
11
10
  } from './types';
package/src/tool/oauth.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  import {
25
25
  resourceUrlFromServerUrl,
26
26
  checkResourceAllowed,
27
+ resourceUrlStripSlash,
27
28
  } from '../util/oauth-util';
28
29
  import { LATEST_PROTOCOL_VERSION } from './types';
29
30
  import { FetchFunction } from '@ai-sdk/provider-utils';
@@ -451,7 +452,10 @@ export async function startAuthorization(
451
452
  }
452
453
 
453
454
  if (resource) {
454
- authorizationUrl.searchParams.set('resource', resource.href);
455
+ authorizationUrl.searchParams.set(
456
+ 'resource',
457
+ resourceUrlStripSlash(resource),
458
+ );
455
459
  }
456
460
 
457
461
  return { authorizationUrl, codeVerifier };
@@ -675,7 +679,7 @@ export async function exchangeAuthorization(
675
679
  }
676
680
 
677
681
  if (resource) {
678
- params.set('resource', resource.href);
682
+ params.set('resource', resourceUrlStripSlash(resource));
679
683
  }
680
684
 
681
685
  const response = await (fetchFn ?? fetch)(tokenUrl, {
@@ -762,7 +766,7 @@ export async function refreshAuthorization(
762
766
  }
763
767
 
764
768
  if (resource) {
765
- params.set('resource', resource.href);
769
+ params.set('resource', resourceUrlStripSlash(resource));
766
770
  }
767
771
 
768
772
  const response = await (fetchFn ?? fetch)(tokenUrl, {
package/src/tool/types.ts CHANGED
@@ -56,8 +56,10 @@ export type McpToolSet<TOOL_SCHEMAS extends ToolSchemas = 'automatic'> =
56
56
  const ClientOrServerImplementationSchema = z.looseObject({
57
57
  name: z.string(),
58
58
  version: z.string(),
59
+ title: z.optional(z.string()),
59
60
  });
60
61
 
62
+ // Maps to `Implementation` in the MCP specification
61
63
  export type Configuration = z.infer<typeof ClientOrServerImplementationSchema>;
62
64
 
63
65
  export const BaseParamsSchema = z.looseObject({
@@ -14,6 +14,19 @@ export function resourceUrlFromServerUrl(url: URL | string): URL {
14
14
  return resourceURL;
15
15
  }
16
16
 
17
+ /**
18
+ * Serializes a resource URL to a string, removing the trailing slash that
19
+ * URL.href adds to pathless URLs. Per the MCP spec, implementations SHOULD
20
+ * use the form without the trailing slash for better interoperability.
21
+ */
22
+ export function resourceUrlStripSlash(resource: URL): string {
23
+ const href = resource.href;
24
+ if (resource.pathname === '/' && href.endsWith('/')) {
25
+ return href.slice(0, -1);
26
+ }
27
+ return href;
28
+ }
29
+
17
30
  /**
18
31
  * Checks if a requested resource URL matches a configured resource URL.
19
32
  * A requested resource matches if it has the same scheme, domain, port,