MCP tools/list_changed notification does not invalidate deferred tool cache or refetch tools/list
Environment
- Codex CLI/core:
0.144.1 - Platform: Linux x86_64 (Linux Mint 22.3 / kernel 6.8)
- MCP transport: stdio
- Relevant enabled feature:
tool_search_always_defer_mcp_tools - Reproducer server: RainbowXie/headless-ida-mcp-server, stdio stub entry point
headless-ida-mcp-stub
What issue are you seeing?
Codex does not refresh an MCP server's tool registry after the server sends the standard notifications/tools/list_changed notification.
The MCP server initially exposes one cheap bootstrap tool, start_daemon, to avoid starting idalib and exposing roughly 89 tools when IDA is not being used. After start_daemon completes, the stdio server:
- fetches and caches the daemon's real tool list;
- sends
notifications/tools/list_changed; - returns the full tool list on every subsequent
tools/listrequest.
Codex calls start_daemon successfully, but never issues another tools/list request. The deferred tool_search surface remains based on the initial one-tool response, so none of the newly available tools can be discovered or called.
This also affects MCP servers with plugin systems: enabling or disabling a plugin changes the tool surface and emits tools/list_changed, but Codex continues using the stale cached surface.
Steps to reproduce
Use an MCP stdio server with this behavior:
- During initialization, advertise:
``json``
{
"capabilities": {
"tools": {
"listChanged": true
}
}
}
- On the initial
tools/list, return onlystart_daemon.
- When
tools/callinvokesstart_daemon:
- change the server's tool list so it now contains additional tools;
- send
notifications/tools/list_changed; - return success.
- Ask Codex to use one of the newly added tools, or inspect the MCP requests made by Codex.
Observed Codex request sequence:
initialize
tools/list # returns only start_daemon
tools/call start_daemon # server sends notifications/tools/list_changed
# no second tools/list request
The same server was verified end-to-end with:
- an in-memory MCP client with a warm daemon;
- an in-memory MCP client with a cold daemon;
- a real stdio subprocess using the Python MCP client;
- a direct live call against the configured server.
Those clients receive the notification, refetch tools/list, and see all 89 tools.
Historical Codex sessions using the same server design before the deferred/lazy tool surface successfully called tools such as decompile, lookup_funcs, and server_health. Current sessions only see/call start_daemon.
Expected behavior
When Codex receives notifications/tools/list_changed, it should:
- invalidate the cached tool list for that MCP server;
- refetch
tools/list; - rebuild or update the deferred
tool_searchindex for that server; - make newly added tools discoverable and callable;
- remove tools that are no longer returned.
Repeated notifications can be debounced or coalesced, but the resulting tool registry must eventually reflect the latest server response.
Additional investigation
The server-side notification path appears correct. The Python MCP clients refetch successfully, and the server advertises tools.listChanged=true.
Codex embeds rmcp. In rmcp, ToolListChangedNotification is dispatched to the application's notification handler; rmcp does not automatically invalidate or refetch the tool list. Codex's application layer appears to cache MCP tools and build the deferred tool_search surface from that cache. My current hypothesis is that the notification handler does not invalidate that cache or trigger rediscovery.
The MCP protocol only lets a server request a refetch; it cannot push a replacement tool list. Therefore, a server cannot preserve a small lazy initial surface or expose dynamic plugin tools reliably if the client ignores this notification.
Related but distinct issues:
- #19425: tools returned by the initial
tools/listare not exposed to Desktop threads. - #21503: an already indexed deferred tool can be missed by
tool_searchranking.
This issue occurs earlier: Codex never refetches the changed list, so the new tools never enter the deferred index at all.
5 Comments
Confirmed on Codex
0.144.6for macOS with a plugin-provided stdio MCP server.My reproduction was:
tools.listChanged: true.notifications/tools/list_changed.tools/listincluded the new tool, and a direct rawtools/callsucceeded.This affects more than lazy server startup. It prevents an agent from discovering a capability activated during a session and leaves removed or permission-dependent tools model-visible until the host refreshes. The same pattern applies to hosted MCP services, plugin systems, and tool surfaces that change with authentication or authorization.
The expected client behavior is exactly what this issue describes: invalidate only the affected server's catalog, re-run
tools/list, then rebuild the active/deferred tool index at the next safe model-turn boundary. Additions, removals, and same-name schema changes should all be applied; notifications can be debounced.For reference, Codex's
0.144.6on_tool_list_changedhandler currently only logs the event, whileManagedClient::listed_toolsreturns the startup-captured vector for ordinary MCP servers. That matches the behavior observed here.I am happy to submit a focused patch with regression coverage if a maintainer would like an external contribution for this issue.
Confirmed this persists in Codex CLI
0.147.0over stdio, withouttool_search_always_defer_mcp_tools. Afternotifications/tools/list_changed, Codex never refetchestools/list; long-lived sessions retain the stale tool schema across blue/green MCP rollovers.After an MCP server sends
notifications/tools/list_changed, a live Codex thread keeps the old tool list. A brand-new raw MCP client sees the new tools; the already-connected thread does not.Method:
tools/listoutside Codex.tool_search_always_defer_mcp_tools, turning it off does not fix this; the stale catalog happens without that flag too.Evidence: reproductions on 0.144.x and 0.147.0. Direct
tools/callon the live server succeeds, while the existing Codex thread still exposes the previous inventory.Independent community workaround; not an official OpenAI fix.
Adding a data point from the other transport: this also happens over Streamable HTTP against a remote server, and there it is worse than a stale live thread — the schema stays stale across sessions and across days.
What we saw
We removed one optional argument from a tool on a hosted MCP server. Clients that had registered the connector before that deploy kept sending the removed argument, and every such call was rejected by Pydantic validation before reaching the tool body.
codex-mcp-client/0.14xandopenai-mcp/1.0.0 (Codex).Restarting a thread did not help, because the stale copy is not held by the thread — it is whatever the connector captured when it was registered.
A second problem specific to remote servers
Even with the client-side handler fixed,
notifications/tools/list_changedcannot reach a stateless Streamable HTTP server's clients, because there is no server→client stream to deliver it on.A stateless server has no session, so
GET /mcpcannot be served, and the spec's backward-compatibility guidance says such a server SHOULD answer405 Method Not Allowed. That is exactly what ours does — and clients keep asking:So for horizontally-scaled remote servers (which is most hosted ones),
listChangedis structurally unavailable, not merely unhandled.Suggestion
Honoring the notification fixes stdio and stateful HTTP. For stateless HTTP, something not notification-based is needed as well — the cheapest being a re-fetch of
tools/listwhen a new session/thread starts, or a TTL on the cached catalog. Today a connector registered weeks ago can still be handing the model a schema the server has not accepted for weeks, with no path to convergence on either side.Happy to provide more detail on the HTTP-side reproduction if useful.
Confirmed on Codex CLI
0.149.1on Windows x64 with a local stdio MCP server using protocol revision2025-06-18.Sanitized reproduction:
capabilities.tools.listChanged: true.tools/listintentionally omits authorization-dependent tools.notifications/tools/list_changed.ToolListChangedNotificationandMCP server tool list changed.This also reproduced on
0.147.0. The result does not depend on deferred-only tool search: inspecting the active programmatic tool inventory directly remains stale. The MCP connection and authorization state remain valid; only the Codex model-facing catalog fails to converge.Expected behavior: invalidate the affected server's cached list, issue a fresh
tools/list, and publish the replacement catalog at a safe turn boundary. No project content, credentials, paths, session identifiers, or proprietary tool names are included in this report.