Skip to content

docs: document tool list changed notifications#1857

Open
nielskaspers wants to merge 3 commits intomodelcontextprotocol:mainfrom
nielskaspers:docs/issue-1132-tool-list-changed-notification
Open

docs: document tool list changed notifications#1857
nielskaspers wants to merge 3 commits intomodelcontextprotocol:mainfrom
nielskaspers:docs/issue-1132-tool-list-changed-notification

Conversation

@nielskaspers
Copy link
Copy Markdown
Contributor

Summary

  • Add documentation for the tool list changed notification feature to the server guide
  • Include a type-checked example showing automatic and manual notification usage

Issue

Closes #1132

Changes

  • docs/server.md: Add "List changed notifications" subsection under Tools, covering automatic notification on registerTool() and manual sendToolListChanged(), with cross-reference to client-side listChanged option
  • examples/server/src/serverGuide.examples.ts: Add sendToolListChanged_basic example region

Testing

  • Example is type-checked as part of the existing examples build
  • Documentation follows the existing format with source= references and {@linkcode} tags

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
@nielskaspers nielskaspers requested a review from a team as a code owner April 6, 2026 06:16
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 6, 2026

⚠️ No Changeset found

Latest commit: d43894e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 6, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@1857

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@1857

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@1857

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@1857

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@1857

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@1857

commit: d43894e

@felixweinberger
Copy link
Copy Markdown
Contributor

@claude review

docs/server.md Outdated
Comment on lines +208 to +220
{@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]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

  1. Open docs/server.md and search for modelcontextprotocol.io/specification.
  2. All matches on lines 66, 333, 362, 521 use the pattern https://modelcontextprotocol.io/specification/latest/... — no subdomain, no date.
  3. The match on line 206 uses https://spec.modelcontextprotocol.io/specification/2025-06-18/... — both the spec. subdomain and the hardcoded date deviate from the established pattern.
  4. Replacing the URL at line 206 with https://modelcontextprotocol.io/specification/latest/server/tools/#list-changed-notification makes 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.

Copy link
Copy Markdown
Contributor

@felixweinberger felixweinberger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't need to do this "manually" - sendToolListChanged() already gets called during update and after registerTool already:

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>
@nielskaspers
Copy link
Copy Markdown
Contributor Author

Good call — you're right that the original example was misleading. registerTool, tool.remove(), tool.enable(), and tool.disable() all fire the notification automatically already, so showing a manual sendToolListChanged() after those operations teaches the wrong mental model.

I've pushed an update that:

  1. Clarifies the automatic behavior upfront (all four methods send the notification — no extra code needed)
  2. Reframes sendToolListChanged() as only needed for external state changes the SDK can't observe (e.g. feature flags, config reloads)
  3. Fixes the spec link to use the /latest/ pattern

Let me know if this framing works better!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Question: Tool list changed notification

2 participants