From 8d3d0624c9795e7b4be4130832056d23634018f2 Mon Sep 17 00:00:00 2001 From: Serhii Lizunov <34252204+LizunovSergey@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:18:46 +0200 Subject: [PATCH 1/3] fix(server): preserve explicit tool listChanged capability --- src/server/index.ts | 5 ++++- src/server/mcp.ts | 2 +- test/server/mcp.test.ts | 47 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index cd75e5954a..8b39d10772 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -445,7 +445,10 @@ export class Server< return this._clientVersion; } - private getCapabilities(): ServerCapabilities { + /** + * Returns the current server capabilities. + */ + public getCapabilities(): ServerCapabilities { return this._capabilities; } diff --git a/src/server/mcp.ts b/src/server/mcp.ts index aa21af7bbf..cefd1cc3c6 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -189,7 +189,7 @@ export class McpServer { this.server.registerCapabilities({ tools: { - listChanged: true + listChanged: this.server.getCapabilities().tools?.listChanged ?? true } }); diff --git a/test/server/mcp.test.ts b/test/server/mcp.test.ts index 9a8db72117..8d33d4f98e 100644 --- a/test/server/mcp.test.ts +++ b/test/server/mcp.test.ts @@ -56,6 +56,53 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { expect(mcpServer.server).toBeDefined(); }); + test('should preserve an explicitly disabled tool listChanged capability', async () => { + const mcpServer = new McpServer( + { + name: 'test server', + version: '1.0' + }, + { + capabilities: { + tools: { listChanged: false } + } + } + ); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + mcpServer.registerTool('test', {}, async () => ({ + content: [{ type: 'text', text: 'Test response' }] + })); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + expect(client.getServerCapabilities()?.tools?.listChanged).toBe(false); + }); + + test('should enable the tool listChanged capability by default', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + mcpServer.registerTool('test', {}, async () => ({ + content: [{ type: 'text', text: 'Test response' }] + })); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + expect(client.getServerCapabilities()?.tools?.listChanged).toBe(true); + }); + /*** * Test: Notification Sending via Server */ From 8aa10857b687146635ae35f85bd54401260dbb7d Mon Sep 17 00:00:00 2001 From: Serhii Lizunov <34252204+LizunovSergey@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:36:37 +0200 Subject: [PATCH 2/3] chore: add changeset for listChanged fix --- .changeset/preserve-tool-list-changed.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/preserve-tool-list-changed.md diff --git a/.changeset/preserve-tool-list-changed.md b/.changeset/preserve-tool-list-changed.md new file mode 100644 index 0000000000..70eae53e80 --- /dev/null +++ b/.changeset/preserve-tool-list-changed.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/sdk': patch +--- + +Preserve an explicitly configured `tools.listChanged` capability when registering tools with `McpServer`. From c879eb4fc331b136020d1ebf2b64d71ab56c6a51 Mon Sep 17 00:00:00 2001 From: Serhii Lizunov <34252204+LizunovSergey@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:26:27 +0100 Subject: [PATCH 3/3] fix(server): skip tool list_changed notifications when listChanged is false --- .changeset/preserve-tool-list-changed.md | 2 +- src/server/mcp.ts | 4 +-- test/server/mcp.test.ts | 43 ++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/.changeset/preserve-tool-list-changed.md b/.changeset/preserve-tool-list-changed.md index 70eae53e80..fe9c54a2f2 100644 --- a/.changeset/preserve-tool-list-changed.md +++ b/.changeset/preserve-tool-list-changed.md @@ -2,4 +2,4 @@ '@modelcontextprotocol/sdk': patch --- -Preserve an explicitly configured `tools.listChanged` capability when registering tools with `McpServer`. +Preserve an explicitly configured `tools.listChanged` capability when registering tools with `McpServer`. When it is `false`, `McpServer` no longer sends `notifications/tools/list_changed`. diff --git a/src/server/mcp.ts b/src/server/mcp.ts index cefd1cc3c6..766bef57b1 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -1261,10 +1261,10 @@ export class McpServer { } /** - * Sends a tool list changed event to the client, if connected. + * Sends a tool list changed event to the client, if connected and `tools.listChanged` is not `false`. */ sendToolListChanged() { - if (this.isConnected()) { + if (this.isConnected() && this.server.getCapabilities().tools?.listChanged !== false) { this.server.sendToolListChanged(); } } diff --git a/test/server/mcp.test.ts b/test/server/mcp.test.ts index 8d33d4f98e..889ddc93ad 100644 --- a/test/server/mcp.test.ts +++ b/test/server/mcp.test.ts @@ -103,6 +103,49 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { expect(client.getServerCapabilities()?.tools?.listChanged).toBe(true); }); + test('should suppress sendToolListChanged notifications when tools.listChanged is explicitly false', async () => { + const mcpServer = new McpServer( + { + name: 'test server', + version: '1.0' + }, + { + capabilities: { + tools: { listChanged: false } + } + } + ); + const notifications: Notification[] = []; + const client = new Client({ + name: 'test client', + version: '1.0' + }); + client.fallbackNotificationHandler = async notification => { + notifications.push(notification); + }; + + const tool = mcpServer.registerTool('test', {}, async () => ({ + content: [{ type: 'text', text: 'Test response' }] + })); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + expect(client.getServerCapabilities()?.tools?.listChanged).toBe(false); + + mcpServer.sendToolListChanged(); + mcpServer.registerTool('test2', {}, async () => ({ + content: [{ type: 'text', text: 'Test response' }] + })); + tool.update({ description: 'Updated' }); + tool.remove(); + + // Yield event loop to let any notification fly + await new Promise(process.nextTick); + + expect(notifications).toHaveLength(0); + }); + /*** * Test: Notification Sending via Server */