codex exec can silently complete empty when configured MCP tools are deferred behind tool_search

Open 💬 2 comments Opened May 26, 2026 by yanxiyue

Summary

codex exec can silently finish with no assistant message when an explicitly configured MCP tool is deferred behind tool_search.

In the failing state, the MCP server is healthy and its tool is present in the runtime registry, but it is no longer directly exposed to the model. It only appears as a deferred source in tool_search. The model then emits only a reasoning item and response.completed; Codex accepts that as a completed turn and returns no final answer (last_agent_message = null).

This is observable as an empty codex exec result even though the task's first required action is to call the configured MCP tool.

Environment

  • codex-cli 0.133.0
  • Source tag checked: rust-v0.133.0
  • macOS
  • Auth mode: ChatGPT auth (auth_mode = "chatgpt")
  • features.apps enabled by default
  • A model/provider that supports the Responses API tool_search tool

Triggering state

The issue reproduces when all of these are true:

  1. ChatGPT auth is active, so Codex enables host-owned codex_apps.
  2. An app connector contributes many tools. In my case the GitHub connector contributed 90 tools.
  3. A bundled plugin MCP is installed/enabled. In my case:

plugins/cache/openai-bundled/computer-use/1.0.799/.mcp.json

  1. A task-specific MCP server is configured, e.g. agentrix_event_broker with a change_title tool.
  2. Developer instructions require that MCP tool as the first action.
  3. Run codex exec --json.

The combined MCP tool count crosses DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD = 100, causing Codex to defer MCP tools behind tool_search.

Minimal shape of the command

This uses a temporary CODEX_HOME copied from a ChatGPT-authenticated setup and then adds a task-specific MCP server via config overrides:

CODEX_HOME="$TMP_HOME" codex exec --json --skip-git-repo-check -C /path/to/repo \
  -c 'developer_instructions="## Title Setting Protocol:\n- MUST set a descriptive title as your FIRST action when the user makes a request\n- Use the Agentrix event broker MCP tool for this: server \\\"agentrix_event_broker\\\", tool \\\"change_title\\\"\n- This is mandatory when the tool is available; do this before any other work"' \
  -c 'mcp_servers.agentrix_event_broker.url="http://127.0.0.1:PORT/mcp/agentrix-event-broker"' \
  -c 'mcp_servers.agentrix_event_broker.bearer_token_env_var="AGENTRIX_EVENT_MCP_TOKEN"' \
  'hello'

Important detail: the configured MCP server is not the problem. If I remove the bundled computer-use plugin MCP from the same temp CODEX_HOME, the model directly calls agentrix_event_broker.change_title and then replies normally.

Actual behavior

With the bundled computer-use plugin present, stdout JSONL contains only:

{"type":"thread.started", ...}
{"type":"turn.started"}
{"type":"turn.completed", "usage": {...}}

The rollout/session file contains only a model reasoning item followed by task completion:

{"type":"response_item","payload":{"type":"reasoning", ...}}
{"type":"event_msg","payload":{"type":"task_complete","last_agent_message":null, ...}}

No tool_search call is emitted. No MCP tool call is emitted. No assistant message is emitted.

Expected behavior

One of these should happen instead:

  • Explicitly configured MCP servers/tools required by developer instructions remain directly visible, even when other MCP tools are deferred.
  • Or Codex should automatically make a tool_search discovery step available/required before treating the turn as complete.
  • Or codex exec should fail loudly with a diagnostic instead of returning an empty successful turn when the model completes without an assistant message and without satisfying a required tool path.

At minimum, a configured MCP server should not become effectively invisible just because unrelated app/plugin tools pushed the total MCP tool count over a threshold.

Evidence from request shape

I captured the outbound Responses request in both cases.

Without the bundled computer-use plugin:

  • Direct namespace exists: mcp__agentrix_event_broker__
  • Child tool exists: change_title
  • The model calls the MCP tool first and then replies.

With the bundled computer-use plugin:

  • The request had 17 top-level tools.
  • There were no direct MCP namespaces at all.
  • tool_search was present.
  • The tool_search description listed deferred sources including:
  • GitHub
  • agentrix_event_broker
  • computer-use
  • node_repl
  • The model did not call tool_search; it emitted only reasoning and completed.

So this is not an MCP startup/discovery failure. It is a model-visible tool exposure / deferred-tool recovery failure.

Relevant source path

