Codex stops at `list_mcp_resources` and fails to discover/use tool-only MCP servers like Context7

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

What version of the IDE extension are you using?

see below

What subscription do you have?

plus

Which IDE are you using?

everyone of them.

What platform is your computer?

_No response_

What issue are you seeing?

Summary

Codex appears to treat an MCP server with no resources as effectively unavailable, instead of continuing to discover and use that server’s tools.

This breaks compatibility with tool-oriented / legacy MCP servers such as Context7, which may expose tools but no resources. In practice, Codex tries list_mcp_resources, gets an empty result, and then says it has no access to MCP servers or that the server is not installed, even though the server is configured and visible in /mcp.

This issue seems broader than Context7 specifically. The underlying bug is that Codex should not probe the resources interface once and give up when a server exposes tools but no resources.

Expected behavior

If an MCP server has no resources, Codex should still:

  • recognize the server as available
  • discover its tools
  • use those tools when appropriate
  • avoid telling the user the server is unavailable just because resources/list is empty

At minimum, an empty resources list should not be treated as equivalent to “server not found” or “no MCP access.”

Actual behavior

Codex repeatedly attempts list_mcp_resources, gets back an empty result or related failure, and then does not proceed correctly with tool discovery / tool usage.

Observed behaviors reported in this issue family include:

  • codex.list_mcp_resources({}) returning {"resources": []}
  • Codex saying Context7 or other configured MCP servers are “not installed”
  • Codex saying it has “no access to MCP servers”
  • server-specific resource calls failing even though the server appears enabled in /mcp
  • /mcp showing the server is enabled, but Codex still not using its tools

Example reported error:

• Called context7.list_mcp_resources({"server":"context7"})
└ Error: resources/list failed: unknown MCP server 'context7'

At the same time, /mcp can show the server as enabled.

Why the prior closure did not resolve the real issue

The previous issue was effectively closed on the basis that “Context7 has no Resources.” That explains why resources/list is empty, but it does not explain or resolve the actual bug:

  • Context7 has tools
  • Codex should still discover and use those tools
  • an empty resources interface is not a valid reason for Codex to conclude the server is unavailable

So the problem is not “user misunderstanding MCP resources.” The problem is that Codex’s MCP discovery / selection flow appears to over-index on resources and fails on tool-only servers.

Reproduction

  1. Configure a tool-oriented MCP server that exposes tools but no resources, such as Context7.
  2. Confirm the server appears enabled in /mcp.
  3. Ask Codex to use that MCP server or to use functionality that should route through its tools.
  4. Observe Codex attempting list_mcp_resources first.
  5. Observe that after an empty resources result or resource-related failure, Codex does not successfully use the server’s tools and may instead claim the server is unavailable.

Example config

[mcp_servers.Context7]
command = "npx"
args = ["-y", "@upstash/context7-mcp@latest"]

[mcp_servers.Context7.env]
CONTEXT7_API_KEY = "********"

Other affected tool-oriented servers have also been reported in the same discussion pattern.

Evidence suggesting this is not isolated to one server

In the linked discussion, multiple users reported that configured MCP servers appeared enabled in /mcp but Codex still failed to use them properly. Some reports also mention other servers beyond Context7.

That suggests the issue is with Codex’s MCP handling strategy, not just one specific MCP package.

Possible root cause

A likely cause is that Codex’s MCP discovery / planning flow:

  1. probes resources/list
  2. sees no resources
  3. does not sufficiently fall back to tool discovery / tool descriptions / direct tool invocation

If so, Codex should treat these as separate capabilities:

  • resources
  • resource templates
  • tools

A server with any valid capability should remain eligible for use.

Requested fix

Please update Codex’s MCP handling so that:

  • empty resources/list does not mark a server as unavailable
  • tool-only MCP servers are discoverable and usable without special prompting
  • Codex falls back from resources discovery to tools discovery automatically
  • error messaging distinguishes:
  • “server exists but has no resources”
  • “server exists and has tools”
  • “server is actually unknown / unavailable”

