Resolution failure for flattened MCP tool names from OpenAI-compatible proxies

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

What issue are you seeing?

When using Codex with non-GPT models via OpenAI-compatible proxies (e.g., Ollama, Bifrost, LiteLLM), MCP tools belonging to a namespace (e.g., mcp__tilth__) cannot be called. The proxy layer flattens the namespaced tool call into a single string (e.g., mcp__tilth__search) and provides it in the name field of the tool call.

Codex's McpConnectionManager and resolve_mcp_tool_info perform strict equality checks on ToolName structs. A call with the plain string mcp__tilth__search fails resolution even if the tool is registered under the mcp__tilth__ namespace. This leads to an "unsupported call" error or an "unsupported payload" error (if routed to the MCP handler without proper metadata).

What steps can reproduce the bug?

  1. Configure Codex to use a local Ollama or Bifrost instance as a model provider.
  2. Enable an MCP server (e.g., tilth).
  3. Attempt to use an MCP tool (e.g., mcp__tilth__search).
  4. Observe that the model generates a flat tool call {"name": "mcp__tilth__search", "arguments": ...}.
  5. Codex fails to resolve this call to the namespaced MCP tool.

What is the expected behavior?

Codex should attempt to "un-flatten" tool names that follow the standard mcp__<server>__<tool> pattern if a direct match fails.

Additional information

A suggested fix is to update McpConnectionManager::resolve_tool_info in codex-mcp/src/connection_manager.rs to attempt a fuzzy match. If a tool name starts with mcp__, the resolver should attempt to split it by __ and match it against registered namespaces.

View original on GitHub ↗

5 Comments

github-actions[bot] contributor · 3 months ago

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

  • #20574
  • #19871

Powered by Codex Action

kapunakap · 3 months ago

I’m seeing an MCP invocation issue with Codex v0.131.0 using Ollama/local provider:

codex --oss --local-provider=ollama -m gemma4:31b-cloud -c model_context_window=256000

BrowserOS MCP is discovered correctly. /mcp shows the server, URL, and full tool list, including browseros_info, list_pages, and get_active_page.

But calling those concrete tools fails:

I attempted to call the following tools, but they failed with the error unsupported call:
mcp__browseros__:

- browseros_info
- list_pages
- get_active_page

So discovery works, but execution fails.

This seems like a Codex + Ollama/local-provider MCP dispatch issue rather than a BrowserOS server issue.

dhruvyad · 2 months ago

+1 also facing this issue

lich-wang · 10 days ago

Additional data point: the flattened name uses . (not __), and splitting into separate namespace + name fields is what actually resolves

Reproduced on codex-cli 0.148.0-alpha.9 (bundled in ChatGPT desktop 26.810.52044) against Ollama 0.32.14 with wire_api = "responses", model qwen3.8.

Two details that differ from the current description:

1. Ollama flattens with a single ., not __. Sending codex's {"type":"namespace","name":"mcp__node_repl","tools":[{"name":"js",...}]} to Ollama's /v1/responses, the model emits:

name = "mcp__node_repl.js"    arguments = {"code":"1+1"}

The arguments are fully correct — the model understands the tool perfectly. Only the name encoding differs.

2. Splitting by __ is not sufficient. I put a rewriting proxy in front of Ollama and tried both fixes:

| Rewrite | codex result |
|---|---|
| mcp__node_repl.js → mcp__node_repl__js | still ERROR codex_core::tools::router: error=unsupported call: mcp__node_repl__js |
| split into namespace="mcp__node_repl", name="js" | works — mcp: node_repl/js (completed) |

So the resolver isn't looking for a particular separator; it needs the two structured fields. This matches #24297, where mcp__node_repl__js is also rejected. A fix that only un-flattens by __ would likely still fail.

Control: same machine, same plugins, only switching to model_provider = "openai" with gpt-5.6-luna — the identical task succeeds (mcp: node_repl/js (completed)), confirming the MCP server and plugin side are healthy.

ericyanpek · 4 days ago

Third failure mode: the namespace is dropped entirely rather than flattened — on the built-in amazon-bedrock-runtime provider

Adding a data point that I think narrows the fix. @lich-wang's finding above (Ollama emits mcp__node_repl.js, and only splitting into the two structured fields resolves it) matches my root cause, but the model I hit does not flatten at all — it drops the namespace, leaving nothing to un-flatten. That makes the mcp__-prefix-splitting fix proposed in the issue body insufficient in a second, distinct way.

Environment

  • codex-cli 0.148.0 (Homebrew), macOS arm64
  • Provider: amazon-bedrock-runtime — Codex's built-in Bedrock provider, not an OSS/local proxy or OpenAI-compatible gateway
  • Failing model: global.xai.grok-4.6
  • Working control: global.openai.gpt-5.6-sol
  • MCP server: local stdio server (my-mcp-server)

Caveat stated up front: Grok 4.6 is not in Codex's shipped models_cache.json; I added it via model_catalog_json. But the failure is not in the catalog layer — the capture below shows Codex emitting a well-formed namespace container, and the model simply not echoing the field back. Any model that does not implement the proprietary namespace container will behave this way regardless of how it is reached.

Control: same provider, same config, only the model changes

