@ai-sdk/mcp 2.0.4 → 2.0.5

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.4",
3
+ "version": "2.0.5",
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.1",
36
- "@ai-sdk/provider-utils": "5.0.2"
36
+ "@ai-sdk/provider-utils": "5.0.3"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "22.19.19",
@@ -3,6 +3,7 @@ import {
3
3
  asSchema,
4
4
  dynamicTool,
5
5
  jsonSchema,
6
+ retryWithExponentialBackoff,
6
7
  safeParseJSON,
7
8
  safeValidateTypes,
8
9
  tool,
@@ -66,6 +67,82 @@ import {
66
67
  type InitializeResult,
67
68
  } from './types';
68
69
  const CLIENT_VERSION = '1.0.0';
70
+ const DEFAULT_MAX_TOOL_CALL_RETRIES = 0;
71
+
72
+ const DEFAULT_RETRY_ERROR_CODES = [
73
+ 'ConnectionRefused',
74
+ 'ConnectionClosed',
75
+ 'FailedToOpenSocket',
76
+ 'ECONNRESET',
77
+ 'ECONNREFUSED',
78
+ 'ETIMEDOUT',
79
+ 'EPIPE',
80
+ ];
81
+
82
+ function getErrorStatusCode(error: unknown): number | undefined {
83
+ if (
84
+ error != null &&
85
+ typeof error === 'object' &&
86
+ 'statusCode' in error &&
87
+ typeof error.statusCode === 'number'
88
+ ) {
89
+ return error.statusCode;
90
+ }
91
+
92
+ return undefined;
93
+ }
94
+
95
+ function getStringErrorCode(error: unknown): string | undefined {
96
+ if (
97
+ error != null &&
98
+ typeof error === 'object' &&
99
+ 'code' in error &&
100
+ typeof error.code === 'string'
101
+ ) {
102
+ return error.code;
103
+ }
104
+
105
+ return undefined;
106
+ }
107
+
108
+ function isRetryableMCPToolCallError(error: unknown): boolean {
109
+ const statusCode = getErrorStatusCode(error);
110
+ if (statusCode != null) {
111
+ return (
112
+ statusCode === 408 ||
113
+ statusCode === 409 ||
114
+ statusCode === 429 ||
115
+ statusCode >= 500
116
+ );
117
+ }
118
+
119
+ if (MCPClientError.isInstance(error) && error.code != null) {
120
+ return false;
121
+ }
122
+
123
+ const errorCode = getStringErrorCode(error);
124
+ return errorCode != null && DEFAULT_RETRY_ERROR_CODES.includes(errorCode);
125
+ }
126
+
127
+ function prepareMaxRetries(maxRetries: number | undefined): number {
128
+ if (maxRetries == null) {
129
+ return DEFAULT_MAX_TOOL_CALL_RETRIES;
130
+ }
131
+
132
+ if (!Number.isInteger(maxRetries)) {
133
+ throw new MCPClientError({
134
+ message: 'maxRetries must be an integer',
135
+ });
136
+ }
137
+
138
+ if (maxRetries < 0) {
139
+ throw new MCPClientError({
140
+ message: 'maxRetries must be >= 0',
141
+ });
142
+ }
143
+
144
+ return maxRetries;
145
+ }
69
146
 
70
147
  function mcpToModelOutput({
71
148
  output,
@@ -104,6 +181,15 @@ export interface MCPClientConfig {
104
181
  transport: MCPTransportConfig | MCPTransport;
105
182
  /** Optional callback for uncaught errors */
106
183
  onUncaughtError?: (error: unknown) => void;
184
+ /**
185
+ * Maximum number of retries for transient MCP tool call failures.
186
+ *
187
+ * Set to 0 to disable retries. Retries only apply to tools/call requests.
188
+ * JSON-RPC application errors, such as invalid params, are not retried.
189
+ *
190
+ * @default 0
191
+ */
192
+ maxRetries?: number;
107
193
  /**
108
194
  * Initialize result from a previous MCP session. When provided, the client
109
195
  * starts the transport and reuses this metadata without sending a new
@@ -248,6 +334,7 @@ export interface MCPClient {
248
334
  class DefaultMCPClient implements MCPClient {
249
335
  private transport: MCPTransport;
250
336
  private onUncaughtError?: (error: unknown) => void;
337
+ private maxRetries: number;
251
338
  private clientInfo: ClientConfiguration;
252
339
  private clientCapabilities: ClientCapabilities;
253
340
  private initialInitializeResult?: InitializeResult;
@@ -275,10 +362,12 @@ class DefaultMCPClient implements MCPClient {
275
362
  clientName = name ?? 'ai-sdk-mcp-client',
276
363
  version = CLIENT_VERSION,
277
364
  onUncaughtError,
365
+ maxRetries,
278
366
  capabilities,
279
367
  initialInitializeResult,
280
368
  }: MCPClientConfig) {
281
369
  this.onUncaughtError = onUncaughtError;
370
+ this.maxRetries = prepareMaxRetries(maxRetries);
282
371
  this.clientCapabilities = capabilities ?? {};
283
372
  this.initialInitializeResult = initialInitializeResult;
284
373
 
@@ -518,6 +607,29 @@ class DefaultMCPClient implements MCPClient {
518
607
  });
519
608
  }
520
609
 
610
+ private async callToolWithRetry({
611
+ options,
612
+ execute,
613
+ }: {
614
+ options?: RequestOptions;
615
+ execute: () => Promise<CallToolResult>;
616
+ }): Promise<CallToolResult> {
617
+ if (this.maxRetries === 0) {
618
+ return execute();
619
+ }
620
+
621
+ return retryWithExponentialBackoff({
622
+ maxRetries: this.maxRetries,
623
+ abortSignal: options?.signal,
624
+ shouldRetry: isRetryableMCPToolCallError,
625
+ createRetryError: ({ message, errors }) =>
626
+ new MCPClientError({
627
+ message,
628
+ cause: errors[errors.length - 1],
629
+ }),
630
+ })(execute);
631
+ }
632
+
521
633
  async callTool({
522
634
  name,
523
635
  arguments: args = {},
@@ -528,10 +640,17 @@ class DefaultMCPClient implements MCPClient {
528
640
  options?: RequestOptions;
529
641
  }): Promise<CallToolResult> {
530
642
  try {
531
- return this.request({
532
- request: { method: 'tools/call', params: { name, arguments: args } },
533
- resultSchema: CallToolResultSchema,
643
+ return this.callToolWithRetry({
534
644
  options,
645
+ execute: () =>
646
+ this.request({
647
+ request: {
648
+ method: 'tools/call',
649
+ params: { name, arguments: args },
650
+ },
651
+ resultSchema: CallToolResultSchema,
652
+ options,
653
+ }),
535
654
  });
536
655
  } catch (error) {
537
656
  throw error;