From 0fde67952ef32c701a3591be85b5a122735ce4a9 Mon Sep 17 00:00:00 2001 From: Genmin Date: Wed, 29 Apr 2026 16:54:08 -0700 Subject: [PATCH 1/3] fix: avoid duplicate SSE close callbacks --- src/server/sse.ts | 6 ++++-- test/server/sse.test.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/server/sse.ts b/src/server/sse.ts index 4931beae60..1976c2a7d1 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -124,8 +124,10 @@ export class SSEServerTransport implements Transport { this._sseResponse = this.res; this.res.on('close', () => { - this._sseResponse = undefined; - this.onclose?.(); + if (this._sseResponse !== undefined) { + this._sseResponse = undefined; + this.onclose?.(); + } }); } diff --git a/test/server/sse.test.ts b/test/server/sse.test.ts index 0e996d1d64..09b3116188 100644 --- a/test/server/sse.test.ts +++ b/test/server/sse.test.ts @@ -442,6 +442,39 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { await transport.close(); expect(transport.onclose).toHaveBeenCalled(); }); + + it('should only call onclose once when close ends the SSE response', async () => { + const onclose = vi.fn(); + const server = createServer(async (_req, res) => { + const transport = new SSEServerTransport('/messages', res); + transport.onclose = onclose; + + await transport.start(); + await transport.close(); + }); + + const baseUrl = await listenOnRandomPort(server); + + try { + await new Promise((resolve, reject) => { + const req = http.request(baseUrl, { headers: { Accept: 'text/event-stream' } }, response => { + response.resume(); + response.on('end', resolve); + response.on('error', reject); + }); + + req.on('error', reject); + req.end(); + }); + + await new Promise(resolve => setTimeout(resolve, 50)); + expect(onclose).toHaveBeenCalledTimes(1); + } finally { + await new Promise((resolve, reject) => { + server.close(error => (error ? reject(error) : resolve())); + }); + } + }); }); describe('send method', () => { From 3e3b2908d75a49ca49ab1a7d0f30d5cecba7a1c7 Mon Sep 17 00:00:00 2001 From: Genmin Date: Thu, 30 Apr 2026 07:35:04 -0700 Subject: [PATCH 2/3] chore: add sse close changeset --- .changeset/fix-v1x-sse-close-once.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-v1x-sse-close-once.md diff --git a/.changeset/fix-v1x-sse-close-once.md b/.changeset/fix-v1x-sse-close-once.md new file mode 100644 index 0000000000..6f17c231ae --- /dev/null +++ b/.changeset/fix-v1x-sse-close-once.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/sdk": patch +--- + +fix: avoid duplicate SSE close callbacks From 1c9fe2c5dd10d3559187a46f8004d50281127968 Mon Sep 17 00:00:00 2001 From: Genmin Date: Thu, 30 Apr 2026 07:53:46 -0700 Subject: [PATCH 3/3] chore: format sse close changeset --- .changeset/fix-v1x-sse-close-once.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix-v1x-sse-close-once.md b/.changeset/fix-v1x-sse-close-once.md index 6f17c231ae..fb7c886b47 100644 --- a/.changeset/fix-v1x-sse-close-once.md +++ b/.changeset/fix-v1x-sse-close-once.md @@ -1,5 +1,5 @@ --- -"@modelcontextprotocol/sdk": patch +'@modelcontextprotocol/sdk': patch --- fix: avoid duplicate SSE close callbacks