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/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:
-
During initialization, advertise:
{
"capabilities": {
"tools": {
"listChanged": true
}
}
}
-
On the initial tools/list, return only start_daemon.
-
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.
-
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_search index 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:
This issue occurs earlier: Codex never refetches the changed list, so the new tools never enter the deferred index at all.
Environment
0.144.1tool_search_always_defer_mcp_toolsheadless-ida-mcp-stubWhat issue are you seeing?
Codex does not refresh an MCP server's tool registry after the server sends the standard
notifications/tools/list_changednotification.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. Afterstart_daemoncompletes, the stdio server:notifications/tools/list_changed;tools/listrequest.Codex calls
start_daemonsuccessfully, but never issues anothertools/listrequest. The deferredtool_searchsurface 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:
{ "capabilities": { "tools": { "listChanged": true } } }On the initial
tools/list, return onlystart_daemon.When
tools/callinvokesstart_daemon:notifications/tools/list_changed;Ask Codex to use one of the newly added tools, or inspect the MCP requests made by Codex.
Observed Codex request sequence:
The same server was verified end-to-end with:
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, andserver_health. Current sessions only see/callstart_daemon.Expected behavior
When Codex receives
notifications/tools/list_changed, it should:tools/list;tool_searchindex for that server;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. Inrmcp,ToolListChangedNotificationis dispatched to the application's notification handler;rmcpdoes not automatically invalidate or refetch the tool list. Codex's application layer appears to cache MCP tools and build the deferredtool_searchsurface 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:
tools/listare not exposed to Desktop threads.tool_searchranking.This issue occurs earlier: Codex never refetches the changed list, so the new tools never enter the deferred index at all.