@ai-sdk/mcp 2.0.18 → 2.0.20

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.
@@ -36,6 +36,30 @@ type JSONRPCMessage = z.infer<typeof JSONRPCMessageSchema>;
36
36
  * Transport interface for MCP (Model Context Protocol) communication.
37
37
  * Maps to the `Transport` interface in the MCP spec.
38
38
  */
39
+ type MCPTransportSendOptions = {
40
+ /**
41
+ * Cancels the transport operation for this message.
42
+ */
43
+ signal?: AbortSignal;
44
+ /**
45
+ * Associates an outgoing message with an incoming request.
46
+ */
47
+ relatedRequestId?: string | number;
48
+ /**
49
+ * Resumes a previously interrupted request.
50
+ */
51
+ resumptionToken?: string;
52
+ /**
53
+ * Receives updated resumption tokens from transports that support them.
54
+ */
55
+ onresumptiontoken?: (token: string) => void;
56
+ };
57
+ type MCPTransportCloseOptions = {
58
+ /**
59
+ * Cancels transport cleanup.
60
+ */
61
+ signal?: AbortSignal;
62
+ };
39
63
  interface MCPTransport {
40
64
  /**
41
65
  * Initialize and start the transport
@@ -44,12 +68,14 @@ interface MCPTransport {
44
68
  /**
45
69
  * Send a JSON-RPC message through the transport
46
70
  * @param message The JSON-RPC message to send
71
+ * @param options Optional request-scoped cancellation options
47
72
  */
48
- send(message: JSONRPCMessage): Promise<void>;
73
+ send(message: JSONRPCMessage, options?: MCPTransportSendOptions): Promise<void>;
49
74
  /**
50
75
  * Clean up and close the transport
76
+ * @param options Optional cancellation options for transport cleanup
51
77
  */
52
- close(): Promise<void>;
78
+ close(options?: MCPTransportCloseOptions): Promise<void>;
53
79
  /**
54
80
  * Event handler for transport closure
55
81
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/mcp",
3
- "version": "2.0.18",
3
+ "version": "2.0.20",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "pkce-challenge": "^5.0.1",
35
35
  "@ai-sdk/provider": "4.0.4",
36
- "@ai-sdk/provider-utils": "5.0.14"
36
+ "@ai-sdk/provider-utils": "5.0.16"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "22.19.19",
@@ -41,7 +41,7 @@
41
41
  "typescript": "5.8.3",
42
42
  "vitest": "^4.1.6",
43
43
  "zod": "3.25.76",
44
- "@ai-sdk/test-server": "2.0.0",
44
+ "@ai-sdk/test-server": "2.0.1",
45
45
  "@vercel/ai-tsconfig": "0.0.0"
46
46
  },
47
47
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -49,7 +49,11 @@ export type {
49
49
  OAuthClientMetadata,
50
50
  OAuthTokens,
51
51
  } from './tool/oauth-types';
52
- export type { MCPTransport } from './tool/mcp-transport';
52
+ export type {
53
+ MCPTransport,
54
+ MCPTransportCloseOptions,
55
+ MCPTransportSendOptions,
56
+ } from './tool/mcp-transport';
53
57
 
54
58
  /**
55
59
  * @deprecated Use `createMCPClient` instead. Will be removed in a future version.
@@ -16,6 +16,7 @@ import type { z } from 'zod/v4';
16
16
  import { MCPClientError } from '../error/mcp-client-error';
17
17
  import type {
18
18
  JSONRPCError,
19
+ JSONRPCMessage,
19
20
  JSONRPCNotification,
20
21
  JSONRPCRequest,
21
22
  JSONRPCResponse,
@@ -144,6 +145,56 @@ function prepareMaxRetries(maxRetries: number | undefined): number {
144
145
  return maxRetries;
145
146
  }
146
147
 
148
+ function getEffectiveTimeout({
149
+ timeout,
150
+ maxTotalTimeout,
151
+ }: RequestOptions): number | undefined {
152
+ if (timeout == null) {
153
+ return maxTotalTimeout;
154
+ }
155
+
156
+ if (maxTotalTimeout == null) {
157
+ return timeout;
158
+ }
159
+
160
+ return Math.min(timeout, maxTotalTimeout);
161
+ }
162
+
163
+ function waitForAbort<T>(
164
+ promise: Promise<T>,
165
+ signal: AbortSignal | undefined,
166
+ ): Promise<T> {
167
+ if (signal == null) {
168
+ return promise;
169
+ }
170
+
171
+ return new Promise((resolve, reject) => {
172
+ const cleanup = () => {
173
+ signal.removeEventListener('abort', onAbort);
174
+ };
175
+ const onAbort = () => {
176
+ cleanup();
177
+ reject(signal.reason);
178
+ };
179
+
180
+ if (signal.aborted) {
181
+ onAbort();
182
+ return;
183
+ }
184
+
185
+ signal.addEventListener('abort', onAbort, { once: true });
186
+ promise
187
+ .then(value => {
188
+ cleanup();
189
+ resolve(value);
190
+ })
191
+ .catch(error => {
192
+ cleanup();
193
+ reject(error);
194
+ });
195
+ });
196
+ }
197
+
147
198
  function mcpToModelOutput({
148
199
  output,
149
200
  }: {
@@ -179,6 +230,10 @@ function mcpToModelOutput({
179
230
  export interface MCPClientConfig {
180
231
  /** Transport configuration for connecting to the MCP server */
181
232
  transport: MCPTransportConfig | MCPTransport;
233
+ /**
234
+ * Options that bound or cancel transport startup and the initialize request.
235
+ */
236
+ initializationOptions?: RequestOptions;
182
237
  /** Optional callback for uncaught errors */
183
238
  onUncaughtError?: (error: unknown) => void;
184
239
  /**
@@ -338,6 +393,7 @@ class DefaultMCPClient implements MCPClient {
338
393
  private clientInfo: ClientConfiguration;
339
394
  private clientCapabilities: ClientCapabilities;
340
395
  private initialInitializeResult?: InitializeResult;
396
+ private initializationOptions?: RequestOptions;
341
397
  private requestMessageId = 0;
342
398
  private responseHandlers: Map<
343
399
  number,
@@ -365,11 +421,13 @@ class DefaultMCPClient implements MCPClient {
365
421
  maxRetries,
366
422
  capabilities,
367
423
  initialInitializeResult,
424
+ initializationOptions,
368
425
  }: MCPClientConfig) {
369
426
  this.onUncaughtError = onUncaughtError;
370
427
  this.maxRetries = prepareMaxRetries(maxRetries);
371
428
  this.clientCapabilities = capabilities ?? {};
372
429
  this.initialInitializeResult = initialInitializeResult;
430
+ this.initializationOptions = initializationOptions;
373
431
 
374
432
  if (isCustomMcpTransport(transportConfig)) {
375
433
  this.transport = transportConfig;
@@ -415,9 +473,34 @@ class DefaultMCPClient implements MCPClient {
415
473
  }
416
474
 
417
475
  async init(): Promise<this> {
476
+ const externalSignal = this.initializationOptions?.signal;
477
+ const timeout = this.initializationOptions
478
+ ? getEffectiveTimeout(this.initializationOptions)
479
+ : undefined;
480
+ const timeoutController =
481
+ timeout == null ? undefined : new AbortController();
482
+ const signal =
483
+ externalSignal == null
484
+ ? timeoutController?.signal
485
+ : timeoutController == null
486
+ ? externalSignal
487
+ : AbortSignal.any([externalSignal, timeoutController.signal]);
488
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
489
+ let timeoutError: MCPClientError | undefined;
490
+
491
+ if (timeout != null) {
492
+ timeoutId = setTimeout(() => {
493
+ timeoutError = new MCPClientError({
494
+ message: `MCP client initialization timed out after ${timeout}ms`,
495
+ });
496
+ timeoutController?.abort(timeoutError);
497
+ }, timeout);
498
+ }
499
+
418
500
  try {
419
- await this.transport.start();
420
501
  this.isClosed = false;
502
+ signal?.throwIfAborted();
503
+ await waitForAbort(this.transport.start(), signal);
421
504
 
422
505
  if (this.initialInitializeResult) {
423
506
  const result = InitializeResultSchema.parse(
@@ -437,6 +520,7 @@ class DefaultMCPClient implements MCPClient {
437
520
  },
438
521
  },
439
522
  resultSchema: InitializeResultSchema,
523
+ options: { signal },
440
524
  });
441
525
 
442
526
  if (result === undefined) {
@@ -448,14 +532,36 @@ class DefaultMCPClient implements MCPClient {
448
532
  this.applyInitializeResult(result);
449
533
 
450
534
  // Complete initialization handshake:
451
- await this.notification({
452
- method: 'notifications/initialized',
453
- });
535
+ await this.notification(
536
+ {
537
+ method: 'notifications/initialized',
538
+ },
539
+ { signal },
540
+ );
454
541
 
455
542
  return this;
456
543
  } catch (error) {
457
- await this.close();
544
+ try {
545
+ await waitForAbort(this.transport.close({ signal }), signal);
546
+ } catch {}
547
+ this.onClose();
548
+
549
+ if (timeoutError != null) {
550
+ throw timeoutError;
551
+ }
552
+
553
+ if (externalSignal?.aborted) {
554
+ throw new MCPClientError({
555
+ message: 'MCP client initialization was aborted',
556
+ cause: externalSignal.reason,
557
+ });
558
+ }
559
+
458
560
  throw error;
561
+ } finally {
562
+ if (timeoutId != null) {
563
+ clearTimeout(timeoutId);
564
+ }
459
565
  }
460
566
  }
461
567
 
@@ -483,6 +589,16 @@ class DefaultMCPClient implements MCPClient {
483
589
  this.onClose();
484
590
  }
485
591
 
592
+ private send(
593
+ message: JSONRPCMessage,
594
+ signal: AbortSignal | undefined,
595
+ ): Promise<void> {
596
+ return this.transport.send(
597
+ message,
598
+ signal == null ? undefined : { signal },
599
+ );
600
+ }
601
+
486
602
  private assertCapability(method: string): void {
487
603
  switch (method) {
488
604
  case 'initialize':
@@ -548,6 +664,17 @@ class DefaultMCPClient implements MCPClient {
548
664
 
549
665
  const signal = options?.signal;
550
666
  signal?.throwIfAborted();
667
+ const timeout =
668
+ options == null ? undefined : getEffectiveTimeout(options);
669
+ const timeoutController =
670
+ timeout == null ? undefined : new AbortController();
671
+ const transportSignal =
672
+ signal == null
673
+ ? timeoutController?.signal
674
+ : timeoutController == null
675
+ ? signal
676
+ : AbortSignal.any([signal, timeoutController.signal]);
677
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
551
678
 
552
679
  const messageId = this.requestMessageId++;
553
680
  const jsonrpcRequest: JSONRPCRequest = {
@@ -568,6 +695,9 @@ class DefaultMCPClient implements MCPClient {
568
695
  const cleanup = () => {
569
696
  this.responseHandlers.delete(messageId);
570
697
  signal?.removeEventListener('abort', onAbort);
698
+ if (timeoutId != null) {
699
+ clearTimeout(timeoutId);
700
+ }
571
701
  };
572
702
 
573
703
  const rejectAndCleanup = (error: unknown) => {
@@ -580,6 +710,14 @@ class DefaultMCPClient implements MCPClient {
580
710
  rejectWithAbortError();
581
711
  };
582
712
 
713
+ const onTimeout = () => {
714
+ const error = new MCPClientError({
715
+ message: `Request timed out after ${timeout}ms`,
716
+ });
717
+ timeoutController?.abort(error);
718
+ rejectAndCleanup(error);
719
+ };
720
+
583
721
  this.responseHandlers.set(messageId, response => {
584
722
  if (signal?.aborted) {
585
723
  cleanup();
@@ -605,7 +743,16 @@ class DefaultMCPClient implements MCPClient {
605
743
 
606
744
  signal?.addEventListener('abort', onAbort, { once: true });
607
745
 
608
- this.transport.send(jsonrpcRequest).catch(error => {
746
+ if (timeout != null) {
747
+ timeoutId = setTimeout(onTimeout, timeout);
748
+ }
749
+
750
+ const sendPromise =
751
+ transportSignal == null
752
+ ? this.transport.send(jsonrpcRequest)
753
+ : this.send(jsonrpcRequest, transportSignal);
754
+
755
+ sendPromise.catch(error => {
609
756
  rejectAndCleanup(error);
610
757
  });
611
758
  });
@@ -778,12 +925,18 @@ class DefaultMCPClient implements MCPClient {
778
925
  });
779
926
  }
780
927
 
781
- private async notification(notification: Notification): Promise<void> {
928
+ private async notification(
929
+ notification: Notification,
930
+ options?: { signal?: AbortSignal },
931
+ ): Promise<void> {
782
932
  const jsonrpcNotification: JSONRPCNotification = {
783
933
  ...notification,
784
934
  jsonrpc: '2.0',
785
935
  };
786
- await this.transport.send(jsonrpcNotification);
936
+ await waitForAbort(
937
+ this.send(jsonrpcNotification, options?.signal),
938
+ options?.signal,
939
+ );
787
940
  }
788
941
 
789
942
  /**
@@ -189,30 +189,45 @@ export class HttpMCPTransport implements MCPTransport {
189
189
  this.startInboundSse();
190
190
  }
191
191
 
192
- async close(): Promise<void> {
192
+ async close(options?: { signal?: AbortSignal }): Promise<void> {
193
193
  this.inboundSseConnection?.close();
194
+ this.abortController?.abort();
195
+
194
196
  try {
195
197
  if (
196
198
  this.sessionId &&
197
199
  this.terminateSessionOnClose &&
198
- this.abortController &&
199
- !this.abortController.signal.aborted
200
+ this.abortController
200
201
  ) {
202
+ options?.signal?.throwIfAborted();
201
203
  const headers = await this.commonHeaders({ base: {} });
204
+ options?.signal?.throwIfAborted();
202
205
  await this.fetchFn(this.url.href, {
203
206
  method: 'DELETE',
204
207
  headers,
205
- signal: this.abortController.signal,
208
+ signal: options?.signal,
206
209
  redirect: this.redirectMode,
207
210
  }).catch(() => undefined);
208
211
  }
209
212
  } catch {}
210
213
 
211
- this.abortController?.abort();
212
214
  this.onclose?.();
213
215
  }
214
216
 
215
- async send(message: JSONRPCMessage): Promise<void> {
217
+ async send(
218
+ message: JSONRPCMessage,
219
+ options?: { signal?: AbortSignal },
220
+ ): Promise<void> {
221
+ options?.signal?.throwIfAborted();
222
+
223
+ const transportSignal = this.abortController?.signal;
224
+ const requestSignal =
225
+ options?.signal == null
226
+ ? transportSignal
227
+ : transportSignal == null
228
+ ? options.signal
229
+ : AbortSignal.any([transportSignal, options.signal]);
230
+
216
231
  const attempt = async (triedAuth: boolean = false): Promise<void> => {
217
232
  try {
218
233
  const isInitializeRequest =
@@ -232,7 +247,7 @@ export class HttpMCPTransport implements MCPTransport {
232
247
  method: 'POST',
233
248
  headers,
234
249
  body: JSON.stringify(message),
235
- signal: this.abortController?.signal,
250
+ signal: requestSignal,
236
251
  redirect: this.redirectMode,
237
252
  } satisfies RequestInit;
238
253
 
@@ -348,7 +363,10 @@ export class HttpMCPTransport implements MCPTransport {
348
363
  }
349
364
  }
350
365
  } catch (error) {
351
- if (error instanceof Error && error.name === 'AbortError') {
366
+ if (
367
+ options?.signal?.aborted ||
368
+ (error instanceof Error && error.name === 'AbortError')
369
+ ) {
352
370
  return;
353
371
  }
354
372
  this.onerror?.(error);
@@ -356,7 +374,10 @@ export class HttpMCPTransport implements MCPTransport {
356
374
  };
357
375
 
358
376
  void processEvents().catch(error => {
359
- if (error instanceof Error && error.name === 'AbortError') {
377
+ if (
378
+ options?.signal?.aborted ||
379
+ (error instanceof Error && error.name === 'AbortError')
380
+ ) {
360
381
  return;
361
382
  }
362
383
  this.onerror?.(error);
@@ -372,6 +393,9 @@ export class HttpMCPTransport implements MCPTransport {
372
393
  this.onerror?.(error);
373
394
  throw error;
374
395
  } catch (error) {
396
+ if (options?.signal?.aborted) {
397
+ throw error;
398
+ }
375
399
  this.onerror?.(error);
376
400
  throw error;
377
401
  }
@@ -236,7 +236,12 @@ export class SseMCPTransport implements MCPTransport {
236
236
  this.onclose?.();
237
237
  }
238
238
 
239
- async send(message: JSONRPCMessage): Promise<void> {
239
+ async send(
240
+ message: JSONRPCMessage,
241
+ options?: { signal?: AbortSignal },
242
+ ): Promise<void> {
243
+ options?.signal?.throwIfAborted();
244
+
240
245
  if (!this.endpoint || !this.connected) {
241
246
  throw new MCPClientError({
242
247
  message: 'MCP SSE Transport Error: Not connected',
@@ -244,6 +249,13 @@ export class SseMCPTransport implements MCPTransport {
244
249
  }
245
250
 
246
251
  const endpoint = this.endpoint as URL;
252
+ const transportSignal = this.abortController?.signal;
253
+ const requestSignal =
254
+ options?.signal == null
255
+ ? transportSignal
256
+ : transportSignal == null
257
+ ? options.signal
258
+ : AbortSignal.any([transportSignal, options.signal]);
247
259
 
248
260
  const attempt = async (triedAuth: boolean = false): Promise<void> => {
249
261
  try {
@@ -254,7 +266,7 @@ export class SseMCPTransport implements MCPTransport {
254
266
  method: 'POST',
255
267
  headers,
256
268
  body: JSON.stringify(message),
257
- signal: this.abortController?.signal,
269
+ signal: requestSignal,
258
270
  redirect: this.redirectMode,
259
271
  };
260
272
 
@@ -289,6 +301,9 @@ export class SseMCPTransport implements MCPTransport {
289
301
  return;
290
302
  }
291
303
  } catch (error) {
304
+ if (options?.signal?.aborted) {
305
+ throw error;
306
+ }
292
307
  this.onerror?.(error);
293
308
  return;
294
309
  }
@@ -9,6 +9,35 @@ import type { OAuthClientProvider } from './oauth';
9
9
  * Transport interface for MCP (Model Context Protocol) communication.
10
10
  * Maps to the `Transport` interface in the MCP spec.
11
11
  */
12
+ export type MCPTransportSendOptions = {
13
+ /**
14
+ * Cancels the transport operation for this message.
15
+ */
16
+ signal?: AbortSignal;
17
+
18
+ /**
19
+ * Associates an outgoing message with an incoming request.
20
+ */
21
+ relatedRequestId?: string | number;
22
+
23
+ /**
24
+ * Resumes a previously interrupted request.
25
+ */
26
+ resumptionToken?: string;
27
+
28
+ /**
29
+ * Receives updated resumption tokens from transports that support them.
30
+ */
31
+ onresumptiontoken?: (token: string) => void;
32
+ };
33
+
34
+ export type MCPTransportCloseOptions = {
35
+ /**
36
+ * Cancels transport cleanup.
37
+ */
38
+ signal?: AbortSignal;
39
+ };
40
+
12
41
  export interface MCPTransport {
13
42
  /**
14
43
  * Initialize and start the transport
@@ -18,13 +47,18 @@ export interface MCPTransport {
18
47
  /**
19
48
  * Send a JSON-RPC message through the transport
20
49
  * @param message The JSON-RPC message to send
50
+ * @param options Optional request-scoped cancellation options
21
51
  */
22
- send(message: JSONRPCMessage): Promise<void>;
52
+ send(
53
+ message: JSONRPCMessage,
54
+ options?: MCPTransportSendOptions,
55
+ ): Promise<void>;
23
56
 
24
57
  /**
25
58
  * Clean up and close the transport
59
+ * @param options Optional cancellation options for transport cleanup
26
60
  */
27
- close(): Promise<void>;
61
+ close(options?: MCPTransportCloseOptions): Promise<void>;
28
62
 
29
63
  /**
30
64
  * Event handler for transport closure