@ai-sdk/mcp 2.0.0-beta.2 → 2.0.0-beta.21

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.
@@ -18,11 +18,11 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
 
20
20
  // src/tool/mcp-stdio/index.ts
21
- var mcp_stdio_exports = {};
22
- __export(mcp_stdio_exports, {
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
23
  Experimental_StdioMCPTransport: () => StdioMCPTransport
24
24
  });
25
- module.exports = __toCommonJS(mcp_stdio_exports);
25
+ module.exports = __toCommonJS(index_exports);
26
26
 
27
27
  // src/tool/json-rpc-message.ts
28
28
  var import_v42 = require("zod/v4");
@@ -257,7 +257,7 @@ var MCPClientError = class extends (_b = AISDKError, _a = symbol, _b) {
257
257
  };
258
258
 
259
259
  // src/tool/mcp-stdio/create-child-process.ts
260
- import { spawn } from "node:child_process";
260
+ import { spawn } from "child_process";
261
261
 
262
262
  // src/tool/mcp-stdio/get-environment.ts
263
263
  function getEnvironment(customEnv) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/mcp",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.0-beta.21",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -33,17 +33,17 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "pkce-challenge": "^5.0.0",
36
- "@ai-sdk/provider-utils": "5.0.0-beta.1",
37
- "@ai-sdk/provider": "4.0.0-beta.0"
36
+ "@ai-sdk/provider": "4.0.0-beta.9",
37
+ "@ai-sdk/provider-utils": "5.0.0-beta.15"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "20.17.24",
41
41
  "tsup": "^8",
42
42
  "typescript": "5.8.3",
43
- "vitest": "2.1.4",
43
+ "vitest": "^4.1.0",
44
44
  "zod": "3.25.76",
45
- "@vercel/ai-tsconfig": "0.0.0",
46
- "@ai-sdk/test-server": "2.0.0-beta.0"
45
+ "@ai-sdk/test-server": "2.0.0-beta.0",
46
+ "@vercel/ai-tsconfig": "0.0.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "zod": "^3.25.76 || ^4.1.8"
@@ -70,9 +70,7 @@
70
70
  "build": "pnpm clean && tsup --tsconfig tsconfig.build.json",
71
71
  "build:watch": "pnpm clean && tsup --watch",
72
72
  "clean": "rm -rf dist *.tsbuildinfo",
73
- "lint": "eslint \"./**/*.ts*\"",
74
73
  "type-check": "tsc --build",
75
- "prettier-check": "prettier --check \"./**/*.ts*\"",
76
74
  "test": "pnpm test:node && pnpm test:edge",
77
75
  "test:update": "pnpm test:node -u",
78
76
  "test:watch": "vitest --config vitest.node.config.js",
@@ -420,7 +420,7 @@ class DefaultMCPClient implements MCPClient {
420
420
  }: {
421
421
  name: string;
422
422
  args: Record<string, unknown>;
423
- options?: ToolExecutionOptions;
423
+ options?: ToolExecutionOptions<{}>;
424
424
  }): Promise<CallToolResult> {
425
425
  try {
426
426
  return this.request({
@@ -579,11 +579,15 @@ class DefaultMCPClient implements MCPClient {
579
579
 
580
580
  const execute = async (
581
581
  args: any,
582
- options: ToolExecutionOptions,
582
+ options: ToolExecutionOptions<{}>,
583
583
  ): Promise<unknown> => {
584
584
  options?.abortSignal?.throwIfAborted();
585
585
  const result = await self.callTool({ name, args, options });
586
586
 
587
+ if (result.isError) {
588
+ return result;
589
+ }
590
+
587
591
  if (outputSchema != null) {
588
592
  return self.extractStructuredContent(result, outputSchema, name);
589
593
  }
@@ -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 {
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';
@@ -83,6 +84,8 @@ export interface OAuthClientProvider {
83
84
  clientInformation: OAuthClientInformation,
84
85
  ): void | Promise<void>;
85
86
  state?(): string | Promise<string>;
87
+ saveState?(state: string): void | Promise<void>;
88
+ storedState?(): string | undefined | Promise<string | undefined>;
86
89
  validateResourceURL?(
87
90
  serverUrl: string | URL,
88
91
  resource?: string,
@@ -449,7 +452,10 @@ export async function startAuthorization(
449
452
  }
450
453
 
451
454
  if (resource) {
452
- authorizationUrl.searchParams.set('resource', resource.href);
455
+ authorizationUrl.searchParams.set(
456
+ 'resource',
457
+ resourceUrlStripSlash(resource),
458
+ );
453
459
  }
454
460
 
455
461
  return { authorizationUrl, codeVerifier };
@@ -673,7 +679,7 @@ export async function exchangeAuthorization(
673
679
  }
674
680
 
675
681
  if (resource) {
676
- params.set('resource', resource.href);
682
+ params.set('resource', resourceUrlStripSlash(resource));
677
683
  }
678
684
 
679
685
  const response = await (fetchFn ?? fetch)(tokenUrl, {
@@ -760,7 +766,7 @@ export async function refreshAuthorization(
760
766
  }
761
767
 
762
768
  if (resource) {
763
- params.set('resource', resource.href);
769
+ params.set('resource', resourceUrlStripSlash(resource));
764
770
  }
765
771
 
766
772
  const response = await (fetchFn ?? fetch)(tokenUrl, {
@@ -827,6 +833,7 @@ export async function auth(
827
833
  options: {
828
834
  serverUrl: string | URL;
829
835
  authorizationCode?: string;
836
+ callbackState?: string;
830
837
  scope?: string;
831
838
  resourceMetadataUrl?: URL;
832
839
  fetchFn?: FetchFunction;
@@ -886,12 +893,14 @@ async function authInternal(
886
893
  {
887
894
  serverUrl,
888
895
  authorizationCode,
896
+ callbackState,
889
897
  scope,
890
898
  resourceMetadataUrl,
891
899
  fetchFn,
892
900
  }: {
893
901
  serverUrl: string | URL;
894
902
  authorizationCode?: string;
903
+ callbackState?: string;
895
904
  scope?: string;
896
905
  resourceMetadataUrl?: URL;
897
906
  fetchFn?: FetchFunction;
@@ -960,6 +969,15 @@ async function authInternal(
960
969
 
961
970
  // Exchange authorization code for tokens
962
971
  if (authorizationCode !== undefined) {
972
+ if (provider.storedState) {
973
+ const expectedState = await provider.storedState();
974
+ if (expectedState !== undefined && expectedState !== callbackState) {
975
+ throw new Error(
976
+ 'OAuth state parameter mismatch - possible CSRF attack',
977
+ );
978
+ }
979
+ }
980
+
963
981
  const codeVerifier = await provider.codeVerifier();
964
982
  const tokens = await exchangeAuthorization(authorizationServerUrl, {
965
983
  metadata,
@@ -1008,6 +1026,9 @@ async function authInternal(
1008
1026
  }
1009
1027
 
1010
1028
  const state = provider.state ? await provider.state() : undefined;
1029
+ if (state && provider.saveState) {
1030
+ await provider.saveState(state);
1031
+ }
1011
1032
 
1012
1033
  // Start new authorization flow
1013
1034
  const { authorizationUrl, codeVerifier } = await startAuthorization(
@@ -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,