docs: document tool list changed notifications#1857
docs: document tool list changed notifications#1857nielskaspers wants to merge 3 commits intomodelcontextprotocol:mainfrom
Conversation
Add a "List changed notifications" subsection to the Tools section in the server guide, explaining how McpServer automatically notifies clients when tools are registered at runtime and how to send the notification manually via sendToolListChanged(). Include a type-checked example in serverGuide.examples.ts and a cross-reference to the client-side listChanged option. Closes modelcontextprotocol#1132
|
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
|
@claude review |
docs/server.md
Outdated
| {@linkcode @modelcontextprotocol/server!server/mcp.McpServer#registerTool | registerTool} sends this notification automatically when called after the client is already connected. To notify manually — for example, after removing a tool or toggling tool availability — call {@linkcode @modelcontextprotocol/server!server/mcp.McpServer#sendToolListChanged | sendToolListChanged}: | ||
|
|
||
| ```ts source="../examples/server/src/serverGuide.examples.ts#sendToolListChanged_basic" | ||
| // Automatic: registering a tool at runtime sends the notification | ||
| server.registerTool('new-tool', { description: 'A dynamically added tool' }, async () => ({ | ||
| content: [{ type: 'text', text: 'done' }] | ||
| })); | ||
|
|
||
| // Manual: notify clients explicitly (e.g. after removing a tool) | ||
| server.sendToolListChanged(); | ||
| ``` | ||
|
|
||
| > [!NOTE] |
There was a problem hiding this comment.
🟡 The documentation's manual sendToolListChanged() example uses "after removing a tool" as the motivating use case, but this is incorrect: tool.remove(), tool.disable(), and tool.enable() all automatically call sendToolListChanged() internally, making a manual call redundant. The correct use case for manual notification is when tool availability changes through external means the SDK cannot observe — such as a feature flag that changes what a handler returns without any SDK call being made.
Extended reasoning...
What the bug is: The new documentation section (docs/server.md lines 208-220) describes manual sendToolListChanged() as appropriate "after removing a tool or toggling tool availability". This is misleading because the SDK's own tool.remove(), tool.disable(), and tool.enable() methods — returned by registerTool() — already call sendToolListChanged() automatically every time they are invoked.
The specific code path: In packages/server/src/server/mcp.ts, the three control methods are implemented as thin wrappers over an internal update() call: disable: () => registeredTool.update({ enabled: false }), enable: () => registeredTool.update({ enabled: true }), and remove: () => registeredTool.update({ name: null }) (lines ~795-797). The update() method unconditionally calls this.sendToolListChanged() at line 828, so every invocation of any of these three methods already triggers the notification.
Why the existing code doesn't prevent the issue: The documentation is simply describing the wrong scenario. Calling sendToolListChanged() after tool.remove() is not an error — it's harmless (just a redundant extra notification) — but teaching it as the canonical example gives developers the wrong mental model of when manual notification is needed.
What the impact is: Developers reading this section will learn a subtly wrong model: that removing/disabling/enabling tools requires a manual follow-up call. More importantly, they will not learn the actual use case: external state changes the SDK has no visibility into, such as reading a feature flag and having a handler conditionally respond without ever calling remove()/disable()/enable().
How to fix it: Update the prose from "for example, after removing a tool or toggling tool availability" to describe the real use case — e.g., "for example, when an external condition such as a feature flag changes which tools are logically available, without calling remove(), disable(), or enable()". Update the comment in the code example from // Manual: notify clients explicitly (e.g. after removing a tool) to something like // Manual: notify clients when tool availability changes through external means and show a realistic scenario (e.g., a flag check).
Step-by-step proof: (1) Developer reads the new documentation and learns to write: const t = server.registerTool('my-tool', ...); t.remove(); server.sendToolListChanged();. (2) At runtime, t.remove() calls registeredTool.update({ name: null }), which on line 828 calls this.sendToolListChanged() — notification sent. (3) The immediately following server.sendToolListChanged() sends a second, redundant notification. (4) No harm done at runtime, but the developer has learned a false mental model. (5) Separately, the real need — changing which tools respond based on feature flags without calling SDK methods — is entirely undocumented.
docs/server.md
Outdated
|
|
||
| ### List changed notifications | ||
|
|
||
| When the set of available tools changes at runtime, the server should notify connected clients so they can refresh their tool list (see [List Changed Notification](https://spec.modelcontextprotocol.io/specification/2025-06-18/server/tools/#list-changed-notification) in the MCP specification). |
There was a problem hiding this comment.
🟡 The new spec link at line 206 uses a different URL format than all other spec links in this file: it uses the spec. subdomain and a pinned date (2025-06-18) instead of the modelcontextprotocol.io/specification/latest/ pattern used everywhere else (lines 66, 333, 362, 521). Replace it with https://modelcontextprotocol.io/specification/latest/server/tools/#list-changed-notification to stay consistent and avoid the link going stale.
Extended reasoning...
What the bug is and how it manifests
The newly added "List changed notifications" section links to the MCP specification using https://spec.modelcontextprotocol.io/specification/2025-06-18/server/tools/#list-changed-notification. This URL differs from every other spec link in the file in two ways: (1) it uses a spec. subdomain that appears nowhere else in the documentation, and (2) it pins a specific date (2025-06-18) rather than using /latest/.
The specific code path that triggers it
The inconsistency is on line 206 of docs/server.md, inside the new "### List changed notifications" subsection. The offending line is:
see [List Changed Notification](https://spec.modelcontextprotocol.io/specification/2025-06-18/server/tools/#list-changed-notification) in the MCP specification
Why existing code doesn't prevent it
This is a documentation-only change with no automated consistency check on external URL format. There is no linter or CI rule enforcing that all spec links follow the same subdomain/path pattern, so the inconsistency slipped through.
What the impact would be
Two practical problems arise: (1) The pinned date 2025-06-18 will become stale — if the spec restructures its pages or the fragment identifier changes, the link will 404 silently while /latest/ links continue to resolve correctly. (2) Readers who follow multiple links from this page will encounter two different domains, which could cause confusion about canonical spec sources.
How to fix it
Replace the URL with: https://modelcontextprotocol.io/specification/latest/server/tools/#list-changed-notification
This matches the pattern used by lines 66 (basic/lifecycle), 333 (server/utilities/logging), 362 (basic/utilities/progress), and 521 (basic/utilities/tasks).
Step-by-step proof
- Open
docs/server.mdand search formodelcontextprotocol.io/specification. - All matches on lines 66, 333, 362, 521 use the pattern
https://modelcontextprotocol.io/specification/latest/...— no subdomain, no date. - The match on line 206 uses
https://spec.modelcontextprotocol.io/specification/2025-06-18/...— both thespec.subdomain and the hardcoded date deviate from the established pattern. - Replacing the URL at line 206 with
https://modelcontextprotocol.io/specification/latest/server/tools/#list-changed-notificationmakes all five links consistent.
Note on duplication: A refutation flagged this as a duplicate of bug_003. Both describe the same issue at the same location; however the inconsistency is real and the fix is unambiguous regardless of tracking ID.
felixweinberger
left a comment
There was a problem hiding this comment.
Thanks for looking into this - I think the example is a bit misleading given we should be firing the tool list changed notification automatically on registerTool (so it shouldn't be necessary to manually sendToolListChanged), but keen to understand if that's not happening under certain circumstances.
docs/server.md
Outdated
|
|
||
| ### List changed notifications | ||
|
|
||
| When the set of available tools changes at runtime, the server should notify connected clients so they can refresh their tool list (see [List Changed Notification](https://spec.modelcontextprotocol.io/specification/2025-06-18/server/tools/#list-changed-notification) in the MCP specification). |
There was a problem hiding this comment.
Outdated spec link - can we update to use latest instead of 2025-06-18 for consistency?
| })); | ||
|
|
||
| // Manual: notify clients explicitly (e.g. after removing a tool) | ||
| server.sendToolListChanged(); |
There was a problem hiding this comment.
We shouldn't need to do this "manually" - sendToolListChanged() already gets called during update and after registerTool already:
- https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/server/src/server/mcp.ts#L828
- https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/server/src/server/mcp.ts#L834
Is that not working? If so we should probably fix that rather than have this manual workaround.
Address review feedback: registerTool, tool.remove(), tool.enable(), and tool.disable() already fire the notification automatically. Reserve sendToolListChanged() for external state changes the SDK cannot observe (e.g. feature flags). Also fix spec link to use /latest/ pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Good call — you're right that the original example was misleading. I've pushed an update that:
Let me know if this framing works better! |
Summary
Issue
Closes #1132
Changes
docs/server.md: Add "List changed notifications" subsection under Tools, covering automatic notification onregisterTool()and manualsendToolListChanged(), with cross-reference to client-sidelistChangedoptionexamples/server/src/serverGuide.examples.ts: AddsendToolListChanged_basicexample regionTesting
source=references and{@linkcode}tags