fix(api): reset mcp sse singleton on new client initialize (#10690) (#10772)

Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
This commit is contained in:
Sahil Singh
2026-08-20 14:59:49 +05:30
committed by GitHub
parent 49a47cbe6f
commit 6ff83077fd
2 changed files with 99 additions and 0 deletions

View File

@@ -284,12 +284,30 @@ export async function handleMcpStreamableHTTP(request: Request): Promise<Respons
return protectMcpSseResponse(request, await handleStreamableRequest(request));
}
interface RpcRequest {
method?: string;
[key: string]: unknown;
}
/**
* Handle SSE requests.
* SSE transport is implemented via Streamable HTTP transport with GET for SSE stream
* and POST for messages (the Streamable HTTP transport supports both patterns).
*/
export async function handleMcpSSE(request: Request): Promise<Response> {
if (request.method === "POST") {
try {
const body = await request.clone().json();
const isInitialize = Array.isArray(body)
? body.some((req: RpcRequest) => req?.method === "initialize")
: (body as RpcRequest)?.method === "initialize";
if (isInitialize) {
console.log("[MCP] New client initialize detected, resetting SSE singleton...");
closeSseTransport();
}
} catch (err) {}
}
const { transport } = ensureSseServer();
try {

View File

@@ -0,0 +1,81 @@
import test from "node:test";
import assert from "node:assert/strict";
const mod = await import("../../open-sse/mcp-server/httpTransport.ts");
function initializeRequest(id: number): Request {
return new Request("http://localhost/api/mcp/sse", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
},
body: JSON.stringify({
jsonrpc: "2.0",
method: "initialize",
id,
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "test-client", version: "1.0.0" },
},
}),
});
}
function batchedInitializeRequest(id: number): Request {
return new Request("http://localhost/api/mcp/sse", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
},
body: JSON.stringify([
{
jsonrpc: "2.0",
method: "initialize",
id,
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "test-client", version: "1.0.0" },
},
},
]),
});
}
// ── Regression #10690 / PR #10772: a second `initialize` POST must reset the
// SSE singleton instead of failing with 400 ("already initialized"). ─────────
test("handleMcpSSE: a second POST initialize after a successful first one does not return 400", async () => {
mod.shutdownMcpHttp();
const firstRes = await mod.handleMcpSSE(initializeRequest(1));
assert.notEqual(firstRes.status, 400, "first initialize should not fail");
const secondRes = await mod.handleMcpSSE(initializeRequest(2));
assert.notEqual(
secondRes.status,
400,
"a second client initialize must reset the SSE singleton instead of returning 400"
);
mod.shutdownMcpHttp();
});
test("handleMcpSSE: a batched (array) JSON-RPC body containing initialize also resets the singleton", async () => {
mod.shutdownMcpHttp();
const firstRes = await mod.handleMcpSSE(initializeRequest(1));
assert.notEqual(firstRes.status, 400, "first initialize should not fail");
const batchedRes = await mod.handleMcpSSE(batchedInitializeRequest(2));
assert.notEqual(
batchedRes.status,
400,
"a batched initialize entry must also trigger the SSE singleton reset"
);
mod.shutdownMcpHttp();
});