From 229bb158984ae7fb7c765b24eea025904bdc500d Mon Sep 17 00:00:00 2001 From: Zelys Date: Mon, 20 Apr 2026 14:07:57 -0500 Subject: [PATCH 1/2] fix(server): respect explicit listChanged: false in McpServer capability registration Backport of #1513 to v1.x. The three lazy-init methods in McpServer (setToolRequestHandlers, setResourceRequestHandlers, setPromptRequestHandlers) unconditionally registered listChanged: true when called, overwriting any explicit listChanged: false passed in capabilities at construction time. Fixed by reading the current capability value via Server.getCapabilities() and applying it with nullish coalescing so that undefined still defaults to true (backwards compatible), but explicit false is preserved. Also made Server.getCapabilities() public to allow McpServer to read it. Fixes #1819 --- .changeset/fix-listchanged-backport.md | 5 ++ src/server/index.ts | 5 +- src/server/mcp.ts | 6 +- test/server/mcp.test.ts | 105 +++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-listchanged-backport.md diff --git a/.changeset/fix-listchanged-backport.md b/.changeset/fix-listchanged-backport.md new file mode 100644 index 0000000000..4a00789054 --- /dev/null +++ b/.changeset/fix-listchanged-backport.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/sdk': patch +--- + +Respected explicit `listChanged: false` capability settings in `McpServer`. Previously, registering a tool, resource, or prompt would unconditionally overwrite `listChanged` to `true`, ignoring any explicit `false` set at construction time. diff --git a/src/server/index.ts b/src/server/index.ts index 531a559dd5..97e2254264 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -464,7 +464,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 9fe0ed549c..cf46173285 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -131,7 +131,7 @@ export class McpServer { this.server.registerCapabilities({ tools: { - listChanged: true + listChanged: this.server.getCapabilities().tools?.listChanged ?? true } }); @@ -493,7 +493,7 @@ export class McpServer { this.server.registerCapabilities({ resources: { - listChanged: true + listChanged: this.server.getCapabilities().resources?.listChanged ?? true } }); @@ -573,7 +573,7 @@ export class McpServer { this.server.registerCapabilities({ prompts: { - listChanged: true + listChanged: this.server.getCapabilities().prompts?.listChanged ?? true } }); diff --git a/test/server/mcp.test.ts b/test/server/mcp.test.ts index 575d6a300e..dbe9a74d23 100644 --- a/test/server/mcp.test.ts +++ b/test/server/mcp.test.ts @@ -252,6 +252,111 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { expect(capabilities?.extensions).toBeDefined(); expect(capabilities?.extensions?.['io.modelcontextprotocol/test-extension']).toEqual({ streaming: true }); }); + + /*** + * Test: listChanged capability should default to true when not specified + */ + test('should default tools.listChanged to true when not explicitly set', 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' }] + })); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + const capabilities = client.getServerCapabilities(); + expect(capabilities?.tools?.listChanged).toBe(true); + }); + + /*** + * Test: listChanged capability should respect explicit false setting + */ + test('should respect tools.listChanged: false when explicitly set', 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' }] + })); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + const capabilities = client.getServerCapabilities(); + expect(capabilities?.tools?.listChanged).toBe(false); + }); + + /*** + * Test: resources.listChanged should respect explicit false setting + */ + test('should respect resources.listChanged: false when explicitly set', async () => { + const mcpServer = new McpServer( + { name: 'test server', version: '1.0' }, + { capabilities: { resources: { listChanged: false } } } + ); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + mcpServer.registerResource( + 'test', + 'test://resource', + {}, + async () => ({ + contents: [{ uri: 'test://resource', text: 'Test', mimeType: 'text/plain' }] + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + const capabilities = client.getServerCapabilities(); + expect(capabilities?.resources?.listChanged).toBe(false); + }); + + /*** + * Test: prompts.listChanged should respect explicit false setting + */ + test('should respect prompts.listChanged: false when explicitly set', async () => { + const mcpServer = new McpServer( + { name: 'test server', version: '1.0' }, + { capabilities: { prompts: { listChanged: false } } } + ); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + mcpServer.registerPrompt( + 'test-prompt', + {}, + async () => ({ + messages: [{ role: 'assistant', content: { type: 'text', text: 'Test' } }] + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + const capabilities = client.getServerCapabilities(); + expect(capabilities?.prompts?.listChanged).toBe(false); + }); }); describe('ResourceTemplate', () => { From 9bf58310cea3a18248e554a9709796346ef25e02 Mon Sep 17 00:00:00 2001 From: Zelys Date: Mon, 20 Apr 2026 14:23:04 -0500 Subject: [PATCH 2/2] chore: fix Prettier formatting in mcp.test.ts --- test/server/mcp.test.ts | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/test/server/mcp.test.ts b/test/server/mcp.test.ts index dbe9a74d23..5c5cb3182c 100644 --- a/test/server/mcp.test.ts +++ b/test/server/mcp.test.ts @@ -281,10 +281,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { * Test: listChanged capability should respect explicit false setting */ test('should respect tools.listChanged: false when explicitly set', async () => { - const mcpServer = new McpServer( - { name: 'test server', version: '1.0' }, - { capabilities: { tools: { listChanged: false } } } - ); + const mcpServer = new McpServer({ name: 'test server', version: '1.0' }, { capabilities: { tools: { listChanged: false } } }); const client = new Client({ name: 'test client', version: '1.0' @@ -314,14 +311,9 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { version: '1.0' }); - mcpServer.registerResource( - 'test', - 'test://resource', - {}, - async () => ({ - contents: [{ uri: 'test://resource', text: 'Test', mimeType: 'text/plain' }] - }) - ); + mcpServer.registerResource('test', 'test://resource', {}, async () => ({ + contents: [{ uri: 'test://resource', text: 'Test', mimeType: 'text/plain' }] + })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); @@ -334,22 +326,15 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { * Test: prompts.listChanged should respect explicit false setting */ test('should respect prompts.listChanged: false when explicitly set', async () => { - const mcpServer = new McpServer( - { name: 'test server', version: '1.0' }, - { capabilities: { prompts: { listChanged: false } } } - ); + const mcpServer = new McpServer({ name: 'test server', version: '1.0' }, { capabilities: { prompts: { listChanged: false } } }); const client = new Client({ name: 'test client', version: '1.0' }); - mcpServer.registerPrompt( - 'test-prompt', - {}, - async () => ({ - messages: [{ role: 'assistant', content: { type: 'text', text: 'Test' } }] - }) - ); + mcpServer.registerPrompt('test-prompt', {}, async () => ({ + messages: [{ role: 'assistant', content: { type: 'text', text: 'Test' } }] + })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);