fix(mcp-demux): handle Streamable-HTTP handshake correctly
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 17s

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 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-04-14 09:15:38 +03:00
parent 5d6e7f7a80
commit c83e12fb63

View File

@@ -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,