openai / openai/codex

MCP tool approval elicitation can pend forever with no timeout or turn-state signal (headless app-server clients hang silently); rmcp is_closed() misses self-terminated service tasks

Open
#39,149 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app-server bug mcp tool-calls
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

This issue was rewritten after the investigation concluded, so it stands alone as a single correct report; the earlier body and follow-up comments described mechanisms that turned out to be wrong.

Summary

An MCP tool approval elicitation can pend indefinitely: no timeout, no model-visible signal, and no turn-state indication that an approval is being awaited. For an app-server client that drives a headless/hidden agent with no approval surface, this presents as a tool call that hangs forever with no error, no log line, and no HTTP request ever leaving the process.

A second, adjacent bug found en route: rmcp 3.0.0's RunningService::is_closed() cannot observe a service task that terminated on its own, and codex's connection reconciliation reuses connections based on that flag.

Version / environment
  • codex-cli 0.147.0 (official release binary), Linux x86_64 (NixOS)
  • codex app-server driven over stdio by a third-party host daemon (Paseo); one app-server process per agent thread
  • Thread config: mcp_servers entry for a local streamable-HTTP MCP server, approval_policy=on-request, sandbox=workspace-write
  • Models: gpt-5.6-* family, model_info.tool_mode = code_mode_only
  • Realtime conversations (thread/realtime/start, version: "v3") in the affected flow
Symptom

Our daemon spawns an internal codex agent as a Live Voice host. This agent is hidden: it has no UI, and the client presents no approval surface for it (normal, user-visible agents do).

When that hidden agent issues an MCP tool call:

  • The call hangs forever (observed 801.8 s, until the user hung up). No error, no timeout.
  • No HTTP request ever reaches the MCP server — confirmed server-side and via netstat (no TCP connection from the app-server process).
  • Nothing is logged, and nothing in the turn state indicates that anything is being awaited. The model sees only an unresolved tool call.
  • On turn abort, the rollout records:
mcp_tool_call_end: result Err "user cancelled MCP tool call", duration { secs: 0, nanos: 0 }
Root cause

The call reaches maybe_request_mcp_tool_approval in core/src/mcp_tool_call.rs. Tools that lack auto-approving annotations raise an MCP tool approval elicitation. Our client surfaces those elicitations for ordinary agents, but the hidden voice host had no surface for them — so the elicitation pended forever.

The "user cancelled MCP tool call" / duration: 0s record above is verbatim the McpToolApprovalDecision::Cancel arm firing on turn abort. It is the epitaph of an unanswered approval, not evidence of a transport failure.

Read-annotated tools auto-approve. That is why the failure appeared to correlate with realtime/voice sessions and with the MCP transport for days: voice-host calls hit non-read tools while every control probe called read tools and passed.

This is resolved on our side by pre-approving the relevant tools for the hidden host ([mcp_servers.<server>.tools.<name>] approval_mode = "approve"; a per-thread config merge works for this).

What we'd like from codex

(a) A pending approval elicitation should be observable and bounded.

Any of these would have turned a multi-day diagnosis into a few minutes:

  • a bounded wait on the elicitation, surfacing a normal tool error on expiry;
  • a narratable "awaiting approval" state in the turn, so a client (and the model) can see why nothing is progressing;
  • at minimum, a log line when an approval elicitation is raised and when it is answered.

More generally: a tool call blocked on a request that the client may never be able to answer should fail fast rather than wait forever. We added a 60 s timeout on code-mode nested dispatch locally, and it cleanly converts the silent hang into a model-visible tool error.

(b) rmcp 3.0.0 RunningService::is_closed() misses self-terminated service tasks.

is_closed() is handle.is_none() || cancellation_token.is_cancelled(), so a service task that terminated on its own is still reported as open. Checking JoinHandle::is_finished() fixes it. This belongs in modelcontextprotocol/rust-sdk, but it matters here because codex's connection reconciliation reuses connections based on this flag, and a dead-but-"open" connection can be retained.

Regression coverage we have

In codex-rs/app-server/tests/suite/v2/:

  • realtime v3 handoff → delegated turn → nested code-mode MCP call
  • HTTP MCP server surviving a runtime refresh (config/mcpServer/reload) with a bounded-timeout call afterwards
  • terminated-service fail-fast (a call against a terminated service must error rather than hang)

Happy to open PRs for the nested-dispatch timeout, the rmcp is_closed() fix, and these tests.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in core/src/mcp_tool_call.rs at maybe_request_mcp_tool_approval and review how pending approvals are represented and cancelled. Then inspect the v2 app-server tests under codex-rs/app-server/tests/suite/v2/, including the nested-dispatch, runtime-refresh, and terminated-service cases. Done means pending approvals become observable or bounded, and the listed regression scenarios fail cleanly instead of hanging.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
api, backend, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.