@ai-sdk/mcp 2.0.0-beta.6 → 2.0.0-beta.67

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;
@@ -27,26 +32,35 @@ export class SseMCPTransport implements MCPTransport {
27
32
  private authProvider?: OAuthClientProvider;
28
33
  private resourceMetadataUrl?: URL;
29
34
  private redirectMode: RequestRedirect;
35
+ private fetchFn: FetchFunction;
30
36
 
31
37
  onclose?: () => void;
32
38
  onerror?: (error: unknown) => void;
33
39
  onmessage?: (message: JSONRPCMessage) => void;
40
+ protocolVersion?: string;
34
41
 
35
42
  constructor({
36
43
  url,
37
44
  headers,
38
45
  authProvider,
39
- redirect = 'follow',
46
+ redirect = 'error',
47
+ fetch: fetchFn,
40
48
  }: {
41
49
  url: string;
42
50
  headers?: Record<string, string>;
43
51
  authProvider?: OAuthClientProvider;
44
52
  redirect?: 'follow' | 'error';
53
+ fetch?: FetchFunction;
45
54
  }) {
46
55
  this.url = new URL(url);
47
56
  this.headers = headers;
48
57
  this.authProvider = authProvider;
49
58
  this.redirectMode = redirect;
59
+ this.fetchFn = fetchFn ?? globalThis.fetch;
60
+ }
61
+
62
+ setProtocolVersion(version: string): void {
63
+ this.protocolVersion = version;
50
64
  }
51
65
 
52
66
  private async commonHeaders(
@@ -55,7 +69,7 @@ export class SseMCPTransport implements MCPTransport {
55
69
  const headers: Record<string, string> = {
56
70
  ...this.headers,
57
71
  ...base,
58
- 'mcp-protocol-version': LATEST_PROTOCOL_VERSION,
72
+ 'mcp-protocol-version': this.protocolVersion ?? LATEST_PROTOCOL_VERSION,
59
73
  };
60
74
 
61
75
  if (this.authProvider) {
@@ -85,7 +99,7 @@ export class SseMCPTransport implements MCPTransport {
85
99
  const headers = await this.commonHeaders({
86
100
  Accept: 'text/event-stream',
87
101
  });
88
- const response = await fetch(this.url.href, {
102
+ const response = await this.fetchFn(this.url.href, {
89
103
  headers,
90
104
  signal: this.abortController?.signal,
91
105
  redirect: this.redirectMode,
@@ -97,6 +111,7 @@ export class SseMCPTransport implements MCPTransport {
97
111
  const result = await auth(this.authProvider, {
98
112
  serverUrl: this.url,
99
113
  resourceMetadataUrl: this.resourceMetadataUrl,
114
+ fetchFn: this.fetchFn,
100
115
  });
101
116
  if (result !== 'AUTHORIZED') {
102
117
  const error = new UnauthorizedError();
@@ -150,21 +165,28 @@ export class SseMCPTransport implements MCPTransport {
150
165
  const { event, data } = value;
151
166
 
152
167
  if (event === 'endpoint') {
153
- this.endpoint = new URL(data, this.url);
168
+ if (this.endpoint) {
169
+ continue;
170
+ }
171
+
172
+ const endpoint = new URL(data, this.url);
154
173
 
155
- 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();
156
179
  throw new MCPClientError({
157
- 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}`,
158
181
  });
159
182
  }
160
183
 
184
+ this.endpoint = endpoint;
161
185
  this.connected = true;
162
186
  resolve();
163
- } else if (event === 'message') {
187
+ } else if (isMessageEvent(event)) {
164
188
  try {
165
- const message = JSONRPCMessageSchema.parse(
166
- JSON.parse(data),
167
- );
189
+ const message = await parseJSONRPCMessage(data);
168
190
  this.onmessage?.(message);
169
191
  } catch (error) {
170
192
  const e = new MCPClientError({
@@ -208,6 +230,7 @@ export class SseMCPTransport implements MCPTransport {
208
230
 
209
231
  async close(): Promise<void> {
210
232
  this.connected = false;
233
+ this.endpoint = undefined;
211
234
  this.sseConnection?.close();
212
235
  this.abortController?.abort();
213
236
  this.onclose?.();
@@ -235,7 +258,7 @@ export class SseMCPTransport implements MCPTransport {
235
258
  redirect: this.redirectMode,
236
259
  };
237
260
 
238
- const response = await fetch(endpoint, init);
261
+ const response = await this.fetchFn(endpoint.href, init);
239
262
 
240
263
  if (response.status === 401 && this.authProvider && !triedAuth) {
241
264
  this.resourceMetadataUrl = extractResourceMetadataUrl(response);
@@ -243,6 +266,7 @@ export class SseMCPTransport implements MCPTransport {
243
266
  const result = await auth(this.authProvider, {
244
267
  serverUrl: this.url,
245
268
  resourceMetadataUrl: this.resourceMetadataUrl,
269
+ fetchFn: this.fetchFn,
246
270
  });
247
271
  if (result !== 'AUTHORIZED') {
248
272
  const error = new UnauthorizedError();
@@ -273,6 +297,8 @@ export class SseMCPTransport implements MCPTransport {
273
297
  }
274
298
  }
275
299
 
276
- export function deserializeMessage(line: string): JSONRPCMessage {
277
- return JSONRPCMessageSchema.parse(JSON.parse(line));
300
+ export async function deserializeMessage(
301
+ line: string,
302
+ ): Promise<JSONRPCMessage> {
303
+ return parseJSONRPCMessage(line);
278
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 = {
@@ -63,9 +74,16 @@ export type MCPTransportConfig = {
63
74
  * Controls how HTTP redirects are handled for transport requests.
64
75
  * - `'follow'`: Follow redirects automatically (standard fetch behavior).
65
76
  * - `'error'`: Reject any redirect response with an error.
66
- * @default 'follow'
77
+ * @default 'error'
67
78
  */
68
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;
69
87
  };
70
88
 
71
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