A deploy that changes the tool set never reaches a running session: tools/list_changed is neither declared nor emitted #118

Open
opened 2026-08-25 07:45:01 +00:00 by jlxq0 · 2 comments
Owner

A matrix-mcp deploy that adds or removes a tool does not reach any session that is already running. Observed 2026-08-25 on v0.10.3, which added download_attachment to CHANNEL_TOOLS: the tool was absent from two separate live sessions while whoami returned a bot mxid and an unchanged device id, so the mount was healthy throughout and nothing surfaced. The workaround was to call the full /mcp mount over HTTP with the same bearer token; the alternative is a restart, which costs the session's context.

This nearly produced a false negative reported as "the release does not work". What prevented it was that the tool's absence had been recorded before the rollout, so absence afterwards carried no information.

What is actually missing

Two things, and they are separate:

  1. The capability is not declared. get_info builds ServerCapabilities::builder().enable_tools() for both mounts (src/mcp.rs:7009, src/mcp.rs:7020). rmcp's enable_tools() leaves tools.listChanged unset; enable_tool_list_changed() is what sets it to true (rmcp 1.7 model/capabilities.rs:454). A client is entitled to ignore a notification from a server that never advertised it.

  2. Nothing ever emits it. There is no notifications/tools/list_changed anywhere in src/ — grep returns the two enable_tools lines and nothing else. rmcp has the type (ServerNotification::ToolListChangedNotification, model.rs:1437), so this is a call that was never written rather than a gap in the SDK.

Proposed change

Declare listChanged: true on both mounts, and emit the notification from on_initialized — the point where a session registers, including the handshake replay a client performs after the pod is replaced. Emitting unconditionally on every registration is simpler than detecting that the set changed, and a re-list is cheap. That is the moment a session that survived a deploy is reachable again.

What is not established

Whether Claude Code re-lists on receiving it. The MCP spec says a client may re-fetch; it does not have to. If Claude Code caches the tool list for the session's lifetime regardless, declaring the capability and emitting the notification changes nothing observable, and this issue closes as "not fixable from our side" rather than as done. Establish that before building on it.

Acceptance

Not "the notification is emitted", and not "the capability is declared" — both are greppable and neither is the property.

A session running against /channel before a deploy, still running after it, calling a tool that did not exist when it connected and getting a result. The negative control is the same session before the change: it must fail, which it does today and which is the whole reason this issue exists.

If Claude Code turns out not to re-list, say so on this issue with what was observed, and close it. A recorded negative is worth more than the workaround being rediscovered next release.

Provenance

Found during #111's acceptance run against v0.10.3 (sha256:81e26f67…), and recorded in the mantis repo's AGENTS.md at df9761d as an operational pitfall. This issue is the server-side half.

A `matrix-mcp` deploy that adds or removes a tool does not reach any session that is already running. Observed 2026-08-25 on `v0.10.3`, which added `download_attachment` to `CHANNEL_TOOLS`: the tool was absent from two separate live sessions while `whoami` returned a bot mxid and an unchanged device id, so the mount was healthy throughout and nothing surfaced. The workaround was to call the full `/mcp` mount over HTTP with the same bearer token; the alternative is a restart, which costs the session's context. This nearly produced a false negative reported as "the release does not work". What prevented it was that the tool's absence had been recorded *before* the rollout, so absence afterwards carried no information. ## What is actually missing Two things, and they are separate: 1. **The capability is not declared.** `get_info` builds `ServerCapabilities::builder().enable_tools()` for both mounts (`src/mcp.rs:7009`, `src/mcp.rs:7020`). rmcp's `enable_tools()` leaves `tools.listChanged` unset; `enable_tool_list_changed()` is what sets it to `true` (rmcp 1.7 `model/capabilities.rs:454`). A client is entitled to ignore a notification from a server that never advertised it. 2. **Nothing ever emits it.** There is no `notifications/tools/list_changed` anywhere in `src/` — grep returns the two `enable_tools` lines and nothing else. rmcp has the type (`ServerNotification::ToolListChangedNotification`, `model.rs:1437`), so this is a call that was never written rather than a gap in the SDK. ## Proposed change Declare `listChanged: true` on both mounts, and emit the notification from `on_initialized` — the point where a session registers, including the handshake replay a client performs after the pod is replaced. Emitting unconditionally on every registration is simpler than detecting that the set changed, and a re-list is cheap. That is the moment a session that survived a deploy is reachable again. ## What is not established **Whether Claude Code re-lists on receiving it.** The MCP spec says a client *may* re-fetch; it does not have to. If Claude Code caches the tool list for the session's lifetime regardless, declaring the capability and emitting the notification changes nothing observable, and this issue closes as "not fixable from our side" rather than as done. Establish that before building on it. ## Acceptance Not "the notification is emitted", and not "the capability is declared" — both are greppable and neither is the property. A session running against `/channel` before a deploy, still running after it, calling a tool that did not exist when it connected and getting a result. The negative control is the same session before the change: it must fail, which it does today and which is the whole reason this issue exists. If Claude Code turns out not to re-list, say so on this issue with what was observed, and close it. A recorded negative is worth more than the workaround being rediscovered next release. ## Provenance Found during #111's acceptance run against `v0.10.3` (`sha256:81e26f67…`), and recorded in the mantis repo's `AGENTS.md` at `df9761d` as an operational pitfall. This issue is the server-side half.
Author
Owner

