Codex CLI at rust-v0.162.1 speaks one wire protocol, the Responses API, and sends every MCP server's tools as a single type:namespace entry named mcp__<server>, while its built-in shell tools go out as flat functions. There is no config key to flatten the namespace: PR #50447 removed the dead namespace_tools capability in 0.162.0, and the provider capabilities table accepts only external_web_access and remote_compaction. So the model server decides whether MCP works, in both directions: it must expand the namespace on the request and put the namespace back on the returned function_call. Ollama v0.34.0 and later (pin v0.40.4) and vLLM v0.25.0 and later (pin v0.31.0) do both. Ollama 0.32.7 to 0.33.x expand but return calls flat, so Codex answers unsupported call. llama.cpp llama-server (b11606) skips the namespace entry with a WARN log line and no client error, so MCP is silently absent. This week, open your model server's log during one Codex turn with one MCP server configured and confirm the tool arrives as callable.
You point Codex CLI at Ollama on the GPU box down the hall, or at vLLM in the cluster, or at llama-server on a workstation. Codex starts, runs shell commands, edits files, and codex mcp list shows your ticketing and docs servers enabled. Then the model never calls a single MCP tool. Or it calls one and Codex replies unsupported call. If you searched "codex cli ollama" because of that, look at the model server before the MCP server: the handshake succeeded, so the tools were lost after Codex had them.
Codex speaks one wire protocol, the Responses API, and it ships each MCP server's tools to the model as one type: "namespace" entry named mcp__<server>. Whether that entry becomes a callable tool is decided entirely by the model server, and it has to get two things right: expand the namespace on the way in, and put the namespace back on the function call on the way out. Nothing in Codex's config lets you change the shape.
Read at their releases on or before October 13, 2026, the four local backends split cleanly. Ollama v0.40.4 and vLLM v0.31.0 round-trip namespaces. llama.cpp's llama-server at b11606 drops them silently. LM Studio rejected them outright in older builds and its current behaviour is not readable from source. Every claim below comes from source at the pinned tags (Codex rust-v0.162.1, Ollama v0.40.4, vLLM v0.31.0, llama.cpp b11606) and from the public issue trackers; this is a reading of code and reports, not a lab run. This is also not a feature comparison of coding agents: for that, see our Codex vs Claude Code CLI comparison.
What Codex CLI actually sends at rust-v0.162.1
Start with the protocol. Codex's WireApi enum has a single variant, Responses, documented as the API OpenAI exposes at /v1/responses. The older options are gone, and they fail loudly at config load rather than quietly:
wire_api = "chat" fails with: ` wire_api = "chat" is no longer supported. How to fix: set wire_api = "responses" in your provider config. `model_provider = "ollama-chat" fails with: ` ollama-chat is no longer supported. How to fix: replace ollama-chat with ollama in model_provider, oss_provider, or --local-provider. `So every local backend you pair with Codex must implement POST /v1/responses, and must understand the tool types Codex puts in it.
Now the tools. For every MCP tool, create_tool_spec in core/src/tools/handlers/mcp.rs returns a ToolSpec::Namespace, unconditionally. The namespace name starts as the server name from your config and gets an mcp__ prefix, so a server declared as [mcp_servers.tickets] goes out as mcp__tickets. A later pass, merge_into_namespaces, folds all of one server's tools into a single namespace entry, sorts the members by name, and fills an empty description with "Tools in the <name> namespace." Each member is a plain function with strict: false. With a server that exposes two tools, the entry in the request looks like this:
{
"type": "namespace",
"name": "mcp__tickets",
"description": "Tools in the mcp__tickets namespace.",
"tools": [
{
"type": "function",
"name": "get_ticket",
"description": "Fetch one ticket by id.",
"strict": false,
"parameters": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] }
},
{
"type": "function",
"name": "search_tickets",
"description": "Search tickets by text.",
"strict": false,
"parameters": { "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] }
}
]
}Codex's built-in shell tools, exec_command and write_stdin, go into the same array as flat type: "function" entries. The patch tool is a freeform custom tool, and the fallback metadata Codex uses for an unrecognized local model leaves it out entirely. That asymmetry is the whole symptom: a backend that only understands flat functions keeps the shell tools and loses MCP, and the session looks healthy because the agent can still read files and run commands.
The namespace type is not a private Codex invention. Some issue reports call it proprietary, but the official openai-python SDK has defined NamespaceTool among the Responses tool types since release 2.25.0 (March 2026). The accurate framing is narrower: an OpenAI-compatible server can implement only part of the Responses tool types, and namespace is the one at issue here.
The return path matters as much as the request
When the model calls a tool, Codex builds the call from the function_call item as a pair: ToolName::new(namespace, name) in core/src/tools/router.rs. If the backend returns the call with no namespace field, the lookup fails and Codex sends the model the message unsupported call: <tool_name> from core/src/tools/registry.rs. A backend that expands namespaces on the request but returns calls flat gives you a model that can see your tools and can never successfully use them.
Codex config.toml has no option to flatten MCP tools
The obvious fix would be a setting that tells Codex to send flat functions to local providers. There is none at rust-v0.162.1.
PR #50447, "Remove the provider capability gate for tool namespaces", merged on October 2 and shipped in rust-v0.162.0 on October 8. It deleted namespace_tools from the provider capabilities along with the filter that used to drop namespace specs when that capability was false. Before the PR, the default was namespace_tools: true for every provider, configured providers used the default, and nothing exposed it as a user setting. So for Ollama, LM Studio and custom providers the PR changed nothing on the wire: namespaces were always sent, before and after. What it removed was a dead gate.
What is left in the provider schema confirms it. A [model_providers.<id>] block accepts keys such as name, base_url, wire_api, env_key, http_headers, query_params, request_max_retries and capabilities, and the capabilities table accepts exactly two keys: external_web_access and remote_compaction. A snippet that sets namespace_tools = false or a flatten mode is printing a key that does not exist.
Two Codex issues ask for the option. #26234, "Flatten MCP namespace tools for non-OpenAI Responses API providers (Ollama, LM Studio, OpenRouter)", opened June 3 and carries 49 thumbs-up and 35 comments. #36942, "MCP tools silently unavailable with llama-server backends: add option to flatten namespace tool specs to functions", opened August 4. Both were open on October 13. A community patch proposed in #26234 adds the capability back and flattens to mcp__<server>__<tool>; it is not in any Codex release.
One more config trap sits next to this one. Built-in provider ids cannot be overridden from config.toml. The schema says so ("Built-in IDs cannot be overridden"), and config validation enforces it: openai, ollama and lmstudio are reserved ids, so a [model_providers.ollama] block with a remote base_url makes the config fail to load with "model_providers contains reserved built-in provider IDs". Use the CODEX_OSS_BASE_URL environment variable, which the source marks experimental, or define a new provider id.
Codex CLI with Ollama, vLLM, llama.cpp and LM Studio: the backend matrix
"Request" is what the server does with the namespace entry. "Response" is whether a returned function_call carries the namespace field Codex needs.
Three details in that table are easy to miss.
First, Codex's own Ollama version check will not warn you. Its minimum is 0.13.4 ("Ollama {version} is too old. Codex requires Ollama {min} or newer."), far below the 0.34.0 that MCP round-tripping needs. An Ollama from the spring passes the check, codex --oss starts, and MCP does not work.
Second, the llama-server row is the dangerous one because nothing fails. In b11606, the Responses-to-chat conversion in tools/server/server-chat.cpp checks each tool's type; anything other than function is logged as unsupported Responses tool type '%s' skipped and dropped. Namespace and freeform tools vanish, Codex's built-in function tools pass through, and the client sees a normal response.
Third, namespaces are not only for MCP. At rust-v0.162.1 some built-in tools are namespaces too, such as the feature-gated current-time and sleep tools and the multi-agent v2 collaboration tools, so a backend that rejects the type outright can fail a request with zero MCP servers configured. The OpenRouter report in Codex #32318 (Codex 0.142.5 to 0.144.1) shows exactly that: HTTP 400 "No endpoints found that support the native namespace tool type" on prompts that used no MCP tools.
The matrix covers namespace handling only. For throughput, batching and the broader choice between the two servers that work, see our Ollama vs vLLM comparison.
| Backend and version | Request | Response | What you see in Codex | Verdict |
|---|---|---|---|---|
| Ollama v0.34.0 to v0.40.4 (pin v0.40.4) | Expanded to mcp__tickets.search_tickets | Namespace restored | Tools listed and callable | Works; name-collision edge case |
| Ollama v0.32.7 to v0.33.x | Expanded | Returned flat | Model calls the tool, gets unsupported call | Upgrade to 0.34.0+ |
| Ollama below v0.32.7 | Collapsed into one schema-less pseudo-function | Not applicable | Model cannot form a valid MCP call | Upgrade |
| vLLM v0.25.0 to v0.31.0 (pin v0.31.0) | Expanded to mcp__tickets__search_tickets | Namespace restored | Tools listed and callable | Works |
| llama.cpp llama-server (b11606) | Skipped, WARN in server log | Not applicable | MCP silently absent, shell tools fine | Proxy or move |
| LM Studio | 0.4.11 to 0.4.14 rejected with invalid_request_error on tools.N.type; a later build reported fixed | Not verifiable (closed source) | Requests carrying a namespace fail on the old builds | Verify on your build |
| Strict gateways (OpenRouter, AWS Bedrock Mantle) | Rejected or passed through, per user reports | Not applicable | OpenRouter: HTTP 400 even with no MCP tool in use | Not an on-prem path |
Codex CLI Ollama: why 0.32.7 was not enough
Ollama fixed this in two steps, a month apart, and the gap between them explains reports that otherwise look contradictory.
Ollama PR #17593, "openai: expand namespace tool declarations in the responses API", merged on August 7 and shipped in v0.32.7 on August 10. Before it, the conversion dropped the namespace's tools array and left the model one schema-less pseudo-function. After it, the members were expanded into real functions. The PR body says plainly that output parity, the namespace field on returned tool calls, was not addressed.
So from 0.32.7 through 0.33.x, the model could see the tools and Codex could not accept the calls. Two user reports show it. On Codex #36942, a comment dated August 23 from someone on codex-cli 0.149.0 with Ollama 0.32.15 describes the model seeing names like mcp__atlassian.getVisibleJiraProjects, calling them with the dotted name, Codex refusing the call, and the model going on to give a confident wrong diagnosis from shell tools alone. Ollama issue #18284, filed against 0.33.3 on September 7, reported function_call items with the namespace folded into the name and no namespace field; it was closed the same day as a duplicate.
The response side arrived with Ollama PR #18223, "openai: add client tool search support", merged September 4 and first released in v0.34.0 on September 5. It added responsesToolCallName and emits function_call with a namespace field. v0.33.2 does not contain it. That makes 0.34.0 the floor for Codex with MCP, not 0.32.7.
What v0.40.4 does on the wire
In openai/responses.go at v0.40.4, convertTools expands each namespace into its member functions and renames each one with qualifyNamespaceToolName, which joins namespace and member with a dot (unless the member already starts with the namespace plus a dot or underscore). Codex's mcp__tickets and search_tickets reach the model as mcp__tickets.search_tickets. On the way back, responsesToolCallName accepts the native form, the dotted form and a colon form (mcp__tickets:search_tickets), maps them to the namespace and member, and sets namespace on the output function_call, in streaming output too. A name that matches nothing comes back with an empty namespace, which Codex will treat as an unsupported call. The residual risk is in the matching. responsesToolCallName returns the first declared namespace whose qualified, dotted or colon form equals the model's string, so two declarations that produce the same string resolve by declaration order. Namespace a.b with member c and namespace a with member b.c both read as a.b.c. With Codex's mcp__<server> naming that needs unusual server or tool names, so treat it as an edge case. Ollama PR #16263, "responses: preserve namespace tool identity", proposes a request-scoped mapping that rejects collisions instead; it was open on October 13. If your MCP tool names contain dots, rename them.
Set the context window yourself
Any local model slug Codex does not recognize gets fallback metadata from model_info_from_slug. It logs "Unknown model {slug} is used. This will use fallback model metadata.", turns off the tool search path so MCP tools are sent directly in the tools array, and assumes a 272,000-token context window. Set model_context_window to what the server actually serves, and set the server side to match: Ollama's num_ctx behaviour is its own trap, covered in our post on Ollama silently truncating prompts at num_ctx. A tools array that includes several MCP servers is a large prompt prefix, and truncation there can look like a model that ignores tools.
How to diagnose missing Codex MCP tools
This is the opposite of an MCP handshake failure. If Codex reports the server process died or the connection closed, you are in MCP error -32000 territory and the model server is irrelevant. Here the handshake succeeds, tools/list succeeds, and the tools never reach the model, so the evidence lives on the model server.
codex mcp list --json and codex mcp get <name> --json. These report configuration and enablement. They do not prove the model received anything.GET /api/version with a version field; below 0.34.0, stop and upgrade. For vLLM, anything below v0.25.0 lacks namespace support. For llama-server, b11606 skips the type, so check tools/server/server-chat.cpp at whatever build you run before expecting a different answer.unsupported Responses tool type 'namespace' skipped. On LM Studio, look for an invalid_request_error with param set to tools.N.type. On Ollama and vLLM, confirm the request carries your mcp__ names.unsupported call: mcp__... in the transcript means the backend expanded the namespace but returned the call without it: Ollama 0.32.7 to 0.33.x, or a proxy that only rewrites one direction.The commands, with a remote Ollama as the example:
# Built-in provider on a remote host: the env var is the only way to move it CODEX_OSS_BASE_URL=http://gpu-01.internal:11434/v1 \ codex --oss --local-provider ollama -m qwen3-coder:30b # What Codex thinks is configured (not what the model sees) codex mcp list --json codex mcp get tickets --json # What the server actually is: needs 0.34.0 or later for MCP round-trips curl -s http://gpu-01.internal:11434/api/version # llama-server: the silent drop is only visible here grep "unsupported Responses tool type" llama-server.log
Without -m, codex --oss uses gpt-oss:20b and pulls it from Ollama if it is not present.
Three on-prem options for Codex MCP tools on a local model
If your approved server is in the "works" rows, you are done once versions are pinned. If it is llama-server, or an Ollama you cannot upgrade this quarter, you have three options.
# ~/.codex/config.toml model = "qwen3-coder:30b" # whatever tag the server serves model_provider = "ollama-gpu" model_context_window = 65536 # match the server, not Codex's 272k fallback # Requires Ollama v0.34.0 or later on the host for MCP calls to round-trip [model_providers.ollama-gpu] name = "Ollama on the GPU host" base_url = "http://gpu-01.internal:11434/v1" wire_api = "responses" # vLLM alternative (v0.25.0 or later, /v1/responses) # [model_providers.vllm-internal] # name = "vLLM" # base_url = "http://vllm.internal:8000/v1" # wire_api = "responses" # env_key = "VLLM_API_KEY" [mcp_servers.tickets] command = "tickets-mcp" args = ["--readonly"] enabled_tools = ["search_tickets", "get_ticket"] startup_timeout_sec = 20 tool_timeout_sec = 60
Option 1: run a backend that round-trips namespaces
Use this one unless something forbids it. vLLM's PR #47024, merged July 6 and first released in v0.25.0, expands each member to <namespace>__<tool> for prompting and parsing (so mcp__tickets__search_tickets) and restores the original namespace and name in Responses output, in streaming, and in replayed function_call history; its description names Codex as an example of the clients it is for. Ollama v0.34.0 and later do the same job with dotted names. Point Codex at it with a new provider id, because the built-in ids cannot be redirected from config: wire_api = "responses" is the only value Codex accepts, and there is deliberately no namespace key in the file because none exists. enabled_tools is worth setting on every server: it trims the namespace to the tools the agent needs, which shrinks the prompt prefix on a model with a modest context window.
Option 2: put a translating proxy in front of llama-server
The #36942 reporter describes running a proxy between Codex and llama-server that expands each namespace into flat functions named <namespace>__<tool> on the request and rewrites each returned function_call back to its namespace and name on the response. Their own words are that both directions are needed, and the Codex source agrees: a proxy that only flattens the request turns a silent drop into unsupported call. This works, and it is code you own. It sits on the path of every tool call the agent makes, so it needs the same review, tests and version pinning as anything else in that path. It is also not an MCP gateway. Governance gateways sit at the MCP layer between the agent and your tool servers, which is a separate decision covered in our MCP gateway comparison; this proxy sits at the Responses API layer between the agent and the model. We would not take a third-party proxy from an issue thread into a regulated environment unreviewed.
Option 3: use a different agent for MCP work
If the server cannot change and nobody wants to own a proxy, keep Codex for work that needs only shell and files, and use an agent that sends flat function tools for MCP-heavy work. The same question for other agents has its own failure modes: we covered what breaks when Claude Code points at a local model and running OpenCode offline in an air-gapped environment. For the protocol itself, our MCP developer guide covers how servers expose tools.
| Option | What you change | What you own afterwards | Pick it when |
|---|---|---|---|
| 1. Backend that round-trips namespaces | Run vLLM v0.25.0+ or Ollama v0.34.0+, pinned in the image | A version pin in change control | You can choose or upgrade the serving stack |
| 2. Translating proxy in front of the backend | Proxy expands namespaces on the request and rewrites calls back on the response | Code that rewrites every tool call the agent makes | llama-server is mandated and cannot move |
| 3. Different agent for MCP work | Agent that sends flat function tools, or Codex without MCP | A second tool in the approved list | You need MCP on the current server now |
Codex MCP tools on-prem in regulated environments
Teams that keep code on-prem point Codex at an in-network model server precisely so source never leaves the building. MCP is then the controlled path from the agent to internal systems: the ticket tracker, the document store, internal code search, each behind its own access policy and audit trail.
A silent drop changes what the agent is without telling anyone. On llama-server, or on an Ollama below 0.34.0, the agent still runs and still answers, but its answers come from the shell alone, and no error appears in the client. The #36942 report above, where the model gave a confident wrong diagnosis after its MCP calls failed, is the risk in one line: an engineer reviewing the output has no signal that the agent never consulted the ticket history it was supposed to read.
Two practices follow.
Pin the agent and the server together. Codex rust-v0.162.1 with Ollama 0.34.0 or later, or with vLLM 0.25.0 or later, is a pairing, and it belongs in change control as one item. An Ollama downgrade for an unrelated reason, or a switch to llama-server because it is what the platform team approved, silently changes which tools the agent has.
Audit at the server, not the agent. The model server's request log is the record of which tools were offered on each turn. codex mcp list and the model's own description of its tools are both self-reports. If your audit requirement says the agent's access to internal systems must be demonstrable, the evidence is the server-side request, and a translating proxy, if you run one, is the natural place to log it.
Check Codex MCP tools on your server this week
On the machine that serves your model, read the server version first: GET /api/version on Ollama, the release tag on vLLM, the build number on llama-server. Then configure exactly one MCP server in Codex, open the server log, and run one turn that should call a tool. If the request shows your mcp__ names and the call comes back without unsupported call, write the two version numbers into your change record as a pair. If it is llama-server, put the decision on the table now: move the coding workload to vLLM or Ollama, or assign an owner for a proxy that rewrites both directions. More on the agent tooling around this in our AI development tools guide.
FAQ
Quick answers to the questions this post tends to raise.



