From 2b6a163a683bc562cb025d5c76bdc35ea0694e87 Mon Sep 17 00:00:00 2001 From: Seal Wang Date: Tue, 1 Sep 2026 18:50:26 +0800 Subject: [PATCH 1/2] fix(server): complete empty listen subscriptions --- packages/server/src/server/listenRouter.ts | 11 +++++++- .../server/createMcpHandlerListen.test.ts | 26 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/listenRouter.ts b/packages/server/src/server/listenRouter.ts index 40c4a38cf2..64e1f302ca 100644 --- a/packages/server/src/server/listenRouter.ts +++ b/packages/server/src/server/listenRouter.ts @@ -213,6 +213,15 @@ export function createListenRouter(options: ListenRouterOptions): ListenRouter { ); writeNotification(ack.method, ack.params); + // A stream that honoured no notification types can never + // deliver an event. Acknowledge that outcome, then complete + // it instead of retaining a socket, bus subscription, and + // keep-alive timer indefinitely. + if (Object.keys(honored).length === 0) { + teardown(true); + return; + } + // Only after the ack frame is enqueued does delivery activate. unsubscribe = bus.subscribe(event => { if (closed || !listenFilterAccepts(honored, event)) return; @@ -232,7 +241,7 @@ export function createListenRouter(options: ListenRouterOptions): ListenRouter { } }); - if (signal !== undefined) { + if (!closed && signal !== undefined) { if (signal.aborted) { teardown(false); } else { diff --git a/packages/server/test/server/createMcpHandlerListen.test.ts b/packages/server/test/server/createMcpHandlerListen.test.ts index fe17aa7536..26118b720a 100644 --- a/packages/server/test/server/createMcpHandlerListen.test.ts +++ b/packages/server/test/server/createMcpHandlerListen.test.ts @@ -170,6 +170,32 @@ describe('createMcpHandler — subscriptions/listen', () => { await handler.close(); }); + it('acknowledges and completes an empty honored filter without opening a subscription', async () => { + vi.useFakeTimers(); + try { + const subscribe = vi.fn(() => () => {}); + const bus: ServerEventBus = { publish() {}, subscribe }; + const handler = createMcpHandler(() => new McpServer({ name: 'no-listen-capabilities', version: '1.0.0' }), { + bus, + keepAliveMs: 1000 + }); + + const response = await handler.fetch(listenRequest(12, { toolsListChanged: true })); + const text = await response.text(); + + expect(text).toContain('notifications/subscriptions/acknowledged'); + expect(text).toContain('"notifications":{}'); + expect(text).toContain('"resultType":"complete"'); + expect(text.indexOf('notifications/subscriptions/acknowledged')).toBeLessThan(text.indexOf('"resultType":"complete"')); + expect(subscribe).not.toHaveBeenCalled(); + expect(vi.getTimerCount()).toBe(0); + + await handler.close(); + } finally { + vi.useRealTimers(); + } + }); + it('delivers only opted-in change types, each stamped with the subscription id', async () => { const handler = createMcpHandler(trivialFactory(), { keepAliveMs: 0 }); const response = await handler.fetch(listenRequest(7, { toolsListChanged: true, resourceSubscriptions: ['file:///a'] })); From c549642f07c2b494644c4a27d438c28ef08f3b9b Mon Sep 17 00:00:00 2001 From: Seal Wang Date: Fri, 4 Sep 2026 15:35:45 +0800 Subject: [PATCH 2/2] test(e2e): retain a subscribed stream for capacity guard --- .changeset/complete-empty-listen-subscriptions.md | 5 +++++ test/e2e/scenarios/subscriptions.test.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/complete-empty-listen-subscriptions.md diff --git a/.changeset/complete-empty-listen-subscriptions.md b/.changeset/complete-empty-listen-subscriptions.md new file mode 100644 index 0000000000..82b5e616c3 --- /dev/null +++ b/.changeset/complete-empty-listen-subscriptions.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/server': patch +--- + +Complete subscriptions that do not honor any requested notification types instead of retaining an idle SSE connection. diff --git a/test/e2e/scenarios/subscriptions.test.ts b/test/e2e/scenarios/subscriptions.test.ts index ce644fa430..ef3a42e444 100644 --- a/test/e2e/scenarios/subscriptions.test.ts +++ b/test/e2e/scenarios/subscriptions.test.ts @@ -276,7 +276,7 @@ verifies('subscriptions:listen:capacity-guard', async () => { jsonrpc: '2.0', id, method: 'subscriptions/listen', - params: { _meta: modernEnvelopeMeta(), notifications: {} } + params: { _meta: modernEnvelopeMeta(), notifications: { toolsListChanged: true } } }) }) );