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'] }));