From the Claude Code MCP docs, "Dynamic tool updates":

Claude Code supports MCP list_changed notifications, allowing MCP servers to dynamically update their available tools, prompts, and resources without requiring you to disconnect and reconnect. When an MCP server sends a list_changed notification, Claude Code automatically refreshes the available capabilities from that server.

So the client half is not a may in practice: it refreshes. Since v2.1.214 a transient failure during that refresh keeps the previously discovered tools rather than replacing them with an empty list.

That raises the odds on the fix without settling it, because of a wrinkle specific to us. Two statements from the same page, both about the v2 runtime, which the fleet is on at 2.1.245:

  • Claude Code receives list_changed notifications from a server on the newer protocol revision over a stream it holds open.
  • Claude Code does not register a channel server that connects on the newer revision, because that revision cannot carry channel messages.

Read together, our /channel mount cannot be on the newer revision or it would not be a channel at all — so the held-open-stream path described there is not the path our notifications take.

What that does not mean is that there is no path. notifications/claude/channel demonstrably reaches a live session on the mount we have, so a server-to-client notification transport already works on the older revision. The question narrows to whether the client's refresh handler fires for notifications/tools/list_changed arriving that way. That is the empirical question the acceptance above already frames, and this is the reason a negative result would be plausible rather than evidence of a broken emit.

Rule out MCP_DISCOVERY_CACHE before trusting any result. Remote HTTP servers can serve a cached tool list from a previous session instead of connecting. It is off by default from v2.1.238 and the fleet is past that, so it should not be a factor — but a session showing cached … · 7 tools for channel in /mcp is a second, independent way a stale list survives a deploy, and it would be indistinguishable from the defect this issue describes. A negative result measured against a cached list means nothing.

Research by the mantis session.

From the Claude Code MCP docs, "Dynamic tool updates": > Claude Code supports MCP `list_changed` notifications, allowing MCP servers to dynamically update their available tools, prompts, and resources without requiring you to disconnect and reconnect. When an MCP server sends a `list_changed` notification, Claude Code automatically refreshes the available capabilities from that server. So the client half is not a *may* in practice: it refreshes. Since v2.1.214 a transient failure during that refresh keeps the previously discovered tools rather than replacing them with an empty list. **That raises the odds on the fix without settling it, because of a wrinkle specific to us.** Two statements from the same page, both about the v2 runtime, which the fleet is on at 2.1.245: - Claude Code receives `list_changed` notifications from a server on the **newer** protocol revision over a stream it holds open. - Claude Code **does not register a channel server** that connects on the newer revision, because that revision cannot carry channel messages. Read together, our `/channel` mount cannot be on the newer revision or it would not be a channel at all — so the held-open-stream path described there is not the path our notifications take. What that does *not* mean is that there is no path. `notifications/claude/channel` demonstrably reaches a live session on the mount we have, so a server-to-client notification transport already works on the older revision. The question narrows to whether the client's refresh handler fires for `notifications/tools/list_changed` arriving that way. That is the empirical question the acceptance above already frames, and this is the reason a negative result would be plausible rather than evidence of a broken emit. **Rule out `MCP_DISCOVERY_CACHE` before trusting any result.** Remote HTTP servers can serve a cached tool list from a previous session instead of connecting. It is off by default from v2.1.238 and the fleet is past that, so it should not be a factor — but a session showing `cached … · 7 tools` for `channel` in `/mcp` is a second, independent way a stale list survives a deploy, and it would be indistinguishable from the defect this issue describes. A negative result measured against a cached list means nothing. *Research by the mantis session.*
Author
Owner

Next up. Triaged 2026-08-31: this is one of three I would take before anything else in my repositories.

It cost real time on 2026-08-29 and 30, in three separate confusions that all looked like different faults.

Lucy's send_email schema showed no body_html against a pod that already carried it, and only a restart made the parameter visible. Mantis's m365 mount read two tools after a deploy and that was taken as the fix having failed, until somebody noticed every session predated the pod. A third session started after the account switch had a connector the others did not.

Each of those was diagnosed from scratch. The general shape is that a running session's tool list is a snapshot of when it connected, and nothing anywhere says so.

**Next up. Triaged 2026-08-31: this is one of three I would take before anything else in my repositories.** It cost real time on 2026-08-29 and 30, in three separate confusions that all looked like different faults. **Lucy's `send_email` schema showed no `body_html` against a pod that already carried it**, and only a restart made the parameter visible. **Mantis's m365 mount read two tools after a deploy** and that was taken as the fix having failed, until somebody noticed every session predated the pod. **A third session started after the account switch had a connector the others did not.** Each of those was diagnosed from scratch. **The general shape is that a running session's tool list is a snapshot of when it connected, and nothing anywhere says so.**
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jlxq0/matrix-mcp#118
No description provided.