@ai-sdk/mcp 2.0.31 → 2.0.33

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.
@@ -10,16 +10,20 @@ import {
10
10
  validateJSONRPCMessage,
11
11
  type JSONRPCMessage,
12
12
  } from './json-rpc-message';
13
- import type { MCPTransport } from './mcp-transport';
13
+ import type { MCPTransport, MCPTransportSendOptions } from './mcp-transport';
14
14
  import { VERSION } from '../version';
15
15
  import {
16
- extractResourceMetadataUrl,
16
+ extractWWWAuthenticateParams,
17
17
  UnauthorizedError,
18
18
  auth,
19
19
  type AuthResult,
20
20
  type OAuthClientProvider,
21
21
  } from './oauth';
22
- import { LATEST_PROTOCOL_VERSION } from './types';
22
+ import {
23
+ LATEST_LEGACY_PROTOCOL_VERSION,
24
+ LATEST_PROTOCOL_VERSION,
25
+ } from './types';
26
+ import { encodeMCPHeaderValue } from './mcp-http-headers';
23
27
 
24
28
  function isMessageEvent(event: string | undefined): boolean {
25
29
  return event === undefined || event === 'message';
@@ -33,6 +37,8 @@ function isMessageEvent(event: string | undefined): boolean {
33
37
  * for receiving messages.
34
38
  */
35
39
  export class HttpMCPTransport implements MCPTransport {
40
+ readonly supportsProtocolVersionDiscovery = true;
41
+ readonly supportsMcpToolParameterHeaders = true;
36
42
  private url: URL;
37
43
  private abortController?: AbortController;
38
44
  private headers?: Record<string, string>;
@@ -90,7 +96,8 @@ export class HttpMCPTransport implements MCPTransport {
90
96
  this.authProvider = authProvider;
91
97
  this.redirectMode = redirect;
92
98
  this.sessionId = initialSessionId;
93
- this.protocolVersion = initialProtocolVersion;
99
+ this.protocolVersion =
100
+ initialProtocolVersion ?? LATEST_LEGACY_PROTOCOL_VERSION;
94
101
  this.onSessionIdChange = onSessionIdChange;
95
102
  this.onSessionExpired = onSessionExpired;
96
103
  this.terminateSessionOnClose = terminateSessionOnClose;
@@ -99,6 +106,27 @@ export class HttpMCPTransport implements MCPTransport {
99
106
 
100
107
  setProtocolVersion(version: string): void {
101
108
  this.protocolVersion = version;
109
+
110
+ if (!this.abortController) {
111
+ return;
112
+ }
113
+
114
+ if (this.isModernProtocol()) {
115
+ this.inboundSseConnection?.close();
116
+ this.inboundSseConnection = undefined;
117
+ return;
118
+ }
119
+
120
+ if (!this.inboundSseConnection) {
121
+ this.startInboundSse();
122
+ }
123
+ }
124
+
125
+ private isModernProtocol(): boolean {
126
+ return (
127
+ (this.protocolVersion ?? LATEST_PROTOCOL_VERSION) ===
128
+ LATEST_PROTOCOL_VERSION
129
+ );
102
130
  }
103
131
 
104
132
  private async commonHeaders({
@@ -111,10 +139,11 @@ export class HttpMCPTransport implements MCPTransport {
111
139
  const headers: Record<string, string> = {
112
140
  ...this.headers,
113
141
  ...base,
114
- 'mcp-protocol-version': this.protocolVersion ?? LATEST_PROTOCOL_VERSION,
142
+ 'mcp-protocol-version':
143
+ this.protocolVersion ?? LATEST_LEGACY_PROTOCOL_VERSION,
115
144
  };
116
145
 
117
- if (includeSessionId && this.sessionId) {
146
+ if (!this.isModernProtocol() && includeSessionId && this.sessionId) {
118
147
  headers['mcp-session-id'] = this.sessionId;
119
148
  }
120
149
 
@@ -142,6 +171,10 @@ export class HttpMCPTransport implements MCPTransport {
142
171
  }
143
172
 
144
173
  private applySessionIdFromResponse(response: Response): void {
174
+ if (this.isModernProtocol()) {
175
+ return;
176
+ }
177
+
145
178
  const sessionId = response.headers.get('mcp-session-id');
146
179
  if (sessionId) {
147
180
  this.setSessionId(sessionId);
@@ -159,7 +192,10 @@ export class HttpMCPTransport implements MCPTransport {
159
192
  /**
160
193
  * Runs a single OAuth recovery flow for concurrent 401 responses.
161
194
  */
162
- private authorizeOnce(resourceMetadataUrl?: URL): Promise<AuthResult> {
195
+ private authorizeOnce(
196
+ resourceMetadataUrl?: URL,
197
+ scope?: string,
198
+ ): Promise<AuthResult> {
163
199
  if (!this.authProvider) {
164
200
  return Promise.resolve('REDIRECT');
165
201
  }
@@ -168,6 +204,7 @@ export class HttpMCPTransport implements MCPTransport {
168
204
  this.authPromise = auth(this.authProvider, {
169
205
  serverUrl: this.url,
170
206
  resourceMetadataUrl,
207
+ scope,
171
208
  fetchFn: this.fetchFn,
172
209
  }).finally(() => {
173
210
  this.authPromise = undefined;
@@ -186,7 +223,12 @@ export class HttpMCPTransport implements MCPTransport {
186
223
  }
187
224
  this.abortController = new AbortController();
188
225
 
189
- this.startInboundSse();
226
+ if (
227
+ this.protocolVersion != null &&
228
+ this.protocolVersion !== LATEST_PROTOCOL_VERSION
229
+ ) {
230
+ this.startInboundSse();
231
+ }
190
232
  }
191
233
 
192
234
  async close(options?: { signal?: AbortSignal }): Promise<void> {
@@ -195,6 +237,7 @@ export class HttpMCPTransport implements MCPTransport {
195
237
 
196
238
  try {
197
239
  if (
240
+ !this.isModernProtocol() &&
198
241
  this.sessionId &&
199
242
  this.terminateSessionOnClose &&
200
243
  this.abortController
@@ -216,7 +259,7 @@ export class HttpMCPTransport implements MCPTransport {
216
259
 
217
260
  async send(
218
261
  message: JSONRPCMessage,
219
- options?: { signal?: AbortSignal },
262
+ options?: MCPTransportSendOptions,
220
263
  ): Promise<void> {
221
264
  options?.signal?.throwIfAborted();
222
265
 
@@ -239,6 +282,12 @@ export class HttpMCPTransport implements MCPTransport {
239
282
  base: {
240
283
  'Content-Type': 'application/json',
241
284
  Accept: 'application/json, text/event-stream',
285
+ ...(this.isModernProtocol() ? options?.headers : {}),
286
+ ...(this.isModernProtocol() &&
287
+ 'method' in message &&
288
+ 'id' in message
289
+ ? this.getStandardRequestHeaders(message)
290
+ : {}),
242
291
  },
243
292
  includeSessionId: !isInitializeRequest,
244
293
  });
@@ -256,9 +305,14 @@ export class HttpMCPTransport implements MCPTransport {
256
305
  this.applySessionIdFromResponse(response);
257
306
 
258
307
  if (response.status === 401 && this.authProvider && !triedAuth) {
259
- this.resourceMetadataUrl = extractResourceMetadataUrl(response);
308
+ const { resourceMetadataUrl, scope } =
309
+ extractWWWAuthenticateParams(response);
310
+ this.resourceMetadataUrl = resourceMetadataUrl;
260
311
  try {
261
- const result = await this.authorizeOnce(this.resourceMetadataUrl);
312
+ const result = await this.authorizeOnce(
313
+ this.resourceMetadataUrl,
314
+ scope,
315
+ );
262
316
  if (result !== 'AUTHORIZED') {
263
317
  const error = new UnauthorizedError();
264
318
  throw error;
@@ -274,7 +328,7 @@ export class HttpMCPTransport implements MCPTransport {
274
328
  if (response.status === 202) {
275
329
  // If inbound SSE was not available earlier (e.g. 405 before init), try again now
276
330
  // Do not await to avoid blocking send()
277
- if (!this.inboundSseConnection) {
331
+ if (!this.isModernProtocol() && !this.inboundSseConnection) {
278
332
  this.startInboundSse();
279
333
  }
280
334
  return;
@@ -282,15 +336,30 @@ export class HttpMCPTransport implements MCPTransport {
282
336
 
283
337
  if (!response.ok) {
284
338
  const text = await response.text().catch(() => null);
339
+
340
+ if ('id' in message && text != null) {
341
+ const jsonRpcMessage = await parseJSONRPCMessage(text).catch(
342
+ () => undefined,
343
+ );
344
+ if (jsonRpcMessage != null && 'error' in jsonRpcMessage) {
345
+ this.onmessage?.(
346
+ jsonRpcMessage.id == null
347
+ ? { ...jsonRpcMessage, id: message.id }
348
+ : jsonRpcMessage,
349
+ );
350
+ return;
351
+ }
352
+ }
353
+
285
354
  let errorMessage = `MCP HTTP Transport Error: POSTing to endpoint (HTTP ${response.status}): ${text}`;
286
355
 
287
356
  if (response.status === 404) {
288
- if (sessionIdForRequest) {
357
+ if (!this.isModernProtocol() && sessionIdForRequest) {
289
358
  this.expireSessionId(sessionIdForRequest);
290
359
 
291
360
  errorMessage +=
292
361
  '. The MCP session expired. Create a new client without `initialSessionId` to start a fresh session';
293
- } else {
362
+ } else if (!this.isModernProtocol()) {
294
363
  errorMessage +=
295
364
  '. This server does not support HTTP transport. Try using `sse` transport instead';
296
365
  }
@@ -404,6 +473,27 @@ export class HttpMCPTransport implements MCPTransport {
404
473
  await attempt();
405
474
  }
406
475
 
476
+ private getStandardRequestHeaders(
477
+ message: Extract<JSONRPCMessage, { method: string; id: unknown }>,
478
+ ): Record<string, string> {
479
+ const headers: Record<string, string> = {
480
+ 'Mcp-Method': message.method,
481
+ };
482
+ const params = message.params;
483
+ const name =
484
+ message.method === 'resources/read'
485
+ ? params?.uri
486
+ : message.method === 'tools/call' || message.method === 'prompts/get'
487
+ ? params?.name
488
+ : undefined;
489
+
490
+ if (typeof name === 'string') {
491
+ headers['Mcp-Name'] = encodeMCPHeaderValue(name);
492
+ }
493
+
494
+ return headers;
495
+ }
496
+
407
497
  private getNextReconnectionDelay(attempt: number): number {
408
498
  const {
409
499
  initialReconnectionDelay,
@@ -439,6 +529,10 @@ export class HttpMCPTransport implements MCPTransport {
439
529
  triedAuth: boolean = false,
440
530
  resumeToken?: string,
441
531
  ): void {
532
+ if (this.isModernProtocol()) {
533
+ return;
534
+ }
535
+
442
536
  void this.openInboundSse(triedAuth, resumeToken).catch(error => {
443
537
  if (error instanceof Error && error.name === 'AbortError') {
444
538
  return;
@@ -452,6 +546,10 @@ export class HttpMCPTransport implements MCPTransport {
452
546
  triedAuth: boolean = false,
453
547
  resumeToken?: string,
454
548
  ): Promise<void> {
549
+ if (this.isModernProtocol()) {
550
+ return;
551
+ }
552
+
455
553
  try {
456
554
  const sessionIdForRequest = this.sessionId;
457
555
  const headers = await this.commonHeaders({
@@ -473,9 +571,14 @@ export class HttpMCPTransport implements MCPTransport {
473
571
  this.applySessionIdFromResponse(response);
474
572
 
475
573
  if (response.status === 401 && this.authProvider && !triedAuth) {
476
- this.resourceMetadataUrl = extractResourceMetadataUrl(response);
574
+ const { resourceMetadataUrl, scope } =
575
+ extractWWWAuthenticateParams(response);
576
+ this.resourceMetadataUrl = resourceMetadataUrl;
477
577
  try {
478
- const result = await this.authorizeOnce(this.resourceMetadataUrl);
578
+ const result = await this.authorizeOnce(
579
+ this.resourceMetadataUrl,
580
+ scope,
581
+ );
479
582
  if (result !== 'AUTHORIZED') {
480
583
  const error = new UnauthorizedError();
481
584
  this.onerror?.(error);
@@ -9,12 +9,12 @@ import { parseJSONRPCMessage, type JSONRPCMessage } from './json-rpc-message';
9
9
  import type { MCPTransport } from './mcp-transport';
10
10
  import { VERSION } from '../version';
11
11
  import {
12
- extractResourceMetadataUrl,
12
+ extractWWWAuthenticateParams,
13
13
  UnauthorizedError,
14
14
  auth,
15
15
  type OAuthClientProvider,
16
16
  } from './oauth';
17
- import { LATEST_PROTOCOL_VERSION } from './types';
17
+ import { LATEST_LEGACY_PROTOCOL_VERSION } from './types';
18
18
 
19
19
  function isMessageEvent(event: string | undefined): boolean {
20
20
  return event === undefined || event === 'message';
@@ -69,7 +69,8 @@ export class SseMCPTransport implements MCPTransport {
69
69
  const headers: Record<string, string> = {
70
70
  ...this.headers,
71
71
  ...base,
72
- 'mcp-protocol-version': this.protocolVersion ?? LATEST_PROTOCOL_VERSION,
72
+ 'mcp-protocol-version':
73
+ this.protocolVersion ?? LATEST_LEGACY_PROTOCOL_VERSION,
73
74
  };
74
75
 
75
76
  if (this.authProvider) {
@@ -106,11 +107,14 @@ export class SseMCPTransport implements MCPTransport {
106
107
  });
107
108
 
108
109
  if (response.status === 401 && this.authProvider && !triedAuth) {
109
- this.resourceMetadataUrl = extractResourceMetadataUrl(response);
110
+ const { resourceMetadataUrl, scope } =
111
+ extractWWWAuthenticateParams(response);
112
+ this.resourceMetadataUrl = resourceMetadataUrl;
110
113
  try {
111
114
  const result = await auth(this.authProvider, {
112
115
  serverUrl: this.url,
113
116
  resourceMetadataUrl: this.resourceMetadataUrl,
117
+ scope,
114
118
  fetchFn: this.fetchFn,
115
119
  });
116
120
  if (result !== 'AUTHORIZED') {
@@ -273,11 +277,14 @@ export class SseMCPTransport implements MCPTransport {
273
277
  const response = await this.fetchFn(endpoint.href, init);
274
278
 
275
279
  if (response.status === 401 && this.authProvider && !triedAuth) {
276
- this.resourceMetadataUrl = extractResourceMetadataUrl(response);
280
+ const { resourceMetadataUrl, scope } =
281
+ extractWWWAuthenticateParams(response);
282
+ this.resourceMetadataUrl = resourceMetadataUrl;
277
283
  try {
278
284
  const result = await auth(this.authProvider, {
279
285
  serverUrl: this.url,
280
286
  resourceMetadataUrl: this.resourceMetadataUrl,
287
+ scope,
281
288
  fetchFn: this.fetchFn,
282
289
  });
283
290
  if (result !== 'AUTHORIZED') {
@@ -14,6 +14,7 @@ export interface StdioConfig {
14
14
  }
15
15
 
16
16
  export class StdioMCPTransport implements MCPTransport {
17
+ readonly supportsProtocolVersionDiscovery = true;
17
18
  private process?: ChildProcess;
18
19
  private abortController: AbortController = new AbortController();
19
20
  private readBuffer: ReadBuffer = new ReadBuffer();
@@ -4,6 +4,7 @@ import type { JSONRPCMessage } from './json-rpc-message';
4
4
  import { SseMCPTransport } from './mcp-sse-transport';
5
5
  import { HttpMCPTransport } from './mcp-http-transport';
6
6
  import type { OAuthClientProvider } from './oauth';
7
+ import { LATEST_PROTOCOL_VERSION } from './types';
7
8
 
8
9
  /**
9
10
  * Transport interface for MCP (Model Context Protocol) communication.
@@ -15,6 +16,11 @@ export type MCPTransportSendOptions = {
15
16
  */
16
17
  signal?: AbortSignal;
17
18
 
19
+ /**
20
+ * Request-specific HTTP headers produced from MCP tool parameters.
21
+ */
22
+ headers?: Record<string, string>;
23
+
18
24
  /**
19
25
  * Associates an outgoing message with an incoming request.
20
26
  */
@@ -39,6 +45,20 @@ export type MCPTransportCloseOptions = {
39
45
  };
40
46
 
41
47
  export interface MCPTransport {
48
+ /**
49
+ * Whether this transport can probe for stateless MCP protocol versions.
50
+ *
51
+ * Custom transports default to the legacy initialization flow unless they
52
+ * explicitly opt in.
53
+ */
54
+ supportsProtocolVersionDiscovery?: boolean;
55
+
56
+ /**
57
+ * Whether this transport mirrors x-mcp-header tool parameters into request
58
+ * headers.
59
+ */
60
+ supportsMcpToolParameterHeaders?: boolean;
61
+
42
62
  /**
43
63
  * Initialize and start the transport
44
64
  */
@@ -161,7 +181,11 @@ export function createMcpTransport(config: MCPTransportConfig): MCPTransport {
161
181
  case 'sse':
162
182
  return new SseMCPTransport(config);
163
183
  case 'http':
164
- return new HttpMCPTransport(config);
184
+ return new HttpMCPTransport({
185
+ ...config,
186
+ initialProtocolVersion:
187
+ config.initialProtocolVersion ?? LATEST_PROTOCOL_VERSION,
188
+ });
165
189
  default:
166
190
  throw new MCPClientError({
167
191
  message:
@@ -2,7 +2,7 @@ import { delay } from '@ai-sdk/provider-utils';
2
2
  import type { JSONRPCMessage } from './json-rpc-message';
3
3
  import type { MCPTransport } from './mcp-transport';
4
4
  import {
5
- LATEST_PROTOCOL_VERSION,
5
+ LATEST_LEGACY_PROTOCOL_VERSION,
6
6
  type MCPTool,
7
7
  type MCPResource,
8
8
  type MCPPrompt,
@@ -163,7 +163,7 @@ export class MockMCPTransport implements MCPTransport {
163
163
  jsonrpc: '2.0',
164
164
  id: message.id,
165
165
  result: this.initializeResult || {
166
- protocolVersion: LATEST_PROTOCOL_VERSION,
166
+ protocolVersion: LATEST_LEGACY_PROTOCOL_VERSION,
167
167
  serverInfo: {
168
168
  name: 'mock-mcp-server',
169
169
  version: '1.0.0',
@@ -39,6 +39,7 @@ export const OAuthTokensSchema = z
39
39
  expires_in: z.number().optional(),
40
40
  scope: z.string().optional(),
41
41
  refresh_token: z.string().optional(),
42
+ issuer: SafeUrlSchema.optional(),
42
43
  authorization_server: SafeUrlSchema.optional(),
43
44
  token_endpoint: SafeUrlSchema.optional(),
44
45
  })
@@ -66,6 +67,8 @@ export const OAuthMetadataSchema = z.looseObject({
66
67
  authorization_endpoint: SafeUrlSchema,
67
68
  token_endpoint: SafeUrlSchema,
68
69
  registration_endpoint: SafeUrlSchema.optional(),
70
+ authorization_response_iss_parameter_supported: z.boolean().optional(),
71
+ client_id_metadata_document_supported: z.boolean().optional(),
69
72
  scopes_supported: z.array(z.string()).optional(),
70
73
  response_types_supported: z.array(z.string()),
71
74
  grant_types_supported: z.array(z.string()).optional(),
@@ -87,6 +90,8 @@ export const OpenIdProviderMetadataSchema = z.looseObject({
87
90
  userinfo_endpoint: SafeUrlSchema.optional(),
88
91
  jwks_uri: SafeUrlSchema,
89
92
  registration_endpoint: SafeUrlSchema.optional(),
93
+ authorization_response_iss_parameter_supported: z.boolean().optional(),
94
+ client_id_metadata_document_supported: z.boolean().optional(),
90
95
  scopes_supported: z.array(z.string()).optional(),
91
96
  response_types_supported: z.array(z.string()),
92
97
  grant_types_supported: z.array(z.string()).optional(),
@@ -114,6 +119,7 @@ export const OAuthClientInformationSchema = z
114
119
  client_secret: z.string().optional(),
115
120
  client_id_issued_at: z.number().optional(),
116
121
  client_secret_expires_at: z.number().optional(),
122
+ issuer: SafeUrlSchema.optional(),
117
123
  authorization_server: SafeUrlSchema.optional(),
118
124
  token_endpoint: SafeUrlSchema.optional(),
119
125
  })
@@ -122,6 +128,9 @@ export const OAuthClientInformationSchema = z
122
128
  export const OAuthClientMetadataSchema = z
123
129
  .object({
124
130
  redirect_uris: z.array(SafeUrlSchema),
131
+ application_type: z
132
+ .union([z.literal('native'), z.literal('web')])
133
+ .optional(),
125
134
  token_endpoint_auth_method: z.string().optional(),
126
135
  grant_types: z.array(z.string()).optional(),
127
136
  response_types: z.array(z.string()).optional(),