modelcontextprotocol / modelcontextprotocol/typescript-sdk

McpServer constructor capabilities.tools.listChanged is silently overridden by setToolRequestHandlers()

Open
#2,622 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug P3 ready for work v1
Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 15h
Merged PRs (30d)
4

Description

Summary

When constructing an McpServer with capabilities: { tools: { listChanged: false } }, the value passed to the constructor is silently overridden to true once any tool is registered. The listChanged: false setting never reaches the wire.

Steps to reproduce

const server = new McpServer(
  { name: 'my-server', version: '1.0.0' },
  { capabilities: { tools: { listChanged: false } } },
);

server.registerTool('my-tool', {
  description: 'A tool',
  inputSchema: { type: 'object', properties: {} },
}, async () => ({ content: [] }));

// Connect and send initialize — the response will contain:
// { capabilities: { tools: { listChanged: true } } }
// NOT { capabilities: { tools: { listChanged: false } } }

Root cause

McpServer.setToolRequestHandlers() (called on the first registerTool) unconditionally calls:

// dist/esm/server/mcp.js, lines 62-66
this.server.registerCapabilities({ tools: { listChanged: true } });
Server.registerCapabilities merges using mergeCapabilities which spreads additional over base:

result[k] = { ...baseValue, ...addValue };  // addValue wins

So { listChanged: false } (constructor) is overwritten by { listChanged: true } (SDK internal). The constructor option is ignored.

Workaround

After registering all tools but before connect():

server.server.registerCapabilities({ tools: { listChanged: false } });
await server.connect(transport);

This is fragile (accesses internal server property) and should not be necessary.

Expected behavior

The constructor option capabilities.tools.listChanged should take precedence over the hardcoded value in setToolRequestHandlers(). If explicitly set to false in the constructor, that value should be preserved. At minimum, the behavior should be documented.

SDK version

@modelcontextprotocol/sdk v1.29.0

Context

Discovered while building a stateless per-request server (sessionIdGenerator: undefined, enableJsonResponse: true) where notifications/tools/list_changed can never be delivered. Advertising listChanged: true is incorrect for this architecture. The behavior is harmless in that case, but the silent constructor override is a correctness issue for servers that genuinely need listChanged: false.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with setToolRequestHandlers() in dist/esm/server/mcp.js and the registerCapabilities merge behavior described in the issue. Trace how constructor capabilities reach the server, then verify that an explicit tools.listChanged: false remains false after registerTool(). Add or run coverage for the reported reproduction and confirm the initialize response preserves the constructor value.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.