diff --git a/open-sse/mcp-server/httpTransport.ts b/open-sse/mcp-server/httpTransport.ts index ab742858c6..d8826c738c 100644 --- a/open-sse/mcp-server/httpTransport.ts +++ b/open-sse/mcp-server/httpTransport.ts @@ -284,12 +284,30 @@ export async function handleMcpStreamableHTTP(request: Request): Promise { + 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 { diff --git a/tests/unit/mcp-sse-singleton-reset-10772.test.ts b/tests/unit/mcp-sse-singleton-reset-10772.test.ts new file mode 100644 index 0000000000..24250cb7ca --- /dev/null +++ b/tests/unit/mcp-sse-singleton-reset-10772.test.ts @@ -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(); +});