@ai-sdk/mcp 2.0.0 → 2.0.1
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.
- package/CHANGELOG.md +6 -0
- package/README.md +24 -1
- package/dist/index.d.ts +71 -1
- package/dist/index.js +95 -30
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/tool/mcp-client.ts +53 -15
- package/src/tool/mcp-http-transport.ts +81 -19
- package/src/tool/mcp-transport.ts +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"typescript": "5.8.3",
|
|
42
42
|
"vitest": "^4.1.6",
|
|
43
43
|
"zod": "3.25.76",
|
|
44
|
-
"@
|
|
45
|
-
"@ai-
|
|
44
|
+
"@ai-sdk/test-server": "2.0.0",
|
|
45
|
+
"@vercel/ai-tsconfig": "0.0.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"zod": "^3.25.76 || ^4.1.8"
|
package/src/index.ts
CHANGED
package/src/tool/mcp-client.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
type ToolSchemas,
|
|
61
61
|
type ToolMeta,
|
|
62
62
|
type McpProviderMetadata,
|
|
63
|
+
type InitializeResult,
|
|
63
64
|
} from './types';
|
|
64
65
|
const CLIENT_VERSION = '1.0.0';
|
|
65
66
|
|
|
@@ -100,6 +101,12 @@ export interface MCPClientConfig {
|
|
|
100
101
|
transport: MCPTransportConfig | MCPTransport;
|
|
101
102
|
/** Optional callback for uncaught errors */
|
|
102
103
|
onUncaughtError?: (error: unknown) => void;
|
|
104
|
+
/**
|
|
105
|
+
* Initialize result from a previous MCP session. When provided, the client
|
|
106
|
+
* starts the transport and reuses this metadata without sending a new
|
|
107
|
+
* initialize request.
|
|
108
|
+
*/
|
|
109
|
+
initialInitializeResult?: InitializeResult;
|
|
103
110
|
/** Optional client name, defaults to 'ai-sdk-mcp-client' */
|
|
104
111
|
clientName?: string;
|
|
105
112
|
/**
|
|
@@ -133,6 +140,12 @@ export interface MCPClient {
|
|
|
133
140
|
*/
|
|
134
141
|
readonly serverInfo: Configuration;
|
|
135
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The full initialize result used by this client, either from the server
|
|
145
|
+
* during initialization or from `initialInitializeResult`.
|
|
146
|
+
*/
|
|
147
|
+
readonly initializeResult: InitializeResult;
|
|
148
|
+
|
|
136
149
|
/**
|
|
137
150
|
* Optional instructions provided by the server during the initialize handshake.
|
|
138
151
|
*
|
|
@@ -220,7 +233,7 @@ export interface MCPClient {
|
|
|
220
233
|
*
|
|
221
234
|
* Not supported:
|
|
222
235
|
* - Accepting notifications
|
|
223
|
-
* -
|
|
236
|
+
* - Automatic session persistence for Streamable HTTP transport
|
|
224
237
|
* - Resumable SSE streams
|
|
225
238
|
*/
|
|
226
239
|
class DefaultMCPClient implements MCPClient {
|
|
@@ -228,6 +241,7 @@ class DefaultMCPClient implements MCPClient {
|
|
|
228
241
|
private onUncaughtError?: (error: unknown) => void;
|
|
229
242
|
private clientInfo: ClientConfiguration;
|
|
230
243
|
private clientCapabilities: ClientCapabilities;
|
|
244
|
+
private initialInitializeResult?: InitializeResult;
|
|
231
245
|
private requestMessageId = 0;
|
|
232
246
|
private responseHandlers: Map<
|
|
233
247
|
number,
|
|
@@ -235,6 +249,11 @@ class DefaultMCPClient implements MCPClient {
|
|
|
235
249
|
> = new Map();
|
|
236
250
|
private serverCapabilities: ServerCapabilities = {};
|
|
237
251
|
private _serverInfo: Configuration = { name: '', version: '' };
|
|
252
|
+
private _initializeResult: InitializeResult = {
|
|
253
|
+
protocolVersion: LATEST_PROTOCOL_VERSION,
|
|
254
|
+
capabilities: {},
|
|
255
|
+
serverInfo: this._serverInfo,
|
|
256
|
+
};
|
|
238
257
|
private _serverInstructions?: string;
|
|
239
258
|
private isClosed = true;
|
|
240
259
|
private elicitationRequestHandler?: (
|
|
@@ -248,9 +267,11 @@ class DefaultMCPClient implements MCPClient {
|
|
|
248
267
|
version = CLIENT_VERSION,
|
|
249
268
|
onUncaughtError,
|
|
250
269
|
capabilities,
|
|
270
|
+
initialInitializeResult,
|
|
251
271
|
}: MCPClientConfig) {
|
|
252
272
|
this.onUncaughtError = onUncaughtError;
|
|
253
273
|
this.clientCapabilities = capabilities ?? {};
|
|
274
|
+
this.initialInitializeResult = initialInitializeResult;
|
|
254
275
|
|
|
255
276
|
if (isCustomMcpTransport(transportConfig)) {
|
|
256
277
|
this.transport = transportConfig;
|
|
@@ -287,6 +308,10 @@ class DefaultMCPClient implements MCPClient {
|
|
|
287
308
|
return this._serverInfo;
|
|
288
309
|
}
|
|
289
310
|
|
|
311
|
+
get initializeResult(): InitializeResult {
|
|
312
|
+
return this._initializeResult;
|
|
313
|
+
}
|
|
314
|
+
|
|
290
315
|
get instructions(): string | undefined {
|
|
291
316
|
return this._serverInstructions;
|
|
292
317
|
}
|
|
@@ -296,6 +321,14 @@ class DefaultMCPClient implements MCPClient {
|
|
|
296
321
|
await this.transport.start();
|
|
297
322
|
this.isClosed = false;
|
|
298
323
|
|
|
324
|
+
if (this.initialInitializeResult) {
|
|
325
|
+
const result = InitializeResultSchema.parse(
|
|
326
|
+
this.initialInitializeResult,
|
|
327
|
+
);
|
|
328
|
+
this.applyInitializeResult(result);
|
|
329
|
+
return this;
|
|
330
|
+
}
|
|
331
|
+
|
|
299
332
|
const result = await this.request({
|
|
300
333
|
request: {
|
|
301
334
|
method: 'initialize',
|
|
@@ -314,20 +347,7 @@ class DefaultMCPClient implements MCPClient {
|
|
|
314
347
|
});
|
|
315
348
|
}
|
|
316
349
|
|
|
317
|
-
|
|
318
|
-
throw new MCPClientError({
|
|
319
|
-
message: `Server's protocol version is not supported: ${result.protocolVersion}`,
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
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;
|
|
350
|
+
this.applyInitializeResult(result);
|
|
331
351
|
|
|
332
352
|
// Complete initialization handshake:
|
|
333
353
|
await this.notification({
|
|
@@ -341,6 +361,24 @@ class DefaultMCPClient implements MCPClient {
|
|
|
341
361
|
}
|
|
342
362
|
}
|
|
343
363
|
|
|
364
|
+
private applyInitializeResult(result: InitializeResult): void {
|
|
365
|
+
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
|
|
366
|
+
throw new MCPClientError({
|
|
367
|
+
message: `Server's protocol version is not supported: ${result.protocolVersion}`,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.serverCapabilities = result.capabilities;
|
|
372
|
+
this._serverInfo = result.serverInfo;
|
|
373
|
+
this._initializeResult = result;
|
|
374
|
+
if (this.transport.setProtocolVersion) {
|
|
375
|
+
this.transport.setProtocolVersion(result.protocolVersion);
|
|
376
|
+
} else {
|
|
377
|
+
this.transport.protocolVersion = result.protocolVersion;
|
|
378
|
+
}
|
|
379
|
+
this._serverInstructions = result.instructions;
|
|
380
|
+
}
|
|
381
|
+
|
|
344
382
|
async close(): Promise<void> {
|
|
345
383
|
if (this.isClosed) return;
|
|
346
384
|
await this.transport?.close();
|
|
@@ -43,6 +43,9 @@ export class HttpMCPTransport implements MCPTransport {
|
|
|
43
43
|
private redirectMode: RequestRedirect;
|
|
44
44
|
private fetchFn: FetchFunction;
|
|
45
45
|
private authPromise?: Promise<AuthResult>;
|
|
46
|
+
private onSessionIdChange?: (sessionId: string | undefined) => void;
|
|
47
|
+
private onSessionExpired?: (sessionId: string) => void;
|
|
48
|
+
private terminateSessionOnClose: boolean;
|
|
46
49
|
|
|
47
50
|
// Inbound SSE resumption and reconnection state
|
|
48
51
|
private lastInboundEventId?: string;
|
|
@@ -64,18 +67,33 @@ export class HttpMCPTransport implements MCPTransport {
|
|
|
64
67
|
headers,
|
|
65
68
|
authProvider,
|
|
66
69
|
redirect = 'error',
|
|
70
|
+
initialSessionId,
|
|
71
|
+
initialProtocolVersion,
|
|
72
|
+
onSessionIdChange,
|
|
73
|
+
onSessionExpired,
|
|
74
|
+
terminateSessionOnClose = true,
|
|
67
75
|
fetch: fetchFn,
|
|
68
76
|
}: {
|
|
69
77
|
url: string;
|
|
70
78
|
headers?: Record<string, string>;
|
|
71
79
|
authProvider?: OAuthClientProvider;
|
|
72
80
|
redirect?: 'follow' | 'error';
|
|
81
|
+
initialSessionId?: string;
|
|
82
|
+
initialProtocolVersion?: string;
|
|
83
|
+
onSessionIdChange?: (sessionId: string | undefined) => void;
|
|
84
|
+
onSessionExpired?: (sessionId: string) => void;
|
|
85
|
+
terminateSessionOnClose?: boolean;
|
|
73
86
|
fetch?: FetchFunction;
|
|
74
87
|
}) {
|
|
75
88
|
this.url = new URL(url);
|
|
76
89
|
this.headers = headers;
|
|
77
90
|
this.authProvider = authProvider;
|
|
78
91
|
this.redirectMode = redirect;
|
|
92
|
+
this.sessionId = initialSessionId;
|
|
93
|
+
this.protocolVersion = initialProtocolVersion;
|
|
94
|
+
this.onSessionIdChange = onSessionIdChange;
|
|
95
|
+
this.onSessionExpired = onSessionExpired;
|
|
96
|
+
this.terminateSessionOnClose = terminateSessionOnClose;
|
|
79
97
|
this.fetchFn = fetchFn ?? globalThis.fetch;
|
|
80
98
|
}
|
|
81
99
|
|
|
@@ -83,16 +101,20 @@ export class HttpMCPTransport implements MCPTransport {
|
|
|
83
101
|
this.protocolVersion = version;
|
|
84
102
|
}
|
|
85
103
|
|
|
86
|
-
private async commonHeaders(
|
|
87
|
-
base
|
|
88
|
-
|
|
104
|
+
private async commonHeaders({
|
|
105
|
+
base,
|
|
106
|
+
includeSessionId = true,
|
|
107
|
+
}: {
|
|
108
|
+
base: Record<string, string>;
|
|
109
|
+
includeSessionId?: boolean;
|
|
110
|
+
}): Promise<Record<string, string>> {
|
|
89
111
|
const headers: Record<string, string> = {
|
|
90
112
|
...this.headers,
|
|
91
113
|
...base,
|
|
92
114
|
'mcp-protocol-version': this.protocolVersion ?? LATEST_PROTOCOL_VERSION,
|
|
93
115
|
};
|
|
94
116
|
|
|
95
|
-
if (this.sessionId) {
|
|
117
|
+
if (includeSessionId && this.sessionId) {
|
|
96
118
|
headers['mcp-session-id'] = this.sessionId;
|
|
97
119
|
}
|
|
98
120
|
|
|
@@ -110,6 +132,30 @@ export class HttpMCPTransport implements MCPTransport {
|
|
|
110
132
|
);
|
|
111
133
|
}
|
|
112
134
|
|
|
135
|
+
private setSessionId(sessionId: string | undefined): void {
|
|
136
|
+
if (this.sessionId === sessionId) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.sessionId = sessionId;
|
|
141
|
+
this.onSessionIdChange?.(sessionId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private applySessionIdFromResponse(response: Response): void {
|
|
145
|
+
const sessionId = response.headers.get('mcp-session-id');
|
|
146
|
+
if (sessionId) {
|
|
147
|
+
this.setSessionId(sessionId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private expireSessionId(sessionId: string): void {
|
|
152
|
+
if (this.sessionId === sessionId) {
|
|
153
|
+
this.setSessionId(undefined);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.onSessionExpired?.(sessionId);
|
|
157
|
+
}
|
|
158
|
+
|
|
113
159
|
/**
|
|
114
160
|
* Runs a single OAuth recovery flow for concurrent 401 responses.
|
|
115
161
|
*/
|
|
@@ -148,10 +194,11 @@ export class HttpMCPTransport implements MCPTransport {
|
|
|
148
194
|
try {
|
|
149
195
|
if (
|
|
150
196
|
this.sessionId &&
|
|
197
|
+
this.terminateSessionOnClose &&
|
|
151
198
|
this.abortController &&
|
|
152
199
|
!this.abortController.signal.aborted
|
|
153
200
|
) {
|
|
154
|
-
const headers = await this.commonHeaders({});
|
|
201
|
+
const headers = await this.commonHeaders({ base: {} });
|
|
155
202
|
await this.fetchFn(this.url.href, {
|
|
156
203
|
method: 'DELETE',
|
|
157
204
|
headers,
|
|
@@ -168,9 +215,17 @@ export class HttpMCPTransport implements MCPTransport {
|
|
|
168
215
|
async send(message: JSONRPCMessage): Promise<void> {
|
|
169
216
|
const attempt = async (triedAuth: boolean = false): Promise<void> => {
|
|
170
217
|
try {
|
|
218
|
+
const isInitializeRequest =
|
|
219
|
+
'method' in message && message.method === 'initialize';
|
|
220
|
+
const sessionIdForRequest = isInitializeRequest
|
|
221
|
+
? undefined
|
|
222
|
+
: this.sessionId;
|
|
171
223
|
const headers = await this.commonHeaders({
|
|
172
|
-
|
|
173
|
-
|
|
224
|
+
base: {
|
|
225
|
+
'Content-Type': 'application/json',
|
|
226
|
+
Accept: 'application/json, text/event-stream',
|
|
227
|
+
},
|
|
228
|
+
includeSessionId: !isInitializeRequest,
|
|
174
229
|
});
|
|
175
230
|
|
|
176
231
|
const init = {
|
|
@@ -183,10 +238,7 @@ export class HttpMCPTransport implements MCPTransport {
|
|
|
183
238
|
|
|
184
239
|
const response = await this.fetchFn(this.url.href, init);
|
|
185
240
|
|
|
186
|
-
|
|
187
|
-
if (sessionId) {
|
|
188
|
-
this.sessionId = sessionId;
|
|
189
|
-
}
|
|
241
|
+
this.applySessionIdFromResponse(response);
|
|
190
242
|
|
|
191
243
|
if (response.status === 401 && this.authProvider && !triedAuth) {
|
|
192
244
|
this.resourceMetadataUrl = extractResourceMetadataUrl(response);
|
|
@@ -217,10 +269,16 @@ export class HttpMCPTransport implements MCPTransport {
|
|
|
217
269
|
const text = await response.text().catch(() => null);
|
|
218
270
|
let errorMessage = `MCP HTTP Transport Error: POSTing to endpoint (HTTP ${response.status}): ${text}`;
|
|
219
271
|
|
|
220
|
-
// 404 since this is a GET request which the server does not support
|
|
221
272
|
if (response.status === 404) {
|
|
222
|
-
|
|
223
|
-
|
|
273
|
+
if (sessionIdForRequest) {
|
|
274
|
+
this.expireSessionId(sessionIdForRequest);
|
|
275
|
+
|
|
276
|
+
errorMessage +=
|
|
277
|
+
'. The MCP session expired. Create a new client without `initialSessionId` to start a fresh session';
|
|
278
|
+
} else {
|
|
279
|
+
errorMessage +=
|
|
280
|
+
'. This server does not support HTTP transport. Try using `sse` transport instead';
|
|
281
|
+
}
|
|
224
282
|
}
|
|
225
283
|
|
|
226
284
|
const error = new MCPClientError({
|
|
@@ -356,8 +414,11 @@ export class HttpMCPTransport implements MCPTransport {
|
|
|
356
414
|
resumeToken?: string,
|
|
357
415
|
): Promise<void> {
|
|
358
416
|
try {
|
|
417
|
+
const sessionIdForRequest = this.sessionId;
|
|
359
418
|
const headers = await this.commonHeaders({
|
|
360
|
-
|
|
419
|
+
base: {
|
|
420
|
+
Accept: 'text/event-stream',
|
|
421
|
+
},
|
|
361
422
|
});
|
|
362
423
|
if (resumeToken) {
|
|
363
424
|
headers['last-event-id'] = resumeToken;
|
|
@@ -370,10 +431,7 @@ export class HttpMCPTransport implements MCPTransport {
|
|
|
370
431
|
redirect: this.redirectMode,
|
|
371
432
|
});
|
|
372
433
|
|
|
373
|
-
|
|
374
|
-
if (sessionId) {
|
|
375
|
-
this.sessionId = sessionId;
|
|
376
|
-
}
|
|
434
|
+
this.applySessionIdFromResponse(response);
|
|
377
435
|
|
|
378
436
|
if (response.status === 401 && this.authProvider && !triedAuth) {
|
|
379
437
|
this.resourceMetadataUrl = extractResourceMetadataUrl(response);
|
|
@@ -396,6 +454,10 @@ export class HttpMCPTransport implements MCPTransport {
|
|
|
396
454
|
}
|
|
397
455
|
|
|
398
456
|
if (!response.ok || !response.body) {
|
|
457
|
+
if (response.status === 404 && sessionIdForRequest) {
|
|
458
|
+
this.expireSessionId(sessionIdForRequest);
|
|
459
|
+
}
|
|
460
|
+
|
|
399
461
|
const error = new MCPClientError({
|
|
400
462
|
message: `MCP HTTP Transport Error: GET SSE failed: ${response.status} ${response.statusText}`,
|
|
401
463
|
statusCode: response.status,
|
|
@@ -78,6 +78,42 @@ export type MCPTransportConfig = {
|
|
|
78
78
|
*/
|
|
79
79
|
redirect?: 'follow' | 'error';
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Initial MCP session id to send with resumed Streamable HTTP requests after
|
|
83
|
+
* initialization.
|
|
84
|
+
* Only used by the HTTP transport.
|
|
85
|
+
*/
|
|
86
|
+
initialSessionId?: string;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Initial MCP protocol version to send before initialize negotiates one.
|
|
90
|
+
* Only used by the HTTP transport.
|
|
91
|
+
*/
|
|
92
|
+
initialProtocolVersion?: string;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Called when the Streamable HTTP server creates, changes, or clears the MCP
|
|
96
|
+
* session id.
|
|
97
|
+
* Only used by the HTTP transport.
|
|
98
|
+
*/
|
|
99
|
+
onSessionIdChange?: (sessionId: string | undefined) => void;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Called when a Streamable HTTP request returns 404 for an existing MCP
|
|
103
|
+
* session id. The transport clears the session id before reporting the
|
|
104
|
+
* underlying HTTP error.
|
|
105
|
+
* Only used by the HTTP transport.
|
|
106
|
+
*/
|
|
107
|
+
onSessionExpired?: (sessionId: string) => void;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether close() should send DELETE for the current MCP session id.
|
|
111
|
+
* Set to false when the application intends to reattach to the session later.
|
|
112
|
+
* Only used by the HTTP transport.
|
|
113
|
+
* @default true
|
|
114
|
+
*/
|
|
115
|
+
terminateSessionOnClose?: boolean;
|
|
116
|
+
|
|
81
117
|
/**
|
|
82
118
|
* Optional custom fetch implementation to use for HTTP requests.
|
|
83
119
|
* Useful for runtimes that need a request-local fetch.
|