[BUG] codex exec: MCP tools from `tools/list` are not injected into the model's tool set

Open 💬 3 comments Opened Aug 15, 2026 by JCY2023
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Environment

  • codex-cli version: codex-cli 0.147.0-alpha.6.6(alpha channel)
  • OS: Windows 11 (native, x64)
  • MCP server type: streamable HTTP (ASP.NET / IIS), responses encoded as SSE (Content-Type: text/event-stream), stateless (no Mcp-Session-Id header returned)
  • Server side verified healthy: direct curl initialize / notifications/initialized / tools/list returns 200/202/200 with the full tool list

config.toml (minimal repro)

[mcp_servers.example-erp]
enabled = true
url = "http://<mcp-host>/mcp"

[mcp_servers.example-erp.http_headers]
Authorization = "Bearer <token>"

Steps to reproduce

  1. Configure a streamable HTTP MCP server as above (server returns tools via tools/list).
  2. Run:

``
codex exec --json --skip-git-repo-check --sandbox workspace-write -C <temp-dir> -m <model> -c notify=[] "Call the MCP tool <tool> and return the result."
``

  1. Observe the agent's available tool set.

Expected behavior

Tools returned by tools/list are registered and callable by the model — as in Codex desktop (ChatGPT app) with the same config, where the same tools are visible and usable.

Actual behavior

The model reports the tools do not exist (e.g. "no erp_describe / erp_query tool in this session"). The MCP connection itself works fine:

  • codex sends initializenotifications/initializedtools/list and receives 200 with all tools (verified by a local reverse proxy capturing the actual HTTP exchange).
  • list_mcp_resources with server=<id> reaches the server and returns -32601: Method 'resources/list' is not available — proving the connection and authentication are working (a wrong token would have produced 401).
  • The model's tool set only contains built-in tools (shell_command, list_mcp_resources, ...); no tools from tools/list appear.

Config variants tried (none changed the behavior)

  • http_headers static Authorization (original)
  • bearer_token_env_var instead of the static header
  • explicit enabled_tools allowlist listing all server tools
  • -c features.tool_call_mcp_elicitation=false
  • approval_policy = { granular = { sandbox_approval=true, rules=true, mcp_elicitations=true, request_permissions=true, skill_approval=true } }
  • --sandbox read-only vs workspace-write; with/without -C and --skip-git-repo-check; temp dir vs trusted project dir
  • direct connection vs local reverse proxy

Diagnostics already performed

  • codex mcp list / codex mcp get <id> / codex doctor: server shown as enabled, transport streamable_http, auth "Bearer token", doctor reports MCP servers healthy (0 disabled, 0 failed).
  • RUST_LOG=codex_core=trace,reqwest=trace: shows the HTTP connection to the MCP host and the completed handshake, but no tool registration from the tools/list response.
  • Feature flags in this build: tool_call_mcp_elicitation = stable/true, mcp_2026_07_28 = under development/false, non_prefixed_mcp_tool_names = under development/false.

Impact

This blocks headless/non-interactive codex exec usage with streamable HTTP MCP servers (agent cannot use any MCP tool), while interactive clients (desktop) work with the identical configuration.

Version

codex --versioncodex-cli 0.147.0-alpha.6.6

View original on GitHub ↗

3 Comments

github-actions[bot] contributor · 13 days ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #37567

Powered by Codex Action

TarjinderSingh · 12 days ago

I have a possibly related macOS A/B reproduction, with one important difference: the MCP tool is injected and the call starts, but execution then fails with an empty error.

  • ChatGPT Desktop: 26.810.50856
  • Bundled Codex CLI 0.148.0-alpha.9: tool starts, then fails with Error calling tool '<tool>': and no error text
  • Standalone Codex CLI 0.147.0: the identical tool call completes
  • Transport: authenticated Streamable HTTP on localhost
  • Both tests used the same shell, inherited token, configuration, server, sandbox, model, and prompt

An authenticated MCP initialize also succeeds independently. This suggests a version-specific failure after tool registration rather than the tool-injection failure described in this issue.

I filed the complete sanitized reproduction at: https://github.com/openai/codex/issues/38794

jdcodes1 · 10 days ago

Traced this on main @ 1f41cc5d92, and there's a mechanism that fits every one of your observations — including why your RUST_LOG capture showed a healthy handshake but no registration.

Optional MCP servers get a 1-second startup grace before being silently omitted from the model's tool set. When the turn builds its tool binding, any non-required server whose startup hasn't completed is given OPTIONAL_MCP_STARTUP_GRACE — hardcoded to 1s — and then dropped from the catalog with only a trace-level log:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/codex-mcp/src/connection_manager/tool_catalog.rs#L175-L225

(const OPTIONAL_MCP_STARTUP_GRACE: Duration = Duration::from_secs(1) at #L35; the omission logs "omitting pending optional MCP server".) User-configured servers are "optional" here — required_servers is a separate turn-level concept — so nothing forces the wait.

Why codex exec fails while Desktop works with identical config: exec is one-shot — its only sampling request snapshots the binding immediately at session start, so a streamable-HTTP server doing initialize → initialized → tools/list over a real network on a cold start loses the 1-second race essentially every time. The wire exchange still completes (your reverse-proxy capture) — just after the binding was already taken, and exec never builds another one. Desktop sessions are long-lived: by the second turn (or the next MCP refresh) startup is complete and the tools appear. list_mcp_resources working is also consistent: that goes through the live connection rather than the pre-snapshotted tool catalog.

Why your trace didn't show it: the omission message is logged under the codex_mcp target at TRACE level; your filter RUST_LOG=codex_core=trace,reqwest=trace doesn't include it. A directly actionable next step that would confirm this end-to-end on your setup:

RUST_LOG=codex_mcp=trace codex exec ... 2> mcp-trace.log
# then look for: "omitting pending optional MCP server"

If the line appears, this is confirmed. (One alternative worth ruling out while you're in there: with a stateless server that returns no Mcp-Session-Id, if startup_complete never flips — e.g. the client blocks establishing the follow-up SSE stream — you'd see the same omission on every snapshot; the trace will show whether startup eventually completes or hangs.)

Fix outline:

  1. For codex exec (single-turn, cold cache), treat configured MCP servers as required for the first binding — or scale the grace with an explicit flag/config (--wait-for-mcp / mcp_startup_timeout) instead of a hardcoded 1s that no headless network server can reliably meet.
  2. Surface the omission as a user-visible warning ("MCP server example-erp was still starting and was excluded from this turn") rather than a trace line — your report documents several hours of elimination work that a single warning would have prevented.
  3. The tool-catalog cache already softens this for warm starts; exec runs with fresh -C temp dirs defeat it, which is exactly the headless CI/automation profile this issue is about.