fix(mcp-bridge): unique wire ids to avoid concurrent session collisions
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 11s

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 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-04-14 09:30:26 +03:00
parent c83e12fb63
commit 17b92f8514

View File

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