@ai-sdk/mcp 1.0.64 → 1.0.66

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
  */
@@ -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": "1.0.64",
3
+ "version": "1.0.66",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "pkce-challenge": "^5.0.0",
36
36
  "@ai-sdk/provider": "3.0.14",
37
- "@ai-sdk/provider-utils": "4.0.40"
37
+ "@ai-sdk/provider-utils": "4.0.41"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "20.17.24",
package/src/index.ts CHANGED
@@ -34,7 +34,11 @@ export type {
34
34
  OAuthClientMetadata,
35
35
  OAuthTokens,
36
36
  } from './tool/oauth-types';
37
- export type { MCPTransport } from './tool/mcp-transport';
37
+ export type {
38
+ MCPTransport,
39
+ MCPTransportCloseOptions,
40
+ MCPTransportSendOptions,
41
+ } from './tool/mcp-transport';
38
42
 
39
43
  /**
40
44
  * @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,
@@ -142,6 +143,56 @@ function prepareMaxRetries(maxRetries: number | undefined): number {
142
143
  return maxRetries;
143
144
  }
144
145
 
146
+ function getEffectiveTimeout({
147
+ timeout,
148
+ maxTotalTimeout,
149
+ }: RequestOptions): number | undefined {
150
+ if (timeout == null) {
151
+ return maxTotalTimeout;
152
+ }
153
+
154
+ if (maxTotalTimeout == null) {
155
+ return timeout;
156
+ }
157
+
158
+ return Math.min(timeout, maxTotalTimeout);
159
+ }
160
+
161
+ function waitForAbort<T>(
162
+ promise: Promise<T>,
163
+ signal: AbortSignal | undefined,
164
+ ): Promise<T> {
165
+ if (signal == null) {
166
+ return promise;
167
+ }
168
+
169
+ return new Promise((resolve, reject) => {
170
+ const cleanup = () => {
171
+ signal.removeEventListener('abort', onAbort);
172
+ };
173
+ const onAbort = () => {
174
+ cleanup();
175
+ reject(signal.reason);
176
+ };
177
+
178
+ if (signal.aborted) {
179
+ onAbort();
180
+ return;
181
+ }
182
+
183
+ signal.addEventListener('abort', onAbort, { once: true });
184
+ promise
185
+ .then(value => {
186
+ cleanup();
187
+ resolve(value);
188
+ })
189
+ .catch(error => {
190
+ cleanup();
191
+ reject(error);
192
+ });
193
+ });
194
+ }
195
+
145
196
  function mcpToModelOutput({
146
197
  output,
147
198
  }: {
@@ -177,6 +228,10 @@ function mcpToModelOutput({
177
228
  export interface MCPClientConfig {
178
229
  /** Transport configuration for connecting to the MCP server */
179
230
  transport: MCPTransportConfig | MCPTransport;
231
+ /**
232
+ * Options that bound or cancel transport startup and the initialize request.
233
+ */
234
+ initializationOptions?: RequestOptions;
180
235
  /** Optional callback for uncaught errors */
181
236
  onUncaughtError?: (error: unknown) => void;
