|
1 | 1 | import { Client } from "@modelcontextprotocol/sdk/client"; |
2 | | -import { getClient } from "./clientPool"; |
| 2 | +import { getClient, evictFromPool } from "./clientPool"; |
| 3 | + |
| 4 | +function isConnectionClosedError(err: unknown): boolean { |
| 5 | + const message = err instanceof Error ? err.message : String(err); |
| 6 | + return message.includes("-32000") || message.toLowerCase().includes("connection closed"); |
| 7 | +} |
3 | 8 |
|
4 | 9 | export interface McpServerConfig { |
5 | 10 | name: string; |
@@ -34,20 +39,40 @@ export async function callMcpTool( |
34 | 39 |
|
35 | 40 | // Get a (possibly pooled) client. The client itself was connected with a signal |
36 | 41 | // that already composes outer cancellation. We still enforce a per-call timeout here. |
37 | | - const activeClient = client ?? (await getClient(server, signal)); |
| 42 | + let activeClient = client ?? (await getClient(server, signal)); |
| 43 | + |
| 44 | + const callToolOptions = { |
| 45 | + signal, |
| 46 | + timeout: timeoutMs, |
| 47 | + // Enable progress tokens so long-running tools keep extending the timeout. |
| 48 | + onprogress: () => {}, |
| 49 | + resetTimeoutOnProgress: true, |
| 50 | + }; |
38 | 51 |
|
39 | | - // Prefer the SDK's built-in request controls (timeout, signal) |
40 | | - const response = await activeClient.callTool( |
41 | | - { name: tool, arguments: normalizedArgs }, |
42 | | - undefined, |
43 | | - { |
44 | | - signal, |
45 | | - timeout: timeoutMs, |
46 | | - // Enable progress tokens so long-running tools keep extending the timeout. |
47 | | - onprogress: () => {}, |
48 | | - resetTimeoutOnProgress: true, |
| 52 | + let response; |
| 53 | + try { |
| 54 | + response = await activeClient.callTool( |
| 55 | + { name: tool, arguments: normalizedArgs }, |
| 56 | + undefined, |
| 57 | + callToolOptions |
| 58 | + ); |
| 59 | + } catch (err) { |
| 60 | + if (!isConnectionClosedError(err)) { |
| 61 | + throw err; |
49 | 62 | } |
50 | | - ); |
| 63 | + |
| 64 | + // Evict stale client and close it |
| 65 | + const stale = evictFromPool(server); |
| 66 | + stale?.close?.().catch(() => {}); |
| 67 | + |
| 68 | + // Retry with fresh client |
| 69 | + activeClient = await getClient(server, signal); |
| 70 | + response = await activeClient.callTool( |
| 71 | + { name: tool, arguments: normalizedArgs }, |
| 72 | + undefined, |
| 73 | + callToolOptions |
| 74 | + ); |
| 75 | + } |
51 | 76 |
|
52 | 77 | const parts = Array.isArray(response?.content) ? (response.content as Array<unknown>) : []; |
53 | 78 | const textParts = parts |
|
0 commit comments