Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,25 @@

If a handler throws instead of returning `isError`, the SDK catches the exception and converts it to `{ isError: true }` automatically — so an explicit try/catch is optional but gives you control over the error message. When `isError` is true, output schema validation is skipped.

### 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).

Check warning on line 206 in docs/server.md

View check run for this annotation

Claude / Claude Code Review

Inconsistent spec URL: wrong subdomain and pinned version

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.
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?

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.


{@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]

Check warning on line 220 in docs/server.md

View check run for this annotation

Claude / Claude Code Review

Misleading sendToolListChanged example: remove/disable already auto-notifies

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 ma
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.

> On the client side, use the {@linkcode @modelcontextprotocol/client!client/client.ClientOptions | listChanged} option to automatically re-fetch tool lists when this notification arrives — see [Automatic list-change tracking](./client.md#automatic-list-change-tracking) in the client guide.

## Resources

Resources expose read-only data — files, database schemas, configuration — that the host application can retrieve and attach as context for the model (see [Resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources) in the MCP overview). Unlike [tools](#tools), which the LLM invokes on its own, resources are application-controlled: the host decides which resources to fetch and how to present them.
Expand Down
14 changes: 14 additions & 0 deletions examples/server/src/serverGuide.examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ function registerTool_annotations(server: McpServer) {
//#endregion registerTool_annotations
}

/** Example: Notifying clients when the tool list changes at runtime. */
function sendToolListChanged_basic(server: McpServer) {
//#region 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();
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.

//#endregion sendToolListChanged_basic
}

/** Example: Registering a static resource at a fixed URI. */
function registerResource_static(server: McpServer) {
//#region registerResource_static
Expand Down Expand Up @@ -540,6 +553,7 @@ void registerTool_basic;
void registerTool_resourceLink;
void registerTool_errorHandling;
void registerTool_annotations;
void sendToolListChanged_basic;
void registerTool_logging;
void registerTool_progress;
void registerTool_sampling;
Expand Down
Loading