Skip to content

Commit 5c8c681

Browse files
committed
Handle closed connection errors in MCP HTTP client
1 parent 7bc9b59 commit 5c8c681

File tree

2 files changed

+47
-13
lines changed

2 files changed

+47
-13
lines changed

src/lib/server/mcp/clientPool.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,12 @@ export async function drainPool() {
5959
pool.delete(key);
6060
}
6161
}
62+
63+
export function evictFromPool(server: McpServerConfig): Client | undefined {
64+
const key = keyOf(server);
65+
const client = pool.get(key);
66+
if (client) {
67+
pool.delete(key);
68+
}
69+
return client;
70+
}

src/lib/server/mcp/httpClient.ts

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
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+
}
38

49
export interface McpServerConfig {
510
name: string;
@@ -34,20 +39,40 @@ export async function callMcpTool(
3439

3540
// Get a (possibly pooled) client. The client itself was connected with a signal
3641
// 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+
};
3851

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;
4962
}
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+
}
5176

5277
const parts = Array.isArray(response?.content) ? (response.content as Array<unknown>) : [];
5378
const textParts = parts

0 commit comments

Comments
 (0)