From ef446bdedcefdb34f9a5612c12b5b7ba914078d9 Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:35:59 +0530 Subject: [PATCH 1/2] fix(server): allow inline list changed notifications --- packages/server/src/server/mcp.ts | 38 +++++++------ packages/server/src/server/server.ts | 19 ++++--- .../server/test/server/streamableHttp.test.ts | 46 ++++++++++++++++ test/integration/test/server.test.ts | 53 +++++++++++++++++++ test/integration/test/server/mcp.test.ts | 37 +++++++++++++ 5 files changed, 168 insertions(+), 25 deletions(-) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 4d9f81c50..5148693cc 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -13,6 +13,7 @@ import type { ListResourcesResult, ListToolsResult, LoggingMessageNotification, + NotificationOptions, Prompt, PromptReference, ReadResourceResult, @@ -599,7 +600,7 @@ export class McpServer { ); this.setResourceRequestHandlers(); - this.sendResourceListChanged(); + void this.sendResourceListChanged(); return registeredResource; } else { if (this._registeredResourceTemplates[name]) { @@ -615,7 +616,7 @@ export class McpServer { ); this.setResourceRequestHandlers(); - this.sendResourceListChanged(); + void this.sendResourceListChanged(); return registeredResourceTemplate; } } @@ -646,7 +647,7 @@ export class McpServer { if (updates.metadata !== undefined) registeredResource.metadata = updates.metadata; if (updates.callback !== undefined) registeredResource.readCallback = updates.callback; if (updates.enabled !== undefined) registeredResource.enabled = updates.enabled; - this.sendResourceListChanged(); + void this.sendResourceListChanged(); } }; this._registeredResources[uri] = registeredResource; @@ -679,7 +680,7 @@ export class McpServer { if (updates.metadata !== undefined) registeredResourceTemplate.metadata = updates.metadata; if (updates.callback !== undefined) registeredResourceTemplate.readCallback = updates.callback; if (updates.enabled !== undefined) registeredResourceTemplate.enabled = updates.enabled; - this.sendResourceListChanged(); + void this.sendResourceListChanged(); } }; this._registeredResourceTemplates[name] = registeredResourceTemplate; @@ -738,7 +739,7 @@ export class McpServer { } if (updates.enabled !== undefined) registeredPrompt.enabled = updates.enabled; - this.sendPromptListChanged(); + void this.sendPromptListChanged(); } }; this._registeredPrompts[name] = registeredPrompt; @@ -821,13 +822,13 @@ export class McpServer { if (updates.annotations !== undefined) registeredTool.annotations = updates.annotations; if (updates._meta !== undefined) registeredTool._meta = updates._meta; if (updates.enabled !== undefined) registeredTool.enabled = updates.enabled; - this.sendToolListChanged(); + void this.sendToolListChanged(); } }; this._registeredTools[name] = registeredTool; this.setToolRequestHandlers(); - this.sendToolListChanged(); + void this.sendToolListChanged(); return registeredTool; } @@ -939,7 +940,7 @@ export class McpServer { ); this.setPromptRequestHandlers(); - this.sendPromptListChanged(); + void this.sendPromptListChanged(); return registeredPrompt; } @@ -973,28 +974,31 @@ export class McpServer { /** * Sends a resource list changed event to the client, if connected. */ - sendResourceListChanged() { - if (this.isConnected()) { - this.server.sendResourceListChanged(); + sendResourceListChanged(options?: NotificationOptions) { + if (!this.isConnected()) { + return; } + return this.server.sendResourceListChanged(options); } /** * Sends a tool list changed event to the client, if connected. */ - sendToolListChanged() { - if (this.isConnected()) { - this.server.sendToolListChanged(); + sendToolListChanged(options?: NotificationOptions) { + if (!this.isConnected()) { + return; } + return this.server.sendToolListChanged(options); } /** * Sends a prompt list changed event to the client, if connected. */ - sendPromptListChanged() { - if (this.isConnected()) { - this.server.sendPromptListChanged(); + sendPromptListChanged(options?: NotificationOptions) { + if (!this.isConnected()) { + return; } + return this.server.sendPromptListChanged(options); } } diff --git a/packages/server/src/server/server.ts b/packages/server/src/server/server.ts index f1a1851f4..a2a758719 100644 --- a/packages/server/src/server/server.ts +++ b/packages/server/src/server/server.ts @@ -660,17 +660,20 @@ export class Server extends Protocol { }); } - async sendResourceListChanged() { - return this.notification({ - method: 'notifications/resources/list_changed' - }); + async sendResourceListChanged(options?: NotificationOptions) { + return this.notification( + { + method: 'notifications/resources/list_changed' + }, + options + ); } - async sendToolListChanged() { - return this.notification({ method: 'notifications/tools/list_changed' }); + async sendToolListChanged(options?: NotificationOptions) { + return this.notification({ method: 'notifications/tools/list_changed' }, options); } - async sendPromptListChanged() { - return this.notification({ method: 'notifications/prompts/list_changed' }); + async sendPromptListChanged(options?: NotificationOptions) { + return this.notification({ method: 'notifications/prompts/list_changed' }, options); } } diff --git a/packages/server/test/server/streamableHttp.test.ts b/packages/server/test/server/streamableHttp.test.ts index aa8ede227..b63bac7f5 100644 --- a/packages/server/test/server/streamableHttp.test.ts +++ b/packages/server/test/server/streamableHttp.test.ts @@ -107,6 +107,13 @@ function parseSSEData(text: string): unknown { return JSON.parse(dataLine.slice(5).trim()); } +function parseSSEEvents(text: string): unknown[] { + return text + .split('\n\n') + .filter(event => event.includes('data:')) + .map(event => parseSSEData(event)); +} + function expectErrorResponse(data: unknown, expectedCode: number, expectedMessagePattern: RegExp): void { expect(data).toMatchObject({ jsonrpc: '2.0', @@ -472,6 +479,45 @@ describe('Zod v4', () => { expect(response.status).toBe(200); }); + + it('should send list changed notifications inline on the POST SSE stream when relatedRequestId is provided', async () => { + mcpServer.registerTool('refresh-tools', { description: 'Refresh tool list' }, async ctx => { + await mcpServer.sendToolListChanged({ relatedRequestId: ctx.mcpReq.id }); + return { content: [{ type: 'text', text: 'refreshed' }] }; + }); + + const initRequest = createRequest('POST', TEST_MESSAGES.initialize); + await transport.handleRequest(initRequest); + + const toolCallMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/call', + params: { + name: 'refresh-tools' + }, + id: 'call-1' + }; + + const request = createRequest('POST', toolCallMessage); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + + const events = parseSSEEvents(await response.text()); + + expect(events).toMatchObject([ + { + method: 'notifications/tools/list_changed' + }, + { + jsonrpc: '2.0', + id: 'call-1', + result: { + content: [{ type: 'text', text: 'refreshed' }] + } + } + ]); + }); }); describe('HTTPServerTransport - JSON Response Mode', () => { diff --git a/test/integration/test/server.test.ts b/test/integration/test/server.test.ts index 825af7ea4..8767f1d22 100644 --- a/test/integration/test/server.test.ts +++ b/test/integration/test/server.test.ts @@ -859,6 +859,59 @@ test('should forward notification options when using elicitation completion noti ); }); +test('should forward notification options for list changed notifications', async () => { + const server = new Server( + { + name: 'test server', + version: '1.0' + }, + { + capabilities: { + tools: {}, + resources: {}, + prompts: {} + } + } + ); + + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); + + const notificationSpy = vi.spyOn(server, 'notification'); + + await server.sendToolListChanged({ relatedRequestId: 11 }); + await server.sendResourceListChanged({ relatedRequestId: 12 }); + await server.sendPromptListChanged({ relatedRequestId: 13 }); + + expect(notificationSpy).toHaveBeenNthCalledWith( + 1, + { + method: 'notifications/tools/list_changed' + }, + expect.objectContaining({ relatedRequestId: 11 }) + ); + expect(notificationSpy).toHaveBeenNthCalledWith( + 2, + { + method: 'notifications/resources/list_changed' + }, + expect.objectContaining({ relatedRequestId: 12 }) + ); + expect(notificationSpy).toHaveBeenNthCalledWith( + 3, + { + method: 'notifications/prompts/list_changed' + }, + expect.objectContaining({ relatedRequestId: 13 }) + ); +}); + test('should create notifier that emits elicitation completion notification', async () => { const server = new Server( { diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 967435834..6769e83cf 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -86,6 +86,43 @@ describe('Zod v4', () => { ]); }); + test('should forward notification options for list changed helpers', async () => { + const mcpServer = new McpServer( + { + name: 'test server', + version: '1.0' + }, + { + capabilities: { + tools: {}, + resources: {}, + prompts: {} + } + } + ); + + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + const sendToolListChangedSpy = vi.spyOn(mcpServer.server, 'sendToolListChanged'); + const sendResourceListChangedSpy = vi.spyOn(mcpServer.server, 'sendResourceListChanged'); + const sendPromptListChangedSpy = vi.spyOn(mcpServer.server, 'sendPromptListChanged'); + + await expect(mcpServer.sendToolListChanged({ relatedRequestId: 21 })).resolves.toBeUndefined(); + await expect(mcpServer.sendResourceListChanged({ relatedRequestId: 22 })).resolves.toBeUndefined(); + await expect(mcpServer.sendPromptListChanged({ relatedRequestId: 23 })).resolves.toBeUndefined(); + + expect(sendToolListChangedSpy).toHaveBeenCalledWith(expect.objectContaining({ relatedRequestId: 21 })); + expect(sendResourceListChangedSpy).toHaveBeenCalledWith(expect.objectContaining({ relatedRequestId: 22 })); + expect(sendPromptListChangedSpy).toHaveBeenCalledWith(expect.objectContaining({ relatedRequestId: 23 })); + }); + /*** * Test: ctx.mcpReq.log convenience method */ From 987f009b318f6829029defdea90ea2860dd7b046 Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:55:22 +0530 Subject: [PATCH 2/2] chore: add server changeset for PR 1797 --- .changeset/short-ducks-move.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/short-ducks-move.md diff --git a/.changeset/short-ducks-move.md b/.changeset/short-ducks-move.md new file mode 100644 index 000000000..296d2f931 --- /dev/null +++ b/.changeset/short-ducks-move.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/server': patch +--- + +Allow `@modelcontextprotocol/server` list-changed helpers to forward notification options so stateless Streamable HTTP servers can send inline resource, tool, and prompt list updates on the active POST response.