From 79e97432112c6ab01c0e2b33c2bab6997e1d2463 Mon Sep 17 00:00:00 2001 From: vjymisal0 Date: Thu, 10 Sep 2026 16:05:43 +0530 Subject: [PATCH 1/4] fix(server): restore onclose after modern exchanges --- .../server/src/server/createMcpHandler.ts | 1 + .../test/server/createMcpHandler.test.ts | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/server/src/server/createMcpHandler.ts b/packages/server/src/server/createMcpHandler.ts index 82cfa061bc..fc60079d0d 100644 --- a/packages/server/src/server/createMcpHandler.ts +++ b/packages/server/src/server/createMcpHandler.ts @@ -869,6 +869,7 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa inflight.add(server); server.onclose = () => { inflight.delete(server); + server.onclose = previousOnClose; previousOnClose?.(); }; diff --git a/packages/server/test/server/createMcpHandler.test.ts b/packages/server/test/server/createMcpHandler.test.ts index 4dae5c0ad7..e1175db22f 100644 --- a/packages/server/test/server/createMcpHandler.test.ts +++ b/packages/server/test/server/createMcpHandler.test.ts @@ -274,6 +274,27 @@ describe('createMcpHandler — modern path', () => { expect(onerror).toHaveBeenCalledWith(expect.objectContaining({ message: 'factory exploded' })); }); + it('restores a reused server onclose handler after each modern exchange', async () => { + const reused = new McpServer({ name: 'entry-test-server', version: '1.0.0' }); + reused.registerTool('echo', { inputSchema: z.object({ text: z.string() }) }, async ({ text }) => ({ + content: [{ type: 'text', text }] + })); + + const originalOnClose = vi.fn(); + reused.server.onclose = originalOnClose; + + const handler = createMcpHandler(() => reused); + + for (let i = 0; i < 3; i++) { + const response = await handler.fetch(postRequest(modernToolsCall('echo', { text: `hello-${i}` }))); + expect(response.status).toBe(200); + await response.text(); + expect(reused.server.onclose).toBe(originalOnClose); + } + + expect(originalOnClose).toHaveBeenCalledTimes(3); + }); + it('closes and releases the per-request instance when a modern exchange fails internally', async () => { const { factory, state } = testFactory(); const onerror = vi.fn(); From 37260aaff86b2cee2a80524ec50a9213ffc58cc0 Mon Sep 17 00:00:00 2001 From: vjymisal0 Date: Thu, 17 Sep 2026 11:03:46 +0530 Subject: [PATCH 2/4] chore(server): add changeset for onclose restoration Signed-off-by: vjymisal0 --- .changeset/curvy-rivers-restore.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/curvy-rivers-restore.md diff --git a/.changeset/curvy-rivers-restore.md b/.changeset/curvy-rivers-restore.md new file mode 100644 index 0000000000..6b76b3ea0d --- /dev/null +++ b/.changeset/curvy-rivers-restore.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/server': patch +--- + +Restore a reused server's original `onclose` handler after each modern exchange so repeated requests do not accumulate nested handlers. From 041fb0e4143faa5fe99e0700995629636dec0ff5 Mon Sep 17 00:00:00 2001 From: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:09:30 +0000 Subject: [PATCH 3/4] chore(changeset): say what was fixed and keep the fresh-instance contract --- .changeset/curvy-rivers-restore.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/curvy-rivers-restore.md b/.changeset/curvy-rivers-restore.md index 6b76b3ea0d..1c15f14631 100644 --- a/.changeset/curvy-rivers-restore.md +++ b/.changeset/curvy-rivers-restore.md @@ -2,4 +2,4 @@ '@modelcontextprotocol/server': patch --- -Restore a reused server's original `onclose` handler after each modern exchange so repeated requests do not accumulate nested handlers. +Fix a stack overflow in `createMcpHandler` when the factory returns the same server instance for more than one request. Returning a fresh instance per request is still required. From 67f14d2c86b66ac3437162e4eaeb2504fba446dc Mon Sep 17 00:00:00 2001 From: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:16:22 +0000 Subject: [PATCH 4/4] fix(server): restore onclose by identity so a handler set during the exchange is kept --- .../server/src/server/createMcpHandler.ts | 8 ++++-- .../test/server/createMcpHandler.test.ts | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/server/src/server/createMcpHandler.ts b/packages/server/src/server/createMcpHandler.ts index fc60079d0d..52ac41fd20 100644 --- a/packages/server/src/server/createMcpHandler.ts +++ b/packages/server/src/server/createMcpHandler.ts @@ -867,11 +867,15 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa // Track the instance until its exchange tears down so close() can abort it. const previousOnClose = server.onclose; inflight.add(server); - server.onclose = () => { + const onExchangeClose = () => { inflight.delete(server); - server.onclose = previousOnClose; + // Restore by identity so a handler installed during the exchange is kept. + if (server.onclose === onExchangeClose) { + server.onclose = previousOnClose; + } previousOnClose?.(); }; + server.onclose = onExchangeClose; try { const response = await invoke(product, route.message, { diff --git a/packages/server/test/server/createMcpHandler.test.ts b/packages/server/test/server/createMcpHandler.test.ts index e1175db22f..8de6672f49 100644 --- a/packages/server/test/server/createMcpHandler.test.ts +++ b/packages/server/test/server/createMcpHandler.test.ts @@ -295,6 +295,32 @@ describe('createMcpHandler — modern path', () => { expect(originalOnClose).toHaveBeenCalledTimes(3); }); + it('keeps an onclose handler that was installed during the exchange', async () => { + const reused = new McpServer({ name: 'entry-test-server', version: '1.0.0' }); + const installedDuringExchange = vi.fn(); + let chained: (() => void) | undefined; + reused.registerTool('echo', { inputSchema: z.object({ text: z.string() }) }, async ({ text }) => { + if (chained === undefined) { + const previous = reused.server.onclose; + chained = () => { + installedDuringExchange(); + previous?.(); + }; + reused.server.onclose = chained; + } + return { content: [{ type: 'text', text }] }; + }); + + const handler = createMcpHandler(() => reused); + + const response = await handler.fetch(postRequest(modernToolsCall('echo', { text: 'hello' }))); + expect(response.status).toBe(200); + await response.text(); + + expect(reused.server.onclose).toBe(chained); + expect(installedDuringExchange).toHaveBeenCalledTimes(1); + }); + it('closes and releases the per-request instance when a modern exchange fails internally', async () => { const { factory, state } = testFactory(); const onerror = vi.fn();