Summary
When Codex uses a custom model_providers.X with wire_api = "responses" (transport responses_http), it emits MCP-server tools wrapped in a proprietary Responses-API extension:
{"type":"namespace","name":"mcp__<server>__","description":"...","tools":[...]}
This wrapper has no equivalent in the Chat-Completions schema that most OpenAI-compatible backends use internally (llama.cpp, LM Studio, Ollama, DeepSeek). When they translate the request, anything whose type is not function is dropped or rejected. The MCP tools never reach the model. The user sees "no MCP tools" with no error from either side.
Related: #19871 — same problem, but bisected as a Codex regression. The cross-backend framing makes it clearer that this is a protocol-shape issue, not just a Codex-internal regression.
Evidence
A codex exec --profile local session against llama-server with one MCP server registered. Codex's outbound /v1/responses tools[] contains:
[
{"type":"function","name":"exec_command", ...},
... 13 native codex tools ...,
{"type":"web_search"},
{"type":"namespace","name":"mcp__open_websearch__",
"tools":[
{"type":"function","name":"fetchWebContent","parameters":{...}},
{"type":"function","name":"search","parameters":{...}}]}
]
The same Codex session against ChatGPT (responses_websocket) sends an identical namespace shape, but the ChatGPT backend understands and unwraps it server-side. Custom backends can't.
Affected backends
Proposed fix
When the transport is not the ChatGPT websocket (i.e. anytime the wire peer is a generic Responses-compatible server), flatten MCP namespaces into top-level function tools on the codex side:
{"type":"function","function":{"name":"mcp__<server>__<tool>","parameters":{...},"description":"..."}}
Tool-call dispatch then routes back through the same path; no reverse map needed if the namespaced name is preserved end-to-end.
Doing the flattening on the codex side fixes every downstream backend at once. Asking every backend to teach itself the namespace extension is the slower path.
Summary
When Codex uses a custom
model_providers.Xwithwire_api = "responses"(transportresponses_http), it emits MCP-server tools wrapped in a proprietary Responses-API extension:{"type":"namespace","name":"mcp__<server>__","description":"...","tools":[...]}This wrapper has no equivalent in the Chat-Completions schema that most OpenAI-compatible backends use internally (llama.cpp, LM Studio, Ollama, DeepSeek). When they translate the request, anything whose
typeis notfunctionis dropped or rejected. The MCP tools never reach the model. The user sees "no MCP tools" with no error from either side.Related: #19871 — same problem, but bisected as a Codex regression. The cross-backend framing makes it clearer that this is a protocol-shape issue, not just a Codex-internal regression.
Evidence
A
codex exec --profile localsession against llama-server with one MCP server registered. Codex's outbound/v1/responsestools[]contains:[ {"type":"function","name":"exec_command", ...}, ... 13 native codex tools ..., {"type":"web_search"}, {"type":"namespace","name":"mcp__open_websearch__", "tools":[ {"type":"function","name":"fetchWebContent","parameters":{...}}, {"type":"function","name":"search","parameters":{...}}]} ]The same Codex session against ChatGPT (
responses_websocket) sends an identicalnamespaceshape, but the ChatGPT backend understands and unwraps it server-side. Custom backends can't.Affected backends
namespace(see Eval bug: Codex, 'type' of tool must be 'function' ggml-org/llama.cpp#20156).invalid_request_error("invalid_request_error" when passing tools with the type "namespace" using Codex and enabled MCPs lmstudio-ai/lmstudio-bug-tracker#1810).function_call(bug(codex/responses): namespace MCP tools are not flattened/mapped for DeepSeek OpenAI-compatible upstream, causing silent no-tool-call router-for-me/CLIProxyAPI#3298).Proposed fix
When the transport is not the ChatGPT websocket (i.e. anytime the wire peer is a generic Responses-compatible server), flatten MCP namespaces into top-level function tools on the codex side:
{"type":"function","function":{"name":"mcp__<server>__<tool>","parameters":{...},"description":"..."}}Tool-call dispatch then routes back through the same path; no reverse map needed if the namespaced name is preserved end-to-end.
Doing the flattening on the codex side fixes every downstream backend at once. Asking every backend to teach itself the
namespaceextension is the slower path.