Environment

Reported examples in the prior thread include:

  • Codex CLI 0.54.0
  • Codex VS Code extension 0.4.35
  • VS Code 1.105.1
  • macOS 26.0.1

Other users in the same thread reported similar behavior with different MCP server configurations.

Related issues

  • #6215
  • #6217
  • #6465

Impact

This makes MCP support unreliable for a class of valid MCP servers and forces users to manually “handhold” Codex into using tools that should be discoverable automatically. It also creates misleading UX because /mcp can show a server as enabled while Codex simultaneously claims it has no MCP access.

What steps can reproduce the bug?

see above!

What is the expected behavior?

_No response_

Additional information

_No response_

View original on GitHub ↗

7 Comments

github-actions[bot] contributor · 4 months ago

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

  • #14115
  • #13766

Powered by Codex Action

suhan3z · 3 months ago

Additional repro on codex-cli 0.118.0 with a local tool-only MCP server (Recallium) over both direct HTTP and stdio bridge.

Environment

  • Codex CLI: 0.118.0
  • Model: gpt-5.4
  • Platform: WSL2 / Ubuntu on Windows
  • Config path: ~/.codex/config.toml

MCP config

[mcp_servers.recallium]
url = "http://127.0.0.1:8001/mcp"

I also tested a stdio bridge variant:

codex mcp add recallium-bridge -- npx -y @thefoot/mcp-stdio-http-bridge --url http://127.0.0.1:8001/mcp

Same result.

What is verified

  • codex mcp list shows the server as enabled
  • codex mcp get recallium shows correct transport and URL
  • curl http://127.0.0.1:8001/mcp/status succeeds outside sandbox
  • Recallium reports tools, but no resources (resources.supported_schemes=[] / tool-only server)

Critical observation

Using codex debug app-server send-message-v2 'recallium' shows:

  • mcpServer/startupStatus/updated -> recallium: starting
  • then recallium: ready

So Codex does start the MCP server successfully and marks it ready.

But after that, the model still behaves as if no MCP tool exists:

  • no MCP tool call is made
  • it treats recallium as plain text / repo name
  • codex exec ... explicitly answers:

> "The recallium MCP tool is unavailable in this runtime."

Interpretation

This looks like a failure in the layer after startup:

config -> MCP server startup -> ready works

but

ready -> MCP tools injected into active runtime tool list fails

Because Recallium is a tool-only MCP server, this seems consistent with the hypothesis in this issue: Codex is not correctly handling servers that expose tools but no resources.

Extra notes

  • Enabling -c features.rmcp_client=true did not help
  • Recreating the server entry with codex mcp remove/add did not help
  • Switching from direct HTTP to stdio bridge did not help

If useful, I can provide a tighter minimal repro based on Recallium, but the key symptom is: server reaches ready, runtime still says tool unavailable.

jkraunelis · 2 months ago

I used codex to help get to the bottom of this.

Codex CLI defaults to bundling MCP server tools as namespace-typed entries in the tools[] of the /v1/responses request. Hosted OpenAI models (gpt‑5.x) understand this Responses‑API extension; local models (e.g. gemma4:26b via Ollama) do not. Two failures stack on top of each other:

  1. Tool exposure: the model only sees the namespace name (e.g. mcp__playwright__), not the inner tool schemas, so it hallucinates names.
  2. Tool routing: even when the model emits a name, codex's MCP router resolves by (namespace, name) pair — a flat mcp__playwright__browser_navigate with no separate namespace field doesn't match. And local models often emit alternative separators (mcp:playwright:browser_navigate).

Fix candidates:

  • Disable namespace_tools capability for the OSS provider so MCP tools are sent inline as function-typed entries.

---

Symptom

Same machine, same MCP config (Playwright MCP enabled), same Codex install:

| Setup | Behavior |
|--------------------------------------------------------|---------------------------------------|
| codex (OpenAI hosted gpt‑5.x) | Calls playwright.browser_navigate ✓ |
| codex --oss -m gemma4:26b (Ollama) | Refuses, hallucinates tool names ✗ |