Unlike previous reports in this cluster, the control here does not switch provider or transport. Both runs go through the identical amazon-bedrock-runtime provider, same config.toml, same MCP server, same prompt. The only variable is the model, which isolates the cause to the model's tool-call schema support rather than to any proxy or wire adapter.

Rollout records (~/.codex/sessions/.../rollout-*.jsonl), same tool, same arguments:

// global.openai.gpt-5.6-sol  ->  resolves
{"type":"function_call","name":"memory_info","namespace":"mcp__my_mcp_server","arguments":"{}"}
// -> McpToolCall{server:"my-mcp-server", tool:"memory_info"}   reaches the MCP server

// global.xai.grok-4.6  ->  fails
{"type":"function_call","name":"memory_info","arguments":"{}"}     // no `namespace` key at all
// -> ERROR codex_core::tools::router: error=unsupported call: memory_info

The request never leaves Codex, so the MCP server, its transport, and credentials are all provably uninvolved.

What Codex puts on the wire

Captured by pointing base_url at a loopback listener that dumps the request body (wire_api = "responses"):

"tools": [
  {"type":"function",  "name":"exec_command"},
  {"type":"function",  "name":"list_mcp_resources"},
  {"type":"custom",    "name":"apply_patch"},
  {"type":"namespace", "name":"mcp__my_mcp_server",
   "tools":[{"name":"memory_info"},{"name":"search_memory"},{"name":"browse_memories"}, ...]},
  {"type":"namespace", "name":"multi_agent_v1",
   "tools":[{"name":"spawn_agent"},{"name":"wait_agent"},{"name":"send_input"}, ...]}
]

Leaf names inside a namespace container are bare; the server identity exists only in the enclosing object. {"type":"namespace"} is not part of the public Responses API tool schema, so a model that has not been trained on it has no way to know the container name is semantically required on the way back.

Three distinct degradation modes are now documented

| Model / provider | Emitted name | namespace | Prefix survives? |
|---|---|---|---|
| Ollama, Bifrost, LiteLLM (#20652 body) | mcp__tilth__search | absent | yes, __ |
| Ollama 0.32.14 + qwen3.8 (@lich-wang) | mcp__node_repl.js | absent | yes, . |
| Grok 4.6 via Bedrock (this report) | memory_info | absent | no — dropped entirely |

Modes 1 and 2 differ only in separator, so a tolerant un-flattener could recover both. Mode 3 cannot be recovered by any string-splitting rule: the emitted name carries zero information about which namespace it came from.

Blast radius is wider than MCP

multi_agent_v1 is shipped as a namespace container too (see the capture above), so on an affected model the multi-agent tools — spawn_agent, wait_agent, send_input, resume_agent, close_agent — are equally unreachable. Only the flat function / custom tools work. This matches the observable symptom precisely: exec_command, apply_patch, list_mcp_resources and friends all work fine, while every namespaced tool fails.

Config-level escape hatches I tested, none of which help

All still produce unsupported call: memory_info:

  • features.non_prefixed_mcp_tool_names = true
  • same with {enabled = true, server_names = ["my-mcp-server"]}
  • features.code_mode = true plus tool_mode = "code_mode" on the catalog entry (this does silence the "does not advertise Code Mode support" warning, so the field is accepted — the model still emits a direct namespaced call)
  • adding multi_agent_version = "v2" to the catalog entry (Grok otherwise falls back to v1 while gpt-5.6-sol runs v2)

So as of 0.148.0 there is no user-reachable workaround short of not using MCP with the model.

Why prefix-splitting is not enough, and what would cover all three modes

ToolRegistry keys on ToolName { namespace, name } and looks up by strict equality, then dead-ends in unsupported_tool_call_message():

// codex-rs/core/src/tools/registry.rs
match self.tools.entry(tool_name) { ... }

fn unsupported_tool_call_message(payload: &ToolPayload, tool_name: &ToolName) -> String {
    match payload {
        ToolPayload::Custom { .. } => format!("unsupported custom tool call: {tool_name}"),
        _ => format!("unsupported call: {tool_name}"),
    }
}

A fallback that covers all three modes, applied only after strict lookup misses and only when is_default_namespace() holds:

  1. If the name matches <namespace><sep><leaf> for any registered namespace, with sep in __ / . / /, resolve it. Matching against registered namespace strings rather than splitting on a fixed separator handles servers whose own names contain __, and covers modes 1 and 2 including #24297.
  2. Otherwise, if the bare name matches the leaf name of exactly one tool across all registered namespaces, resolve to it. This is the only thing that can recover mode 3.
  3. If step 2 is ambiguous, fail — but with a message naming the candidate namespaces, so the model can retry with a qualified name instead of guessing. The current flat unsupported call: <name> gives it nothing to correct.

Step 2 is the part that needs an owner's decision, since silently picking a unique leaf name is a routing-behaviour change and the ambiguous case needs a deliberate policy. Happy to supply the full captured request body or the raw rollout files if that would help.

One framing note: because amazon-bedrock-runtime is a first-party built-in provider, this is not only a custom-proxy concern — it currently means no non-OpenAI model on Bedrock can use MCP or multi-agent tools at all. Given that Use Responses compaction for Amazon Bedrock landed on 2026-08-21, the Bedrock path looks actively maintained, so this may be cheaper to fix now than the custom-model label suggests.