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); } });