From c83e12fb6314da04e15a4dda5d0636a88a940da6 Mon Sep 17 00:00:00 2001 From: Christian Gick Date: Tue, 14 Apr 2026 09:15:38 +0300 Subject: [PATCH] fix(mcp-demux): handle Streamable-HTTP handshake correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LiteLLM opens a fresh MCP session per tools/call: initialize → notifications/initialized → tools/call. Demux forwarded everything to the WS unchanged, so: - notifications/initialized (no id) hung forwardMcpCall waiting on a response id, and the 200 JSON-RPC body confused the SDK reader into cancelling the subsequent tools/call ("duplicate response suppressed"). - initialize added an unnecessary WS round-trip coupled to extension availability. Answer initialize locally, return 202 empty for notifications/*, set explicit Content-Type + Mcp-Session-Id on the handshake. SB-48 Co-Authored-By: Claude Opus 4.6 --- src/routes/mcpDemux.ts | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/routes/mcpDemux.ts b/src/routes/mcpDemux.ts index b851c3e..f742e71 100644 --- a/src/routes/mcpDemux.ts +++ b/src/routes/mcpDemux.ts @@ -25,11 +25,39 @@ export async function mcpDemuxRoutes(app: FastifyInstance) { const { customer_id } = req.params; + const body = req.body as { + method: string; + params?: unknown; + id?: string | number; + }; + + // MCP Streamable-HTTP: notifications must return 202 empty, never forward. + // notifications have no id; forwardMcpCall would hang waiting for a response. + if (body.method?.startsWith("notifications/")) { + return reply.code(202).send(); + } + + // Answer initialize locally — avoids a WS round-trip per LiteLLM tool call + // and makes the handshake robust to transient extension disconnects. + if (body.method === "initialize") { + reply.header("Mcp-Session-Id", randomUUID()); + reply.header("Content-Type", "application/json"); + return reply.send({ + jsonrpc: "2.0", + id: body.id ?? null, + result: { + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "sitebridge-demux", version: "1.0.0" }, + }, + }); + } + if (!isCustomerOnline(customer_id)) { // Return a JSON-RPC error in MCP format return reply.code(200).send({ jsonrpc: "2.0", - id: (req.body as { id?: unknown })?.id ?? null, + id: body.id ?? null, error: { code: -32001, message: "tool_unavailable: customer not online", @@ -37,12 +65,6 @@ export async function mcpDemuxRoutes(app: FastifyInstance) { }); } - const body = req.body as { - method: string; - params?: unknown; - id?: string | number; - }; - const callId = body.id ?? randomUUID(); try { @@ -52,6 +74,7 @@ export async function mcpDemuxRoutes(app: FastifyInstance) { config.mcpBridgeTimeoutMs ); + reply.header("Content-Type", "application/json"); return reply.send({ jsonrpc: "2.0", id: callId,