From d15d817e92ef946dd16035ac8938ba90b84615b6 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Sun, 18 Oct 2026 20:22:31 -0700 Subject: [PATCH] fix(mcp): leave the live session alone when the new client left during the initialize probe A complete initialize still parses after its client disconnects, so the takeover closed the working transport for a client that would never receive the new session. The takeover now also requires that the request's signal has not aborted. Co-Authored-By: Claude Opus 5.5 --- src/server/http/serveStreamableHttp.test.ts | 24 +++++++++++++++++++++ src/server/http/serveStreamableHttp.ts | 4 +++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/server/http/serveStreamableHttp.test.ts b/src/server/http/serveStreamableHttp.test.ts index accba0c190..9c5b43b22a 100644 --- a/src/server/http/serveStreamableHttp.test.ts +++ b/src/server/http/serveStreamableHttp.test.ts @@ -519,6 +519,30 @@ it("requires exact Accept media ranges before a sessionless POST may replace the await client.close(); }); +it("keeps the live session when the new client left before its initialize was probed", async () => { + const slot = new McpSessionSlot( + createEchoServer(() => undefined), + undefined, + ); + const close = vi.spyOn( + WebStandardStreamableHTTPServerTransport.prototype, + "close", + ); + try { + expect((await slot.handle(initializeRequest())).status).toBe(200); + const left = new AbortController(); + left.abort(); + const abandoned = new Request(initializeRequest(), { signal: left.signal }); + expect((await slot.handle(abandoned)).status).toBe(400); + expect(close).not.toHaveBeenCalled(); + expect((await slot.handle(initializeRequest())).status).toBe(200); + expect(close).toHaveBeenCalledTimes(1); + } finally { + close.mockRestore(); + await slot.close(); + } +}); + it("keeps a session whose transport shutdown failed so the next close retries it", async () => { let shutdowns = 0; const slot = new McpSessionSlot( diff --git a/src/server/http/serveStreamableHttp.ts b/src/server/http/serveStreamableHttp.ts index 432c4ea597..b0ef27637f 100644 --- a/src/server/http/serveStreamableHttp.ts +++ b/src/server/http/serveStreamableHttp.ts @@ -232,7 +232,9 @@ export class McpSessionSlot { const open = this.#live; if ( open?.transport.sessionId !== undefined && - (await isInitializePost(request)) + (await isInitializePost(request)) && + // A client that left while its initialize was probed would evict the session for nobody. + !request.signal.aborted ) { if (this.#closed) return SHUTTING_DOWN(); this.logger?.info("MCP HTTP session replaced by a new client");