Failing transcript (turn 1, before any fix):

› can you open a browser and navigate to nhl playoffs schedule
⚠ Model metadata for `gemma4:26b` not found. Defaulting to fallback metadata; ...
• I cannot execute the mcp:playwright:launch_browser command directly...
  However, if you have the Playwright MCP server installed... you can use it
  by calling the tool through your interface.

Notice the model invented mcp:playwright:launch_browser — a name that does not
exist (the real tool is playwright.browser_navigate).

---

Reproduction (against unpatched codex)

# 1. Start Ollama with a tool-capable model
ollama pull gemma4:26b
ollama serve   # listens on :11434

# 2. Configure Codex with at least one MCP server in ~/.codex/config.toml
#    (Playwright MCP is the canonical reproducer here)

# 3. Run codex against the OSS provider
codex --oss -m gemma4:26b
# > "can you open a browser and navigate to nhl playoffs schedule"

Expected: tool call. Actual: refusal + hallucinated tool names.

---

Investigation Summary

  1. Hypothesis: capture the actual outbound request body via a logging proxy. Wrote /tmp/codex_ollama_proxy.py (see below). Saw codex sending MCP tools as {"type": "namespace", "name": "mcp__playwright__", ...} instead of expanding them as individual function entries. Confirmed gemma4:26b can call tool schemas correctly when sent inline (verified with a direct curl with one tool definition).
  1. Verified routing failure: after the proxy expanded namespaces, the model responded with name: mcp:playwright:browser_navigate" (single colon separator) and codex emitted `function_call_output: unsupported call:

