From 17b92f851423d4573892c0f9a4e6f3c97402b8b3 Mon Sep 17 00:00:00 2001 From: Christian Gick Date: Tue, 14 Apr 2026 09:30:26 +0300 Subject: [PATCH] fix(mcp-bridge): unique wire ids to avoid concurrent session collisions LiteLLM opens a fresh MCP session per tool call. Two concurrent sessions both send id=1 (tools/list + tools/call) which overwrite each other in the shared pending-map keyed by the inbound id. One of the two calls never gets a response and times out. Assign a monotonic demux-N wire id per forwardMcpCall. The caller's original id is echoed back in the HTTP response by the route handler. SB-48 Co-Authored-By: Claude Opus 4.6 --- src/mcp/bridge.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/mcp/bridge.ts b/src/mcp/bridge.ts index ae9c6d6..01ab1d6 100644 --- a/src/mcp/bridge.ts +++ b/src/mcp/bridge.ts @@ -81,6 +81,12 @@ export function isCustomerOnline(customerId: string): boolean { * Forward a JSON-RPC MCP call to the customer's WebSocket and return the result. * Throws if customer is offline or times out. */ +// Monotonic counter to ensure each WS-forwarded call has a unique id. +// Caller-provided ids (e.g. from LiteLLM) can collide across concurrent +// sessions — LiteLLM opens a fresh session per tool call and each uses +// id=1. We wrap with our own id on the wire and translate back. +let nextWireId = 1; + export async function forwardMcpCall( customerId: string, request: { method: string; params?: unknown; id: string | number }, @@ -91,19 +97,28 @@ export async function forwardMcpCall( throw new McpBridgeError("tool_unavailable: customer not online", -32001); } + const wireId = `demux-${nextWireId++}`; + return new Promise((resolve, reject) => { const timer = setTimeout(() => { - session.pending.delete(request.id); + session.pending.delete(wireId); reject(new McpBridgeError(`tool_unavailable: timeout after ${timeoutMs}ms`, -32002)); }, timeoutMs); - session.pending.set(request.id, { resolve, reject, timer }); + session.pending.set(wireId, { resolve, reject, timer }); try { - session.ws.send(JSON.stringify({ jsonrpc: "2.0", ...request })); + session.ws.send( + JSON.stringify({ + jsonrpc: "2.0", + method: request.method, + params: request.params, + id: wireId, + }) + ); } catch (err) { clearTimeout(timer); - session.pending.delete(request.id); + session.pending.delete(wireId); reject(err); } });