From rust-v0.133.0:

  • codex-rs/codex-mcp/src/mcp/mod.rs
  • host_owned_codex_apps_enabled(config, auth) returns true when config.apps_enabled && auth.uses_codex_backend().
  • codex-rs/core/src/session/turn.rs
  • built_tools() calls list_all_tools() and then build_mcp_tool_exposure(...).
  • codex-rs/core/src/mcp_tool_exposure.rs
  • DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD = 100
  • when search_tool_enabled and the deferred MCP tool count crosses the threshold, direct_tools becomes empty and all MCP tools become deferred.
  • codex-rs/core/src/tools/spec_plan.rs
  • only direct exposures are added to model-visible specs.
  • deferred tools are added to the registry and surfaced through ToolSearchHandler.

That means the configured MCP tool is technically callable after discovery, but it is not directly visible to the model. In exec, the model is still allowed to end the turn after producing only reasoning, yielding an empty result.

Why this matters

This makes non-interactive automation brittle. A user-local plugin or ChatGPT connector can change the tool exposure mode for an unrelated task-specific MCP server. Once that happens, tasks that rely on a configured MCP tool can complete successfully from Codex's perspective while producing no output and no useful error.

View original on GitHub ↗

2 Comments

jshaofa-ui · 3 months ago

Silent Completion Fix — Solution Submission

1|# Codex #24536 — Silent Completion When MCP Tools Deferred Behind tool_search
2|
3|## Problem
4|
5|codex exec silently finishes with no assistant message when an explicitly configured MCP tool is deferred behind tool_search. The model emits only a reasoning item and response.completed; Codex accepts that as a completed turn and returns no final answer.
6|
7|## Root Cause
8|
9|When the combined MCP tool count crosses DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD = 100, Codex defers MCP tools behind tool_search. The model doesn't know to call tool_search first to discover the deferred tools, so it emits only reasoning and completes the turn. The last_agent_message is null, and codex exec returns an empty result.
10|
11|## Fix
12|
13|### 1. Add explicit tool_search guidance when tools are deferred
14|
15|``rust
16|// In the turn completion logic:
17|
18|fn handle_turn_completion(&self, response: &Response) -> Result<TurnResult> {
19| let has_tool_calls = response.items.iter().any(|item| matches!(item, ResponseItem::ToolCall(_)));
20| let has_reasoning_only = response.items.iter().all(|item| matches!(item, ResponseItem::Reasoning(_)));
21|
22| // FIX: If tools are deferred behind tool_search and model only reasoned,
23| // inject guidance to use tool_search first
24| if !has_tool_calls && has_reasoning_only && self.has_deferred_tools() {
25| let deferred_count = self.count_deferred_tools();
26| tracing::warn!(
27| "Model completed turn with reasoning only but {} MCP tools are deferred behind tool_search. Injecting guidance to discover tools first.",
28| deferred_count
29| );
30|
31| // Inject a system message guiding the model to use tool_search
32| self.inject_system_message(format!(
33| "Note: {} MCP tools are available but deferred behind tool_search. Use the tool_search tool to discover and call the tools you need before completing your response.",
34| deferred_count
35| ));
36|
37| return Ok(TurnResult::Continue); // Continue the turn
38| }
39|
40| // Existing logic for normal completion
41| Ok(TurnResult::Complete)
42|}
43|
44|fn has_deferred_tools(&self) -> bool {
45| self.mcp_tool_count > DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD
46|}
47|
``
48|
49|### 2. Add validation for empty exec results
50|
51|

Lulz3r · 1 month ago

Additional Windows Codex Desktop automation evidence (app 26.715.4045.0):

  • A local stdio MCP server is configured and healthy.
  • Its governed executor tool is present in the runtime callable catalog as mcp__quiet_exec__run.
  • The tool is not initially exposed as a direct task tool.
  • A scheduled automation that must use this executor first previously hit a raw-shell policy denial, then failed an exact lazy-discovery query and reported a false tooling failure.
  • A broader catalog inspection in the same environment found the tool immediately and the direct call succeeded.
  • The application/production system under audit was healthy; the failure was entirely in executor discovery/routing.

This makes the issue especially harmful for unattended automations: prompt wording and unrelated tool cardinality can determine whether a required governed executor is reachable.

A robust runtime contract would provide one of:

  1. A per-server/per-tool always_direct or pinned exposure option for required governed tools.
  2. Automatic inclusion of explicitly required MCP tools before deferring unrelated tools.
  3. Deterministic tool_search matching by exact server/tool identity.
  4. A fail-loud completion path when a required executor remains deferred, instead of allowing application-level “tooling noise” or empty success.

The current local mitigation is to inspect the full callable catalog before tool_search, but this is prompt-level policy and should not be necessary for a required Windows executor.