Skip to content

Commit 80ba9a0

Browse files
test(e2e): subscriptions/listen honored filter narrows against advertised capabilities
Adds an e2e cell on entryModern where the listen filter requests toolsListChanged + promptsListChanged + resourcesListChanged but the server advertises only tools.listChanged: honoredFilter on the resolved McpSubscription is { toolsListChanged: true } and only the tools change reaches the stream. A stdio e2e of the modern listen path is not yet feasible without harness changes (the e2e stdio arms wire the standard child-process StdioServerTransport, not the serveStdio entry); stdio narrowing is covered at unit level in serveStdioListen.test.ts. Recorded in the requirement note.
1 parent 4d6e540 commit 80ba9a0

2 files changed

Lines changed: 28 additions & 0 deletions

File tree

test/e2e/requirements.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2684,6 +2684,14 @@ export const REQUIREMENTS: Record<string, Requirement> = {
26842684
transports: ['entryModern'],
26852685
note: 'Hosted by the test body via createMcpHandler so it can publish via handler.notify.'
26862686
},
2687+
'subscriptions:listen:honored-filter-narrows-to-advertised': {
2688+
source: 'https://modelcontextprotocol.io/specification/draft/basic/patterns/subscriptions#acknowledgment',
2689+
behavior:
2690+
"The acknowledged filter on a subscriptions/listen stream is the requested set narrowed against the server's declared listChanged/subscribe capability bits — a requested type the server does not advertise is dropped from honoredFilter and is never delivered.",
2691+
addedInSpecVersion: '2026-07-28',
2692+
transports: ['entryModern'],
2693+
note: 'Hosted by the test body via createMcpHandler so it can publish via handler.notify. A stdio e2e of the modern listen path is not yet feasible without harness changes (the e2e stdio arms wire the standard child-process StdioServerTransport, not the serveStdio entry); stdio narrowing is covered at unit level in serveStdioListen.test.ts.'
2694+
},
26872695
'subscriptions:listen:capacity-guard': {
26882696
source: 'sdk',
26892697
behavior:

test/e2e/scenarios/subscriptions.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,26 @@ verifies('typescript:subscriptions:listen:legacy-era-steer', async ({ transport
114114
expect((error as SdkError).message).toContain('resources/subscribe');
115115
});
116116

117+
verifies('subscriptions:listen:honored-filter-narrows-to-advertised', async () => {
118+
// makeServer registers a tool but no prompts/resources: a listen requesting
119+
// toolsListChanged + promptsListChanged + resourcesListChanged must come
120+
// back honored as toolsListChanged only — the ack reflects only what the
121+
// server advertises.
122+
await using h = await hostListen();
123+
const sub = await h.client.listen({ toolsListChanged: true, promptsListChanged: true, resourcesListChanged: true });
124+
expect(sub.honoredFilter).toEqual({ toolsListChanged: true });
125+
// And nothing the server doesn't advertise reaches the stream: the entry
126+
// delivers via the same narrowed filter it acknowledged.
127+
const seen: string[] = [];
128+
h.client.setNotificationHandler('notifications/prompts/list_changed', () => void seen.push('prompts'));
129+
h.client.setNotificationHandler('notifications/tools/list_changed', () => void seen.push('tools'));
130+
h.handler.notify.promptsChanged();
131+
h.handler.notify.toolsChanged();
132+
await new Promise(r => setTimeout(r, 30));
133+
expect(seen).toEqual(['tools']);
134+
await sub.close();
135+
});
136+
117137
verifies('subscriptions:listen:capacity-guard', async () => {
118138
const handler = createMcpHandler(() => makeServer(), { legacy: 'reject', keepAliveMs: 0, maxSubscriptions: 1 });
119139
const post = (id: number) =>

0 commit comments

Comments
 (0)