182
237
  /**
@@ -314,6 +369,7 @@ class DefaultMCPClient implements MCPClient {
314
369
  private maxRetries: number;
315
370
  private clientInfo: ClientConfiguration;
316
371
  private clientCapabilities: ClientCapabilities;
372
+ private initializationOptions?: RequestOptions;
317
373
  private requestMessageId = 0;
318
374
  private responseHandlers: Map<
319
375
  number,
@@ -335,10 +391,12 @@ class DefaultMCPClient implements MCPClient {
335
391
  onUncaughtError,
336
392
  maxRetries,
337
393
  capabilities,
394
+ initializationOptions,
338
395
  }: MCPClientConfig) {
339
396
  this.onUncaughtError = onUncaughtError;
340
397
  this.maxRetries = prepareMaxRetries(maxRetries);
341
398
  this.clientCapabilities = capabilities ?? {};
399
+ this.initializationOptions = initializationOptions;
342
400
 
343
401
  if (isCustomMcpTransport(transportConfig)) {
344
402
  this.transport = transportConfig;
@@ -380,9 +438,34 @@ class DefaultMCPClient implements MCPClient {
380
438
  }
381
439
 
382
440
  async init(): Promise<this> {
441
+ const externalSignal = this.initializationOptions?.signal;
442
+ const timeout = this.initializationOptions
443
+ ? getEffectiveTimeout(this.initializationOptions)
444
+ : undefined;
445
+ const timeoutController =
446
+ timeout == null ? undefined : new AbortController();
447
+ const signal =
448
+ externalSignal == null
449
+ ? timeoutController?.signal
450
+ : timeoutController == null
451
+ ? externalSignal
452
+ : AbortSignal.any([externalSignal, timeoutController.signal]);
453
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
454
+ let timeoutError: MCPClientError | undefined;
455
+
456
+ if (timeout != null) {
457
+ timeoutId = setTimeout(() => {
458
+ timeoutError = new MCPClientError({
459
+ message: `MCP client initialization timed out after ${timeout}ms`,
460
+ });
461
+ timeoutController?.abort(timeoutError);
462
+ }, timeout);
463
+ }
464
+
383
465
  try {
384
- await this.transport.start();
385
466
  this.isClosed = false;
467
+ signal?.throwIfAborted();
468
+ await waitForAbort(this.transport.start(), signal);
386
469
 
387
470
  const result = await this.request({
388
471
  request: {
@@ -394,6 +477,7 @@ class DefaultMCPClient implements MCPClient {
394
477
  },
395
478
  },
396
479
  resultSchema: InitializeResultSchema,
480
+ options: { signal },
397
481
  });
398
482
 
399
483
  if (result === undefined) {
@@ -418,14 +502,36 @@ class DefaultMCPClient implements MCPClient {
418
502
  this._serverInstructions = result.instructions;
419
503
 
420
504
  // Complete initialization handshake:
421
- await this.notification({
422
- method: 'notifications/initialized',
423
- });
505
+ await this.notification(
506
+ {
507
+ method: 'notifications/initialized',
508
+ },
509
+ { signal },
510
+ );
424
511
 
425
512
  return this;
426
513
  } catch (error) {
427
- await this.close();
514
+ try {
515
+ await waitForAbort(this.transport.close({ signal }), signal);
516
+ } catch {}
517
+ this.onClose();
518
+
519
+ if (timeoutError != null) {
520
+ throw timeoutError;
521
+ }
522
+
523
+ if (externalSignal?.aborted) {
524
+ throw new MCPClientError({
525
+ message: 'MCP client initialization was aborted',
526
+ cause: externalSignal.reason,
527
+ });
528
+ }
529
+
428
530
  throw error;
531
+ } finally {
532
+ if (timeoutId != null) {
533
+ clearTimeout(timeoutId);
534
+ }
429
535
  }
430
536
  }
431
537
 
@@ -435,6 +541,16 @@ class DefaultMCPClient implements MCPClient {
435
541
  this.onClose();
436
542
  }
437
543
 
544
+ private send(
545
+ message: JSONRPCMessage,
546
+ signal: AbortSignal | undefined,
547
+ ): Promise<void> {
548
+ return this.transport.send(
549
+ message,
550
+ signal == null ? undefined : { signal },
551
+ );
552
+ }
553
+
438
554
  private assertCapability(method: string): void {
439
555
  switch (method) {
440
556
  case 'initialize':
@@ -500,6 +616,17 @@ class DefaultMCPClient implements MCPClient {
500
616
 
501
617
  const signal = options?.signal;
502
618
  signal?.throwIfAborted();
619
+ const timeout =
620
+ options == null ? undefined : getEffectiveTimeout(options);
621
+ const timeoutController =
622
+ timeout == null ? undefined : new AbortController();
623
+ const transportSignal =
624
+ signal == null
625
+ ? timeoutController?.signal
626
+ : timeoutController == null
627
+ ? signal
628
+ : AbortSignal.any([signal, timeoutController.signal]);
629
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
503
630
 
504
631
  const messageId = this.requestMessageId++;
505
632
  const jsonrpcRequest: JSONRPCRequest = {
@@ -520,6 +647,9 @@ class DefaultMCPClient implements MCPClient {
520
647
  const cleanup = () => {
521
648
  this.responseHandlers.delete(messageId);
522
649
  signal?.removeEventListener('abort', onAbort);
650
+ if (timeoutId != null) {
651
+ clearTimeout(timeoutId);
652
+ }
523
653
  };
524
654
 
525
655
  const rejectAndCleanup = (error: unknown) => {
@@ -532,6 +662,14 @@ class DefaultMCPClient implements MCPClient {
532
662
  rejectWithAbortError();
533
663
  };
534
664
 
665
+ const onTimeout = () => {
666
+ const error = new MCPClientError({
667
+ message: `Request timed out after ${timeout}ms`,
668
+ });
669
+ timeoutController?.abort(error);
670
+ rejectAndCleanup(error);
671
+ };
672
+
535
673
  this.responseHandlers.set(messageId, response => {
536
674
  if (signal?.aborted) {
537
675
  cleanup();
@@ -557,7 +695,16 @@ class DefaultMCPClient implements MCPClient {
557
695
 
558
696
  signal?.addEventListener('abort', onAbort, { once: true });
559
697
 
560
- this.transport.send(jsonrpcRequest).catch(error => {
698
+ if (timeout != null) {
699
+ timeoutId = setTimeout(onTimeout, timeout);
700
+ }
701
+
702
+ const sendPromise =
703
+ transportSignal == null
704
+ ? this.transport.send(jsonrpcRequest)
705
+ : this.send(jsonrpcRequest, transportSignal);
706
+
707
+ sendPromise.catch(error => {
561
708
  rejectAndCleanup(error);
562
709
  });
563
710
  });
@@ -732,12 +879,18 @@ class DefaultMCPClient implements MCPClient {
732
879
  });
733
880
  }
734
881
 
735
- private async notification(notification: Notification): Promise<void> {
882
+ private async notification(
883
+ notification: Notification,
884
+ options?: { signal?: AbortSignal },
885
+ ): Promise<void> {
736
886
  const jsonrpcNotification: JSONRPCNotification = {
737
887
  ...notification,
738
888
  jsonrpc: '2.0',
739
889
  };
740
- await this.transport.send(jsonrpcNotification);
890
+ await waitForAbort(
891
+ this.send(jsonrpcNotification, options?.signal),
892
+ options?.signal,
893
+ );
741
894
  }
742
895
 
743
896
  /**
@@ -143,29 +143,41 @@ export class HttpMCPTransport implements MCPTransport {
143
143
  this.startInboundSse();
144
144
  }
145
145
 
146
- async close(): Promise<void> {
146
+ async close(options?: { signal?: AbortSignal }): Promise<void> {
147
147
  this.inboundSseConnection?.close();
148
+ this.abortController?.abort();
149
+
148
150
  try {
149
- if (
150
- this.sessionId &&
151
- this.abortController &&
152
- !this.abortController.signal.aborted
153
- ) {
151
+ if (this.sessionId && this.abortController) {
152
+ options?.signal?.throwIfAborted();
154
153
  const headers = await this.commonHeaders({});
154
+ options?.signal?.throwIfAborted();
155
155
  await this.fetchFn(this.url.href, {
156
156
  method: 'DELETE',
157
157
  headers,
158
- signal: this.abortController.signal,
158
+ signal: options?.signal,
159
159
  redirect: this.redirectMode,
160
160
  }).catch(() => undefined);
161
161
  }
162
162
  } catch {}
163
163
 
164
- this.abortController?.abort();
165
164
  this.onclose?.();
166
165
  }
167
166
 
168
- async send(message: JSONRPCMessage): Promise<void> {
167
+ async send(
168
+ message: JSONRPCMessage,
169
+ options?: { signal?: AbortSignal },
170
+ ): Promise<void> {
171
+ options?.signal?.throwIfAborted();
172
+
173
+ const transportSignal = this.abortController?.signal;
174
+ const requestSignal =
175
+ options?.signal == null
176
+ ? transportSignal
177
+ : transportSignal == null
178
+ ? options.signal
179
+ : AbortSignal.any([transportSignal, options.signal]);
180
+
169
181
  const attempt = async (triedAuth: boolean = false): Promise<void> => {
170
182
  try {
171
183
  const headers = await this.commonHeaders({
@@ -177,7 +189,7 @@ export class HttpMCPTransport implements MCPTransport {
177
189
  method: 'POST',
178
190
  headers,
179
191
  body: JSON.stringify(message),
180
- signal: this.abortController?.signal,
192
+ signal: requestSignal,
181
193
  redirect: this.redirectMode,
182
194
  } satisfies RequestInit;
183
195
 
@@ -290,7 +302,10 @@ export class HttpMCPTransport implements MCPTransport {
290
302
  }
291
303
  }
292
304
  } catch (error) {
293
- if (error instanceof Error && error.name === 'AbortError') {
305
+ if (
306
+ options?.signal?.aborted ||
307
+ (error instanceof Error && error.name === 'AbortError')
308
+ ) {
294
309
  return;
295
310
  }
296
311
  this.onerror?.(error);
@@ -298,7 +313,10 @@ export class HttpMCPTransport implements MCPTransport {
298
313
  };
299
314
 
300
315
  void processEvents().catch(error => {
301
- if (error instanceof Error && error.name === 'AbortError') {
316
+ if (
317
+ options?.signal?.aborted ||
318
+ (error instanceof Error && error.name === 'AbortError')
319
+ ) {
302
320
  return;
303
321
  }
304
322
  this.onerror?.(error);
@@ -314,6 +332,9 @@ export class HttpMCPTransport implements MCPTransport {
314
332
  this.onerror?.(error);
315
333
  throw error;
316
334
  } catch (error) {
335
+ if (options?.signal?.aborted) {
336
+ throw error;
337
+ }
317
338
  this.onerror?.(error);
318
339
  throw error;
319
340
  }
@@ -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