@ai-sdk/mcp 2.0.0 → 2.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/mcp",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "pkce-challenge": "^5.0.1",
35
- "@ai-sdk/provider": "4.0.0",
36
- "@ai-sdk/provider-utils": "5.0.0"
35
+ "@ai-sdk/provider-utils": "5.0.0",
36
+ "@ai-sdk/provider": "4.0.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "22.19.19",
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export type {
27
27
  Configuration,
28
28
  ElicitationRequest,
29
29
  ElicitResult,
30
+ InitializeResult,
30
31
  ListToolsResult,
31
32
  McpProviderMetadata,
32
33
  ClientCapabilities as MCPClientCapabilities,
@@ -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
- * - Session management (when passing a sessionId to an instance of the Streamable HTTP transport)
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
- if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
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: Record<string, string>,
88
- ): Promise<Record<string, string>> {
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
- 'Content-Type': 'application/json',
173
- Accept: 'application/json, text/event-stream',
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
- const sessionId = response.headers.get('mcp-session-id');
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
- errorMessage +=
223
- '. This server does not support HTTP transport. Try using `sse` transport instead';
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
- Accept: 'text/event-stream',
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
- const sessionId = response.headers.get('mcp-session-id');
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.
@@ -44,63 +44,57 @@ export const OAuthTokensSchema = z
44
44
  })
45
45
  .strip();
46
46
 
47
- export const OAuthProtectedResourceMetadataSchema = z
48
- .object({
49
- resource: z.string().url(),
50
- authorization_servers: z.array(SafeUrlSchema).optional(),
51
- jwks_uri: z.string().url().optional(),
52
- scopes_supported: z.array(z.string()).optional(),
53
- bearer_methods_supported: z.array(z.string()).optional(),
54
- resource_signing_alg_values_supported: z.array(z.string()).optional(),
55
- resource_name: z.string().optional(),
56
- resource_documentation: z.string().optional(),
57
- resource_policy_uri: z.string().url().optional(),
58
- resource_tos_uri: z.string().url().optional(),
59
- tls_client_certificate_bound_access_tokens: z.boolean().optional(),
60
- authorization_details_types_supported: z.array(z.string()).optional(),
61
- dpop_signing_alg_values_supported: z.array(z.string()).optional(),
62
- dpop_bound_access_tokens_required: z.boolean().optional(),
63
- })
64
- .passthrough();
47
+ export const OAuthProtectedResourceMetadataSchema = z.looseObject({
48
+ resource: z.string().url(),
49
+ authorization_servers: z.array(SafeUrlSchema).optional(),
50
+ jwks_uri: z.string().url().optional(),
51
+ scopes_supported: z.array(z.string()).optional(),
52
+ bearer_methods_supported: z.array(z.string()).optional(),
53
+ resource_signing_alg_values_supported: z.array(z.string()).optional(),
54
+ resource_name: z.string().optional(),
55
+ resource_documentation: z.string().optional(),
56
+ resource_policy_uri: z.string().url().optional(),
57
+ resource_tos_uri: z.string().url().optional(),
58
+ tls_client_certificate_bound_access_tokens: z.boolean().optional(),
59
+ authorization_details_types_supported: z.array(z.string()).optional(),
60
+ dpop_signing_alg_values_supported: z.array(z.string()).optional(),
61
+ dpop_bound_access_tokens_required: z.boolean().optional(),
62
+ });
65
63
 
66
- export const OAuthMetadataSchema = z
67
- .object({
68
- issuer: z.string(),
69
- authorization_endpoint: SafeUrlSchema,
70
- token_endpoint: SafeUrlSchema,
71
- registration_endpoint: SafeUrlSchema.optional(),
72
- scopes_supported: z.array(z.string()).optional(),
73
- response_types_supported: z.array(z.string()),
74
- grant_types_supported: z.array(z.string()).optional(),
75
- code_challenge_methods_supported: z.array(z.string()),
76
- token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
77
- token_endpoint_auth_signing_alg_values_supported: z
78
- .array(z.string())
79
- .optional(),
80
- })
81
- .passthrough();
64
+ export const OAuthMetadataSchema = z.looseObject({
65
+ issuer: z.string(),
66
+ authorization_endpoint: SafeUrlSchema,
67
+ token_endpoint: SafeUrlSchema,
68
+ registration_endpoint: SafeUrlSchema.optional(),
69
+ scopes_supported: z.array(z.string()).optional(),
70
+ response_types_supported: z.array(z.string()),
71
+ grant_types_supported: z.array(z.string()).optional(),
72
+ code_challenge_methods_supported: z.array(z.string()),
73
+ token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
74
+ token_endpoint_auth_signing_alg_values_supported: z
75
+ .array(z.string())
76
+ .optional(),
77
+ });
82
78
 
83
79
  /**
84
80
  * OpenID Connect Discovery 1.0 Provider Metadata
85
81
  * see: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
86
82
  */
87
- export const OpenIdProviderMetadataSchema = z
88
- .object({
89
- issuer: z.string(),
90
- authorization_endpoint: SafeUrlSchema,
91
- token_endpoint: SafeUrlSchema,
92
- userinfo_endpoint: SafeUrlSchema.optional(),
93
- jwks_uri: SafeUrlSchema,
94
- registration_endpoint: SafeUrlSchema.optional(),
95
- scopes_supported: z.array(z.string()).optional(),
96
- response_types_supported: z.array(z.string()),
97
- grant_types_supported: z.array(z.string()).optional(),
98
- subject_types_supported: z.array(z.string()),
99
- id_token_signing_alg_values_supported: z.array(z.string()),
100
- claims_supported: z.array(z.string()).optional(),
101
- token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
102
- })
103
- .passthrough();
83
+ export const OpenIdProviderMetadataSchema = z.looseObject({
84
+ issuer: z.string(),
85
+ authorization_endpoint: SafeUrlSchema,
86
+ token_endpoint: SafeUrlSchema,
87
+ userinfo_endpoint: SafeUrlSchema.optional(),
88
+ jwks_uri: SafeUrlSchema,
89
+ registration_endpoint: SafeUrlSchema.optional(),
90
+ scopes_supported: z.array(z.string()).optional(),
91
+ response_types_supported: z.array(z.string()),
92
+ grant_types_supported: z.array(z.string()).optional(),
93
+ subject_types_supported: z.array(z.string()),
94
+ id_token_signing_alg_values_supported: z.array(z.string()),
95
+ claims_supported: z.array(z.string()).optional(),
96
+ token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
97
+ });
104
98
 
105
99
  /**
106
100
  * OpenID Connect Discovery metadata that may include OAuth 2.0 fields