@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.
@@ -2,19 +2,24 @@ 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 { parseJSONRPCMessage, type JSONRPCMessage } from './json-rpc-message';
9
+ import type { MCPTransport } from './mcp-transport';
9
10
  import { VERSION } from '../version';
10
11
  import {
11
- OAuthClientProvider,
12
12
  extractResourceMetadataUrl,
13
13
  UnauthorizedError,
14
14
  auth,
15
+ type OAuthClientProvider,
15
16
  } from './oauth';
16
17
  import { LATEST_PROTOCOL_VERSION } from './types';
17
18
 
19
+ function isMessageEvent(event: string | undefined): boolean {
20
+ return event === undefined || event === 'message';
21
+ }
22
+
18
23
  export class SseMCPTransport implements MCPTransport {
19
24
  private endpoint?: URL;
20
25
  private abortController?: AbortController;
@@ -26,23 +31,36 @@ export class SseMCPTransport implements MCPTransport {
26
31
  private headers?: Record<string, string>;
27
32
  private authProvider?: OAuthClientProvider;
28
33
  private resourceMetadataUrl?: URL;
34
+ private redirectMode: RequestRedirect;
35
+ private fetchFn: FetchFunction;
29
36
 
30
37
  onclose?: () => void;
31
38
  onerror?: (error: unknown) => void;
32
39
  onmessage?: (message: JSONRPCMessage) => void;
40
+ protocolVersion?: string;
33
41
 
34
42
  constructor({
35
43
  url,
36
44
  headers,
37
45
  authProvider,
46
+ redirect = 'error',
47
+ fetch: fetchFn,
38
48
  }: {
39
49
  url: string;
40
50
  headers?: Record<string, string>;
41
51
  authProvider?: OAuthClientProvider;
52
+ redirect?: 'follow' | 'error';
53
+ fetch?: FetchFunction;
42
54
  }) {
43
55
  this.url = new URL(url);
44
56
  this.headers = headers;
45
57
  this.authProvider = authProvider;
58
+ this.redirectMode = redirect;
59
+ this.fetchFn = fetchFn ?? globalThis.fetch;
60
+ }
61
+
62
+ setProtocolVersion(version: string): void {
63
+ this.protocolVersion = version;
46
64
  }
47
65
 
48
66
  private async commonHeaders(
@@ -51,7 +69,7 @@ export class SseMCPTransport implements MCPTransport {
51
69
  const headers: Record<string, string> = {
52
70
  ...this.headers,
53
71
  ...base,
54
- 'mcp-protocol-version': LATEST_PROTOCOL_VERSION,
72
+ 'mcp-protocol-version': this.protocolVersion ?? LATEST_PROTOCOL_VERSION,
55
73
  };
56
74
 
57
75
  if (this.authProvider) {
@@ -81,9 +99,10 @@ export class SseMCPTransport implements MCPTransport {
81
99
  const headers = await this.commonHeaders({
82
100
  Accept: 'text/event-stream',
83
101
  });
84
- const response = await fetch(this.url.href, {
102
+ const response = await this.fetchFn(this.url.href, {
85
103
  headers,
86
104
  signal: this.abortController?.signal,
105
+ redirect: this.redirectMode,
87
106
  });
88
107
 
89
108
  if (response.status === 401 && this.authProvider && !triedAuth) {
@@ -92,6 +111,7 @@ export class SseMCPTransport implements MCPTransport {
92
111
  const result = await auth(this.authProvider, {
93
112
  serverUrl: this.url,
94
113
  resourceMetadataUrl: this.resourceMetadataUrl,
114
+ fetchFn: this.fetchFn,
95
115
  });
96
116
  if (result !== 'AUTHORIZED') {
97
117
  const error = new UnauthorizedError();
@@ -145,21 +165,28 @@ export class SseMCPTransport implements MCPTransport {
145
165
  const { event, data } = value;
146
166
 
147
167
  if (event === 'endpoint') {
148
- this.endpoint = new URL(data, this.url);
168
+ if (this.endpoint) {
169
+ continue;
170
+ }
171
+
172
+ const endpoint = new URL(data, this.url);
149
173
 
150
- if (this.endpoint.origin !== this.url.origin) {
174
+ if (endpoint.origin !== this.url.origin) {
175
+ this.connected = false;
176
+ this.endpoint = undefined;
177
+ this.sseConnection?.close();
178
+ this.abortController?.abort();
151
179
  throw new MCPClientError({
152
- message: `MCP SSE Transport Error: Endpoint origin does not match connection origin: ${this.endpoint.origin}`,
180
+ message: `MCP SSE Transport Error: Endpoint origin does not match connection origin: ${endpoint.origin}`,
153
181
  });
154
182
  }
155
183
 
184
+ this.endpoint = endpoint;
156
185
  this.connected = true;
157
186
  resolve();
158
- } else if (event === 'message') {
187
+ } else if (isMessageEvent(event)) {
159
188
  try {
160
- const message = JSONRPCMessageSchema.parse(
161
- JSON.parse(data),
162
- );
189
+ const message = await parseJSONRPCMessage(data);
163
190
  this.onmessage?.(message);
164
191
  } catch (error) {
165
192
  const e = new MCPClientError({
@@ -203,6 +230,7 @@ export class SseMCPTransport implements MCPTransport {
203
230
 
204
231
  async close(): Promise<void> {
205
232
  this.connected = false;
233
+ this.endpoint = undefined;
206
234
  this.sseConnection?.close();
207
235
  this.abortController?.abort();
208
236
  this.onclose?.();
@@ -227,9 +255,10 @@ export class SseMCPTransport implements MCPTransport {
227
255
  headers,
228
256
  body: JSON.stringify(message),
229
257
  signal: this.abortController?.signal,
258
+ redirect: this.redirectMode,
230
259
  };
231
260
 
232
- const response = await fetch(endpoint, init);
261
+ const response = await this.fetchFn(endpoint.href, init);
233
262
 
234
263
  if (response.status === 401 && this.authProvider && !triedAuth) {
235
264
  this.resourceMetadataUrl = extractResourceMetadataUrl(response);
@@ -237,6 +266,7 @@ export class SseMCPTransport implements MCPTransport {
237
266
  const result = await auth(this.authProvider, {
238
267
  serverUrl: this.url,
239
268
  resourceMetadataUrl: this.resourceMetadataUrl,
269
+ fetchFn: this.fetchFn,
240
270
  });
241
271
  if (result !== 'AUTHORIZED') {
242
272
  const error = new UnauthorizedError();
@@ -267,6 +297,8 @@ export class SseMCPTransport implements MCPTransport {
267
297
  }
268
298
  }
269
299
 
270
- export function deserializeMessage(line: string): JSONRPCMessage {
271
- return JSONRPCMessageSchema.parse(JSON.parse(line));
300
+ export async function deserializeMessage(
301
+ line: string,
302
+ ): Promise<JSONRPCMessage> {
303
+ return parseJSONRPCMessage(line);
272
304
  }
@@ -1,6 +1,6 @@
1
- import { ChildProcess, spawn } from 'node:child_process';
1
+ import { spawn, type ChildProcess } from 'node:child_process';
2
2
  import { getEnvironment } from './get-environment';
3
- import { StdioConfig } from './mcp-stdio-transport';
3
+ import type { StdioConfig } from './mcp-stdio-transport';
4
4
 
5
5
  export function createChildProcess(
6
6
  config: StdioConfig,
@@ -1,7 +1,7 @@
1
1
  import type { ChildProcess, IOType } from 'node:child_process';
2
- import { Stream } from 'node:stream';
3
- import { JSONRPCMessage, JSONRPCMessageSchema } from '../json-rpc-message';
4
- import { MCPTransport } from '../mcp-transport';
2
+ import type { Stream } from 'node:stream';
3
+ import { parseJSONRPCMessage, type JSONRPCMessage } from '../json-rpc-message';
4
+ import type { MCPTransport } from '../mcp-transport';
5
5
  import { MCPClientError } from '../../error/mcp-client-error';
6
6
  import { createChildProcess } from './create-child-process';
7
7
 
@@ -68,7 +68,7 @@ export class StdioMCPTransport implements MCPTransport {
68
68
 
69
69
  this.process.stdout?.on('data', chunk => {
70
70
  this.readBuffer.append(chunk);
71
- this.processReadBuffer();
71
+ void this.processReadBuffer();
72
72
  });
73
73
 
74
74
  this.process.stdout?.on('error', error => {
@@ -81,14 +81,15 @@ export class StdioMCPTransport implements MCPTransport {
81
81
  });
82
82
  }
83
83
 
84
- private processReadBuffer() {
84
+ private async processReadBuffer() {
85
85
  while (true) {
86
- try {
87
- const message = this.readBuffer.readMessage();
88
- if (message === null) {
89
- break;
90
- }
86
+ const line = this.readBuffer.readLine();
87
+ if (line === null) {
88
+ break;
89
+ }
91
90
 
91
+ try {
92
+ const message = await deserializeMessage(line);
92
93
  this.onmessage?.(message);
93
94
  } catch (error) {
94
95
  this.onerror?.(error as Error);
@@ -127,7 +128,7 @@ class ReadBuffer {
127
128
  this.buffer = this.buffer ? Buffer.concat([this.buffer, chunk]) : chunk;
128
129
  }
129
130
 
130
- readMessage(): JSONRPCMessage | null {
131
+ readLine(): string | null {
131
132
  if (!this.buffer) return null;
132
133
 
133
134
  const index = this.buffer.indexOf('\n');
@@ -137,7 +138,7 @@ class ReadBuffer {
137
138
 
138
139
  const line = this.buffer.toString('utf8', 0, index);
139
140
  this.buffer = this.buffer.subarray(index + 1);
140
- return deserializeMessage(line);
141
+ return line;
141
142
  }
142
143
 
143
144
  clear(): void {
@@ -149,6 +150,8 @@ function serializeMessage(message: JSONRPCMessage): string {
149
150
  return JSON.stringify(message) + '\n';
150
151
  }
151
152
 
152
- export function deserializeMessage(line: string): JSONRPCMessage {
153
- return JSONRPCMessageSchema.parse(JSON.parse(line));
153
+ export async function deserializeMessage(
154
+ line: string,
155
+ ): Promise<JSONRPCMessage> {
156
+ return parseJSONRPCMessage(line);
154
157
  }
@@ -1,8 +1,9 @@
1
+ import type { FetchFunction } from '@ai-sdk/provider-utils';
1
2
  import { MCPClientError } from '../error/mcp-client-error';
2
- import { JSONRPCMessage } from './json-rpc-message';
3
+ import type { JSONRPCMessage } from './json-rpc-message';
3
4
  import { SseMCPTransport } from './mcp-sse-transport';
4
5
  import { HttpMCPTransport } from './mcp-http-transport';
5
- import { OAuthClientProvider } from './oauth';
6
+ import type { OAuthClientProvider } from './oauth';
6
7
 
7
8
  /**
8
9
  * Transport interface for MCP (Model Context Protocol) communication.
@@ -39,6 +40,16 @@ export interface MCPTransport {
39
40
  * Event handler for received messages
40
41
  */
41
42
  onmessage?: (message: JSONRPCMessage) => void;
43
+
44
+ /**
45
+ * The protocol version negotiated during initialization.
46
+ */
47
+ protocolVersion?: string;
48
+
49
+ /**
50
+ * Set the protocol version negotiated during initialization.
51
+ */
52
+ setProtocolVersion?(version: string): void;
42
53
  }
43
54
 
44
55
  export type MCPTransportConfig = {
@@ -58,6 +69,21 @@ export type MCPTransportConfig = {
58
69
  * An optional OAuth client provider to use for authentication for MCP servers.
59
70
  */
60
71
  authProvider?: OAuthClientProvider;
72
+
73
+ /**
74
+ * Controls how HTTP redirects are handled for transport requests.
75
+ * - `'follow'`: Follow redirects automatically (standard fetch behavior).
76
+ * - `'error'`: Reject any redirect response with an error.
77
+ * @default 'error'
78
+ */
79
+ redirect?: 'follow' | 'error';
80
+
81
+ /**
82
+ * Optional custom fetch implementation to use for HTTP requests.
83
+ * Useful for runtimes that need a request-local fetch.
84
+ * @default globalThis.fetch
85
+ */
86
+ fetch?: FetchFunction;
61
87
  };
62
88
 
63
89
  export function createMcpTransport(config: MCPTransportConfig): MCPTransport {
@@ -1,15 +1,13 @@
1
1
  import { delay } from '@ai-sdk/provider-utils';
2
- import { JSONRPCMessage } from './json-rpc-message';
3
- import { MCPTransport } from './mcp-transport';
2
+ import type { JSONRPCMessage } from './json-rpc-message';
3
+ import type { MCPTransport } from './mcp-transport';
4
4
  import {
5
- MCPTool,
6
- MCPResource,
7
- MCPPrompt,
8
- GetPromptResult,
9
- CallToolResult,
10
5
  LATEST_PROTOCOL_VERSION,
6
+ type MCPTool,
7
+ type MCPResource,
8
+ type MCPPrompt,
9
+ type CallToolResult,
11
10
  } from './types';
12
-
13
11
  const DEFAULT_TOOLS: MCPTool[] = [
14
12
  {
15
13
  name: 'mock-tool',
@@ -45,7 +43,7 @@ export class MockMCPTransport implements MCPTransport {
45
43
  onmessage?: (message: JSONRPCMessage) => void;
46
44
  onclose?: () => void;
47
45
  onerror?: (error: Error) => void;
48
-
46
+ protocolVersion?: string;
49
47
  constructor({
50
48
  overrideTools = DEFAULT_TOOLS,
51
49
  resources = [
@@ -122,6 +120,7 @@ export class MockMCPTransport implements MCPTransport {
122
120
  name?: string;
123
121
  title?: string;
124
122
  mimeType?: string;
123
+ _meta?: Record<string, unknown>;
125
124
  } & ({ text: string } | { blob: string })
126
125
  >;
127
126
  failOnInvalidToolParams?: boolean;
@@ -1,18 +1,4 @@
1
1
  import { z } from 'zod/v4';
2
- /**
3
- * OAuth 2.1 token response
4
- */
5
- export const OAuthTokensSchema = z
6
- .object({
7
- access_token: z.string(),
8
- id_token: z.string().optional(), // Optional for OAuth 2.1, but necessary in OpenID Connect
9
- token_type: z.string(),
10
- expires_in: z.number().optional(),
11
- scope: z.string().optional(),
12
- refresh_token: z.string().optional(),
13
- })
14
- .strip();
15
-
16
2
  /**
17
3
  * Reusable URL validation that disallows javascript: scheme
18
4
  */
@@ -32,16 +18,32 @@ export const SafeUrlSchema = z
32
18
  })
33
19
  .refine(
34
20
  url => {
35
- const u = new URL(url);
21
+ const parsedUrl = new URL(url);
36
22
  return (
37
- u.protocol !== 'javascript:' &&
38
- u.protocol !== 'data:' &&
39
- u.protocol !== 'vbscript:'
23
+ parsedUrl.protocol !== 'javascript:' &&
24
+ parsedUrl.protocol !== 'data:' &&
25
+ parsedUrl.protocol !== 'vbscript:'
40
26
  );
41
27
  },
42
28
  { message: 'URL cannot use javascript:, data:, or vbscript: scheme' },
43
29
  );
44
30
 
31
+ /**
32
+ * OAuth 2.1 token response
33
+ */
34
+ export const OAuthTokensSchema = z
35
+ .object({
36
+ access_token: z.string(),
37
+ id_token: z.string().optional(), // Optional for OAuth 2.1, but necessary in OpenID Connect
38
+ token_type: z.string(),
39
+ expires_in: z.number().optional(),
40
+ scope: z.string().optional(),
41
+ refresh_token: z.string().optional(),
42
+ authorization_server: SafeUrlSchema.optional(),
43
+ token_endpoint: SafeUrlSchema.optional(),
44
+ })
45
+ .strip();
46
+
45
47
  export const OAuthProtectedResourceMetadataSchema = z
46
48
  .object({
47
49
  resource: z.string().url(),
@@ -118,6 +120,8 @@ export const OAuthClientInformationSchema = z
118
120
  client_secret: z.string().optional(),
119
121
  client_id_issued_at: z.number().optional(),
120
122
  client_secret_expires_at: z.number().optional(),
123
+ authorization_server: SafeUrlSchema.optional(),
124
+ token_endpoint: SafeUrlSchema.optional(),
121
125
  })
122
126
  .strip();
123
127