mcp:playwright:browser_navigate. The router constructs ToolName::new(namespace, name) and looks up by canonical (namespace, name) pair — a flat name with :` separator never matches.

  1. End-to-end fix: extended the proxy to (a) expand namespaces on the request side and (b) on the response side, normalize alternate separators (:, ., /, -) back to __ and split flat mcp__SERVER__TOOL names into {name: "TOOL", namespace: "mcp__SERVER__"} so they match codex's canonical form. Browser navigated successfully.

---

Root cause (with code citations)

All paths relative to codex/codex-rs/.

Where the namespace tool format is defined

tools/src/responses_api.rs:43-67:

pub enum LoadableToolSpec {
    Function(ResponsesApiTool),
    Namespace(ResponsesApiNamespace),    // <-- the offending wrapper
}

pub struct ResponsesApiNamespace {
    pub name: String,                                 // e.g. "mcp__playwright__"
    pub description: String,
    pub tools: Vec<ResponsesApiNamespaceTool>,        // inner tools (with schemas)
}

Where the capability is decided per provider

model-provider/src/provider.rs:27-39:

pub struct ProviderCapabilities {
    pub namespace_tools: bool,
    pub image_generation: bool,
    pub web_search: bool,
}

impl Default for ProviderCapabilities {
    fn default() -> Self {
        Self {
            namespace_tools: true,   // <-- default-on for everyone
            image_generation: true,
            web_search: true,
        }
    }
}

model-provider/src/amazon_bedrock/mod.rs:60: Bedrock explicitly overrides to false.
model-provider/src/provider.rs:156 (ConfiguredModelProvider): does not override, so all custom providers — including the built-in OSS provider for Ollama/LMStudio — inherit namespace_tools: true.

Where the capability gates expansion

tools/src/tool_config.rs:244 (with_namespace_tools_capability):

pub fn with_namespace_tools_capability(mut self, namespace_tools: bool) -> Self {
    if !namespace_tools {
        self.namespace_tools = false;   // forces inline `function` expansion
    }
    self
}

When false, MCP tools become individual ToolSpec::Function entries instead of
being grouped into a ToolSpec::Namespace.

Where routing requires the (namespace, name) split

core/src/tools/router.rs:180-192:

ResponseItem::FunctionCall { name, namespace, arguments, call_id, .. } => {
    let tool_name = ToolName::new(namespace, name);
    if let Some(tool_info) = session.resolve_mcp_tool_info(&tool_name).await {
        // dispatch to the right MCP server
    }
}

codex-mcp/src/connection_manager.rs:616-621:

pub async fn resolve_tool_info(&self, tool_name: &ToolName) -> Option<ToolInfo> {
    let all_tools = self.list_all_tools().await;
    all_tools.into_values()
        .find(|tool| tool.canonical_tool_name() == *tool_name)
}

codex-mcp/src/tools.rs:51:

pub fn canonical_tool_name(&self) -> ToolName {
    ToolName::namespaced(self.callable_namespace.clone(), self.callable_name.clone())
}

So a flat name like mcp__playwright__browser_navigate with namespace: None will never == the canonical ToolName::namespaced("mcp__playwright__", "browser_navigate"). The router then falls through to the plain function lookup at core/src/tools/registry.rs:321, which emits "unsupported call: <name>".

Where the unsupported-call message is emitted

core/src/tools/registry.rs:579-584:

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

This is purely a name-lookup miss — not a permissions/sandbox failure.

Working mitigation: Python proxy codex_ollama_proxy.py

What it does

  1. Listens on 127.0.0.1:11500, forwards to http://localhost:11434 (Ollama).
  2. Request side (when EXPAND_NAMESPACES=1, default on, and the path is POST /v1/responses): walks tools[], replaces each {"type":"namespace", "name":NS, "tools":[...]} with the inner function entries flattened to top level. Inner names are prefixed with the namespace name (e.g., browser_navigatemcp__playwright__browser_navigate). Recomputes Content-Length.
  3. Response side: line-buffers the SSE stream. For each data: {...} event, walks the JSON for type: "function_call" items and normalizes the name field via normalize_function_call_name:
  • Tries the original name plus variants where :, ., /, - are

replaced by __.

  • Matches against the namespace prefixes seen in the corresponding request.
  • Splits matched names into {name: "TOOL", namespace: "mcp__SERVER__"}.
  1. Always sends Connection: close to avoid HTTP/1.1 keep-alive framing bugs (codex's reqwest client + python BaseHTTPRequestHandler + chunked SSE was hanging without this).
  2. Dumps every full request body, every rewritten request body, and every raw response stream to /tmp/codex_proxy_bodies/HHMMSS-{req-orig,req-rewritten,resp.sse}`.
  3. Logs structured summaries to stderr (and /tmp/codex_proxy.log if you start it via python3 codex /tmp/ollama_proxy.py 2>&1 | tee /tmp/codex_proxy.log).

How to run

# Terminal A — proxy
python3 /tmp/codex_ollama_proxy.py 2>&1 | tee /tmp/codex_proxy.log

# Terminal B — codex pointed at the proxy
CODEX_OSS_BASE_URL=http://localhost:11500/v1 codex --oss -m gemma4:26b

Confirmed working: > can you open a browser and navigate to nhl playoffs schedule produces a real playwright.browser_navigate({"url":"https://www.google.com"}) (🙃) call and a snapshot of the page.

