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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 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
|