First-turn API call blocks until pending MCP tools/list times out
What version of Codex CLI is running?
codex-cli 0.124.0 (also reproduced on 0.123.0; symptoms reported as far back as 0.114.0 in #14470, #14627)
What subscription do you have?
Pro
Which model were you using?
gpt-5.5 with reasoning xhigh
What platform is your computer?
macOS 26.5, arm64
What terminal emulator and version are you using (if applicable)?
Terminal CLI session
What issue are you seeing?
When any configured MCP server stalls on tools/list (or completes initialize slowly), the first turn after launch or session resume sits idle for the full startup_timeout_sec (default 30s) before any model API call goes out. The TUI shows Working (Xs) for the whole window without saying what it is waiting on. Ctrl+C during the wait aborts the turn with no assistant output.
In the original session that triggered this report (codex-cli 0.123.0, 4 newly added MCPs in ~/.codex/config.toml), the wait stretched to ~340s and ~470s on consecutive retries before I gave up and closed the session. The 0.124.0 wait is bounded by the 30s timeout but the same code path stalls every first turn.
What steps can reproduce the bug?
Save this as tools_list_hang_mcp_server.py (zero deps, stdlib only):
import json, sys
for raw in sys.stdin:
line = raw.strip()
if not line:
continue
req = json.loads(line)
method = req.get("method")
if method == "initialize":
print(json.dumps({
"jsonrpc": "2.0",
"id": req["id"],
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {"listChanged": False}},
"serverInfo": {"name": "hang", "version": "0.1.0"},
},
}), flush=True)
# tools/list and everything else: no response
Then resume any prior session with this server attached and submit a prompt:
codex resume <session-id> \
-c 'mcp_servers.hang.command="python3"' \
-c 'mcp_servers.hang.args=["/abs/path/tools_list_hang_mcp_server.py"]'
> Reply with OK.
A fresh session (codex with the same -c overrides) reproduces it too.
What is the expected behavior?
The first turn dispatches the API call without waiting on tools/list from every server. A slow MCP's tools become available on the next turn once its startup completes or times out.
Additional information
Rollout for the resumed turn shows the gap concretely:
L547 task_started 2026-04-25T11:37:53.449Z
L548 user msg 2026-04-25T11:38:04.592Z (+11.1s)
L549 turn_context 2026-04-25T11:38:04.593Z
L550 user prompt 2026-04-25T11:38:20.234Z (+15.6s)
L551 user_message event
L552 token_count 2026-04-25T11:38:20.895Z (API call dispatched, +27.4s)
L553 agent_message 2026-04-25T11:38:25.580Z
A normal turn on the same session without the hanging MCP takes task_started to token_count under 700ms.
After the timeout fires the TUI shows:
⚠ MCP client for `hang` failed to start: MCP startup failed:
timed out awaiting tools/list after 30s
That message arrives 30 seconds after the user submitted, with Working shown the whole time.
Root cause is in codex-rs/codex-mcp/src/mcp_connection_manager.rs:
AsyncManagedClient::listed_tools()(around L630) awaitsself.client()for any server without astartup_snapshot, blocking on the full per-server startup future.McpConnectionManager::list_all_tools()(L925) iterates every client and awaits each in turn, so one stalled server gates the whole map.Session::run_turncallslist_all_tools()(codex-rs/core/src/session/turn.rs:190) before the API call.- The existing test
list_all_tools_blocks_while_client_is_pending_without_startup_snapshot(mcp_connection_manager_tests.rs:687) asserts the blocking behavior, so the fix is a contract change to that test rather than a one-line patch.
The codex_apps server already has a startup_snapshot cache via load_startup_cached_codex_apps_tools_snapshot, so it is not affected. User-defined MCPs always go through the blocking path on first turn.
Possibly the same root cause behind older reports that lacked a deterministic repro: #14470, #14627, #11015. Each describes a silent first-turn hang with MCP servers active but never produced a reliable trigger.
I have a working patch (skip clients with startup_complete == false in listed_tools(), fall back to existing startup_snapshot) plus updated tests. Happy to open a PR if invited.
3 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Looked at both. Neither is a dup. #19542 is on Windows in the sub-agent startup path. #19284 is the Cloudflare 403 on
codex_appsconnectors discovery, which only happens on long-lived ChatGPT-auth sessions;codex_appsalso has the startup-snapshot cache so it bypasses the blocking path here. Mine reproduces with one user-defined stdio MCP that never answerstools/list, on a fresh session.Adding a current Codex Desktop data point that appears to hit this same
list_all_tools/ pending MCP startup path.Environment:
app.version=0.136.0-alpha.2gpt-5.5, reasoning effortxhighclaude-context, configured as:Observed behavior:
claude-contexttools successfully.Most relevant log rows from
~/.codex/logs_2.sqlitefor the stuck thread:There were no subsequent
listed MCP server toolsrows for that thread/server before interrupt. The thread remainedactive_turn_status=InProgress; a later status-check prompt queued behind it and also could not progress. The interrupt then logged:For comparison, nearby successful threads had the expected pair:
Local health after restarting Docker:
claude-contextanswered an index-status check for the affected worktree: fully indexed, 505 files, 2546 chunks.claude-context-mcpchild processes were present under the Codex app-server parent, so the server is being launched locally.Local mitigation applied:
That may reduce the chance of hitting the bad path during cold Docker/Milvus startup, but it does not address the core behavior: once a server has
startup_complete=falseand no cached tool snapshot,list_all_toolscan still keep the first turn/subagent from dispatching any model request. The expected behavior would be to skip pending optional MCP servers for that turn, use ready/cached tool snapshots, and surface the MCP startup state separately instead of silently blocking the first model request.