Source-side fix (drafted, but I'm not an approved Contributor)

Single-method override on the catch-all ConfiguredModelProvider:

codex-rs/model-provider/src/provider.rs (insert into the impl ModelProvider for
ConfiguredModelProvider
block, after fn info):

fn capabilities(&self) -> ProviderCapabilities {
    let mut caps = ProviderCapabilities::default();
    // OSS providers (ollama, lmstudio, and other endpoints that use the
    // built-in `gpt-oss` provider definition) are pointed at local model
    // servers whose models have not been trained on the Responses-API
    // `namespace`-typed tool wrapper. Sending MCP tools as namespaces makes
    // those models hallucinate non-existent tool names. Expand MCP tools
    // inline as `function`-typed entries instead.
    if self.info.name == "gpt-oss" {
        caps.namespace_tools = false;
    }
    caps
}
rnett · 2 months ago

See also: https://github.com/openai/codex/issues/20574. Might actually be a dupe.

yunusemregul · 4 days ago

I can confirm a closely related reproduction on Codex Desktop 26.707.72221 with the bundled Codex CLI/app-server 0.144.2.

I have two independent custom Streamable HTTP MCP servers configured globally with the current [mcp_servers.<name>] syntax. Both servers:

  • appear enabled in codex mcp list
  • initialize successfully
  • advertise the MCP tools capability
  • return complete valid results from direct tools/list
  • can execute their tools through a direct MCP client

However, new Desktop threads receive no callable mcp__<server>__... tools. Restarting Desktop and creating a fresh thread does not help. The servers are tool-oriented and do not advertise resources, while the thread-facing resource discovery path does not expose them.

I posted the full environment and discovery-versus-exposure evidence in #19425. This looks like the intersection of this tool-only discovery issue and the Desktop thread injection/indexing problem described there. Related Desktop report: #10499. Historical thread-injection regression: #19649.

yunusemregul · 4 days ago

Follow-up: I traced and fixed the overlapping unreachable-tool path locally.

Under code_mode_only, MCP tools can be discovered successfully and then deferred, but tool_search was also removed from the top-level model surface because it retained normal exposure. That leaves the discovered tools unreachable even though tools/list succeeded. The patch makes tool_search DirectModelOnly, while keeping MCP tools deferred until search returns their namespace.

The change includes planner and Responses Lite integration coverage for search -> deferred MCP tool call:

The PR is hosted in the fork because upstream documents contributions as invitation-only and GitHub rejected direct PR creation for this account. A maintainer invitation would allow the same branch to be submitted upstream.

konard · 1 day ago

I can confirm the namespace-routing half of this issue with Codex CLI 0.144.6, a local tool-only MCP server, and a custom Responses API provider.

Minimal reproduction

  1. Configure a local MCP server that advertises two read-only tools, websearch and webfetch, and no resources.
  2. Configure Codex to use a custom Responses provider.
  3. The provider receives the MCP tools as a Responses namespace similar to:
{
  "type": "namespace",
  "name": "mcp__issue781",
  "tools": [
    {"type": "function", "name": "websearch", "parameters": {"type": "object"}},
    {"type": "function", "name": "webfetch", "parameters": {"type": "object"}}
  ]
}
  1. If the provider replies with the apparently qualified but flat call below, Codex rejects it:
{
  "type": "function_call",
  "call_id": "call_1",
  "name": "mcp__issue781__websearch",
  "arguments": "{\"query\":\"Acer A325-45 charger\"}"
}

Observed result: unsupported call: mcp__issue781__websearch.

  1. Returning the same logical call as a (namespace, name) pair works:
{
  "type": "function_call",
  "call_id": "call_1",
  "namespace": "mcp__issue781",
  "name": "websearch",
  "arguments": "{\"query\":\"Acer A325-45 charger\"}"
}

With that envelope, Codex dispatched one search, three sequential fetches, and received the final cited synthesis.

Workaround

Our provider now recursively expands namespace children into qualified names only for internal capability selection, retains the original namespace definition, and rehydrates a selected child to separate namespace and name fields in the Responses output. We also mark the MCP tools readOnlyHint: true; without the annotation, a non-interactive read-only Codex run cancelled the otherwise valid calls as approval-requiring operations.

The complete red/green transcripts and regression are being preserved in link-assistant/formal-ai#803.

Suggested fixes

  • Document that Responses namespace calls must round-trip as the exact (namespace, child name) pair; a flattened fully-qualified name is not equivalent.
  • Validate malformed/flattened namespace calls with a targeted diagnostic that includes the expected envelope instead of the generic unsupported call message.
  • For custom/OSS providers that do not natively understand Responses namespace tools, expose an explicit provider capability to request flattened function definitions and normalize the response before routing.
  • Keep tool discovery independent of resource discovery so a valid tool-only MCP server remains callable.

This matches the router analysis already posted above and adds a current, deterministic end-to-end reproduction against a custom Responses provider.