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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