bug: tools/list_changed after a policy reload sometimes never reaches Claude Code #53

Open
opened 2026-10-08 10:53:06 +02:00 by pti · 0 comments
Owner

After a policy reload, a connected Claude Code session usually picks up the new tool list without a reconnect, but not always. One approval out of four on 2026-10-08 never showed up until /mcp reconnect wraptool.

What happened

One Claude Code session, connected over sse (/sse?cwd=…). Each capability approval was applied to the config, and the file watcher reloaded it:

Approval New tool(s) Seen by Claude Code without reconnect
req_ff9b721b fj_issue_create yes
req_c6eccd3e, req_904e18eb, req_b221502c fj_issue_search, fj_issue_view, fj_issue_edit yes
req_abe9d8f2 fj_issue_comment no

For the last one the server side was fine: the tool was registered, wraptool_discover listed it, and a direct tools/list over a fresh MCP session returned it. Claude Code's tool list stayed unchanged until /mcp reconnect wraptool, after which the tool appeared at once.

Suspects

  1. Silent drop in mcp-go. sendNotificationCore (mcp-go v0.54.0, server/session.go) does a non-blocking send to the session's notification channel (buffer 100 on the SSE transport) and drops the notification when it is full. The only trace is an OnError hook, and wraptool registers none, so a drop is invisible.
  2. Burst size per reload. registrar.reload calls DeleteTools once and then AddTool once per tool (registerSource), and each call emits its own notifications/tools/list_changed. A reload therefore sends one notification per registered tool, plus one. Several reloads in a row, a large policy, or a stalled SSE writer could fill the buffer.
  3. A stale or replaced SSE session. If Claude Code's SSE stream was dropped and re-opened, notifications may go to a session nobody reads.
  4. The client side. Claude Code may have received the notification and not acted on it, for example by debouncing a burst. Rule this out only after 1–3.

To investigate

  • Register an OnError hook that logs ErrNotificationChannelBlocked with the session ID, so a drop shows up in the server log.
  • Log how many notifications each reload sends, and per session whether the send succeeded.
  • Replace the per-tool AddTool loop with one AddTools call (one notification per batch), and check whether DeleteTools + AddTools can be made a single notification.
  • Reproduce: one long-lived SSE client, approve several capabilities in a row, and count list_changed received against those sent. Repeat over Streamable HTTP (/mcp).
  • Check the server log around the req_abe9d8f2 reload for errors or session churn.

Workaround

/mcp reconnect wraptool in Claude Code. Agents can confirm the server's real state with wraptool_discover.

#50 (tool search; its hot-reload question).

🤖 Generated with Claude Code

After a policy reload, a connected Claude Code session usually picks up the new tool list without a reconnect, but not always. One approval out of four on 2026-10-08 never showed up until `/mcp reconnect wraptool`. ## What happened One Claude Code session, connected over `sse` (`/sse?cwd=…`). Each capability approval was applied to the config, and the file watcher reloaded it: | Approval | New tool(s) | Seen by Claude Code without reconnect | |---|---|---| | `req_ff9b721b` | `fj_issue_create` | yes | | `req_c6eccd3e`, `req_904e18eb`, `req_b221502c` | `fj_issue_search`, `fj_issue_view`, `fj_issue_edit` | yes | | `req_abe9d8f2` | `fj_issue_comment` | **no** | For the last one the server side was fine: the tool was registered, `wraptool_discover` listed it, and a direct `tools/list` over a fresh MCP session returned it. Claude Code's tool list stayed unchanged until `/mcp reconnect wraptool`, after which the tool appeared at once. ## Suspects 1. **Silent drop in mcp-go.** `sendNotificationCore` (mcp-go v0.54.0, `server/session.go`) does a non-blocking send to the session's notification channel (buffer 100 on the SSE transport) and drops the notification when it is full. The only trace is an `OnError` hook, and wraptool registers none, so a drop is invisible. 2. **Burst size per reload.** `registrar.reload` calls `DeleteTools` once and then `AddTool` once per tool (`registerSource`), and each call emits its own `notifications/tools/list_changed`. A reload therefore sends one notification per registered tool, plus one. Several reloads in a row, a large policy, or a stalled SSE writer could fill the buffer. 3. **A stale or replaced SSE session.** If Claude Code's SSE stream was dropped and re-opened, notifications may go to a session nobody reads. 4. **The client side.** Claude Code may have received the notification and not acted on it, for example by debouncing a burst. Rule this out only after 1–3. ## To investigate - Register an `OnError` hook that logs `ErrNotificationChannelBlocked` with the session ID, so a drop shows up in the server log. - Log how many notifications each reload sends, and per session whether the send succeeded. - Replace the per-tool `AddTool` loop with one `AddTools` call (one notification per batch), and check whether `DeleteTools` + `AddTools` can be made a single notification. - Reproduce: one long-lived SSE client, approve several capabilities in a row, and count `list_changed` received against those sent. Repeat over Streamable HTTP (`/mcp`). - Check the server log around the `req_abe9d8f2` reload for errors or session churn. ## Workaround `/mcp reconnect wraptool` in Claude Code. Agents can confirm the server's real state with `wraptool_discover`. ## Related #50 (tool search; its hot-reload question). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No labels
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
pti/wraptool#53
No description provided.