fix(mcp-demux): handle Streamable-HTTP handshake correctly
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 17s
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:
@@ -25,11 +25,39 @@ export async function mcpDemuxRoutes(app: FastifyInstance) {
|
|||||||
|
|
||||||
const { customer_id } = req.params;
|
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)) {
|
if (!isCustomerOnline(customer_id)) {
|
||||||
// Return a JSON-RPC error in MCP format
|
// Return a JSON-RPC error in MCP format
|
||||||
return reply.code(200).send({
|
return reply.code(200).send({
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id: (req.body as { id?: unknown })?.id ?? null,
|
id: body.id ?? null,
|
||||||
error: {
|
error: {
|
||||||
code: -32001,
|
code: -32001,
|
||||||
message: "tool_unavailable: customer not online",
|
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();
|
const callId = body.id ?? randomUUID();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -52,6 +74,7 @@ export async function mcpDemuxRoutes(app: FastifyInstance) {
|
|||||||
config.mcpBridgeTimeoutMs
|
config.mcpBridgeTimeoutMs
|
||||||
);
|
);
|
||||||
|
|
||||||
|
reply.header("Content-Type", "application/json");
|
||||||
return reply.send({
|
return reply.send({
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id: callId,
|
id: callId,
|
||||||
|
|||||||
Reference in New Issue
Block a user