MCP tools/list_changed notification does not invalidate deferred tool cache or refetch tools/list

Open 💬 5 comments Opened Jul 15, 2026 by RainbowXie

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:

  1. fetches and caches the daemon's real tool list;
  2. sends notifications/tools/list_changed;
  3. returns the full tool list on every subsequent tools/list request.

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:

  1. During initialization, advertise:

``json
{
"capabilities": {
"tools": {
"listChanged": true
}
}
}
``

  1. On the initial tools/list, return only start_daemon.
  1. When tools/call invokes start_daemon:
  • change the server's tool list so it now contains additional tools;
  • send notifications/tools/list_changed;
  • return success.
  1. 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:

  1. invalidate the cached tool list for that MCP server;
  2. refetch tools/list;
  3. rebuild or update the deferred tool_search index for that server;
  4. make newly added tools discoverable and callable;
  5. 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/list are not exposed to Desktop threads.
  • #21503: an already indexed deferred tool can be missed by tool_search ranking.

This issue occurs earlier: Codex never refetches the changed list, so the new tools never enter the deferred index at all.

View original on GitHub ↗

5 Comments

joshuajbouw · 1 month ago

Confirmed on Codex 0.144.6 for macOS with a plugin-provided stdio MCP server.

My reproduction was:

  1. The server initialized with tools.listChanged: true.
  2. Its runtime tool surface changed and it sent notifications/tools/list_changed.
  3. A fresh raw MCP connection's tools/list included the new tool, and a direct raw tools/call succeeded.
  4. The already-connected Codex thread continued exposing its previous tool inventory and could not discover the new tool.
  5. Calling an already-known tool returned newly updated server content, confirming that the live runtime and connection had changed while only Codex's model-facing tool catalog remained stale.

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.6 on_tool_list_changed handler currently only logs the event, while ManagedClient::listed_tools returns 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.

aoaoaoaoaoaoaoaoaoaoaoaoaoaoaoaoaoaoaoa · 10 days ago

Confirmed this persists in Codex CLI 0.147.0 over stdio, without tool_search_always_defer_mcp_tools. After notifications/tools/list_changed, Codex never refetches tools/list; long-lived sessions retain the stale tool schema across blue/green MCP rollovers.

naipi11 · 9 days ago

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:

  1. After the server's tool surface changes, start a new Codex thread (or fully restart CLI/Desktop). Do not expect the current turn to pick up the new tools.
  2. Confirm the server itself is fine with a fresh tools/list outside Codex.
  3. If you enabled tool_search_always_defer_mcp_tools, turning it off does not fix this; the stale catalog happens without that flag too.
  4. Call only tools that the current thread already listed.

Evidence: reproductions on 0.144.x and 0.147.0. Direct tools/call on the live server succeeds, while the existing Codex thread still exposes the previous inventory.

Independent community workaround; not an official OpenAI fix.

y-inoue3 · 4 days ago

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.

  • Duration: 11 days, ending only when we made the server accept-and-ignore the old argument again.
  • Rate: 37 rejections vs 47 successes on the last day alone (44% of calls to that tool), roughly 20–25/day sustained.
  • Clients: codex-mcp-client/0.14x and openai-mcp/1.0.0 (Codex).
  • The users could not self-diagnose: the tool description already said the argument did not exist, but the schema their host handed the model still said it did.

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_changed cannot 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 /mcp cannot be served, and the spec's backward-compatibility guidance says such a server SHOULD answer 405 Method Not Allowed. That is exactly what ours does — and clients keep asking:

GET /mcp  ->  405   259 requests over 7 days, all from codex-mcp-client

So for horizontally-scaled remote servers (which is most hosted ones), listChanged is 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/list when 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.

tcorkran · 3 days ago

Confirmed on Codex CLI 0.149.1 on Windows x64 with a local stdio MCP server using protocol revision 2025-06-18.

Sanitized reproduction:

  1. The server initializes with capabilities.tools.listChanged: true.
  2. Its initial tools/list intentionally omits authorization-dependent tools.
  3. A tool call observes a valid out-of-band authorization transition.
  4. The server's available tool set changes and it emits notifications/tools/list_changed.
  5. Codex logs both receipt of ToolListChangedNotification and MCP server tool list changed.
  6. The model-visible tool inventory remains the startup catalog in the same turn and in subsequent turns; the newly available tools are absent.
  7. No newly authorized tool can be invoked because it never enters the active catalog.

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.