Skip to content

fix: wait for MCP server startup before starting an agent turn - #453

Open
bgeisberger wants to merge 1 commit into
agentclientprotocol:mainfrom
bgeisberger:air/main
Open

fix: wait for MCP server startup before starting an agent turn#453
bgeisberger wants to merge 1 commit into
agentclientprotocol:mainfrom
bgeisberger:air/main

Conversation

@bgeisberger

Copy link
Copy Markdown

Summary

  • wait for the session's MCP servers to reach a terminal startup state before dispatching a turn
  • only gate prompts that actually run an agent turn; commands the adapter answers itself dispatch immediately
  • bound the wait with MCP_STARTUP_PROMPT_TIMEOUT_MS (default 30s) and skip it on later prompts once it expires
  • handle cancellation during the wait, where no turn exists yet to interrupt
  • add regression coverage for the gate, the local-command bypass, cancellation, and the timeout fallback

Root cause

session/new registers the session's pending MCP startup and publishes its status fire-and-forget, then returns. Nothing orders turn/start against that startup, so a prompt sent immediately after runs without the server's tools.

Measured against a stdio MCP server that delays initialize by 6s: turn/start goes out ~0.2s after session creation, about six seconds before the server reports ready, and the model has no access to the tool. Codex does not await MCP startup on the app-server path, so the adapter has to order it.

Earlier versions awaited MCP startup during session creation. That await was removed in #83 and #112 when the startup tracking was reworked, presumably for unrelated reasons.

The wait is bounded because it only settles once every requested server reports a status newer than the version snapshot taken at session start. If Codex ever reuses an already-running server without re-announcing it, no such status arrives. On timeout the turn starts without those tools rather than hanging.

Upstream

Codex's CLI path does await MCP startup before listing tools, tracked upstream as a first-turn stall (openai/codex#19556). The app-server path we drive does not, so this change adds the ordering. openai/codex#21318 and openai/codex#29321 propose building turns from only-ready MCP tools in the CLI as well, so this gate stays necessary rather than becoming redundant if they land.

Testing

  • npm run typecheck
  • npm test: 492 passed, 26 skipped
  • npm run bundle:all: all six targets compile, and codex-acp-x64-linux --version runs
  • verified the new tests fail without the gate (turn starts early, cancel returns end_turn) and that the local-command test fails if /status is misclassified as turn-starting

End-to-end against a real MCP server

Setup: an ACP session configured with one stdio MCP server that sleeps 6s before answering initialize, and exposes a single tool returning a fixed magic string. The prompt instructs the model to call that tool and echo the string back, or to answer NO_TOOL if no such tool is available to it. Three runs per case, and all timings are relative to session creation.

Branch Prompt sent turn/start MCP ready Model could use the tool
pre-fix immediately 0.19s 6.2s no (0/3)
pre-fix after 20s 20.2s 6.2s yes (3/3)
pre-fix, MCP delay removed immediately 0.19s 0.19s yes (3/3)
with fix immediately 6.21s 6.19s yes (3/3)

Row 1 is the bug: the turn starts six seconds before the server is ready and the model cannot see the tool. Rows 2 and 3 are controls showing the same setup works whenever the turn happens to start after readiness. Row 4 is the fix, with the turn now starting 15ms after readiness instead of six seconds before it.

Sessions created with MCP servers registered a pending startup entry and
kicked off the status publisher as fire-and-forget, so newSession and
loadSession returned before Codex had started the servers. A prompt sent
right after could start a turn without the session's MCP tools.

Retain the startup promise and await it in prompt() before dispatching a
turn. Only prompts that actually run an agent turn wait: commands the
adapter answers itself, and Codex requests that never run a turn, are
classified by CodexCommands.startsAgentTurn() and dispatch immediately.

The wait is bounded by MCP_STARTUP_PROMPT_TIMEOUT_MS (default 30s),
because the startup result only settles once every requested server
reports a status newer than the snapshot version, which Codex may never
send. On timeout the turn starts without those tools and later prompts
are not delayed again.

Cancel during the wait is handled explicitly: no turn exists yet, so
cancel() would otherwise have found nothing to interrupt.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant