victor HF Staff commited on
Commit
99156e4
·
1 Parent(s): d6b4c4f

Handle closed connection errors in MCP HTTP client

Browse files
src/lib/server/mcp/clientPool.ts CHANGED
@@ -59,3 +59,12 @@ export async function drainPool() {
59
  pool.delete(key);
60
  }
61
  }
 
 
 
 
 
 
 
 
 
 
59
  pool.delete(key);
60
  }
61
  }
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 CHANGED
@@ -1,5 +1,10 @@
1
  import { Client } from "@modelcontextprotocol/sdk/client";
2
- import { getClient } from "./clientPool";
 
 
 
 
 
3
 
4
  export interface McpServerConfig {
5
  name: string;
@@ -34,20 +39,40 @@ export async function callMcpTool(
34
 
35
  // Get a (possibly pooled) client. The client itself was connected with a signal
36
  // that already composes outer cancellation. We still enforce a per-call timeout here.
37
- const activeClient = client ?? (await getClient(server, signal));
 
 
 
 
 
 
 
 
38
 
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,
49
  }
50
- );
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  const parts = Array.isArray(response?.content) ? (response.content as Array<unknown>) : [];
53
  const textParts = parts
 
1
  import { Client } from "@modelcontextprotocol/sdk/client";
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
+ }
8
 
9
  export interface McpServerConfig {
10
  name: string;
 
39
 
40
  // Get a (possibly pooled) client. The client itself was connected with a signal
41
  // that already composes outer cancellation. We still enforce a per-call timeout here.
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
+ };
51
 
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;
62
  }
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
+ }
76
 
77
  const parts = Array.isArray(response?.content) ? (response.content as Array<unknown>) : [];
78
  const textParts = parts