Codex tries resources/read on a tool-only MCP server instead of invoking the available fetch tool

Open 💬 0 comments Opened Jun 9, 2026 by kid0114

Summary

Codex appears to over-route URL fetching through MCP resources when using a tool-only MCP server.

My MCP server exposes a fetch tool, but does not expose arbitrary HTTP/HTTPS URLs as MCP resources. When I asked Codex to read a web page, it first listed MCP resources, got an empty list, and then attempted to call resources/read using the raw HTTPS URL.

This failed with Unknown resource, even though the correct tool was available.

Example failure

Error: resources/read failed: resources/read failed for llmmcp-fetch
(https://example.com/article):
Mcp error: 0: Unknown resource:
https://example.com/article

Codex then called:

Called llmmcp-fetch.list_mcp_resources({"server":"llmmcp-fetch"})
└ {"server": "llmmcp-fetch", "resources": []}

And then:

Called llmmcp-fetch.read_mcp_resource({
  "server": "llmmcp-fetch",
  "uri": "https://example.com/article"
})

Which failed:

Error: resources/read failed: resources/read failed for llmmcp-fetch
(https://example.com/article):
Mcp error: 0: Unknown resource:
https://example.com/article

Expected behavior

For a tool-only MCP server, an empty resource list should not cause Codex to attempt resources/read with an arbitrary HTTPS URL.

If the server exposes a tool such as:

fetch_url(url: string, timeout?: number)

Codex should prefer invoking that tool for arbitrary URL fetching.

Actual behavior

Codex probes MCP resources:

list_mcp_resources -> []

Then attempts:

read_mcp_resource(uri="https://example.com/article")

This fails because arbitrary HTTPS URLs are not MCP resources for this server.

Why this matters

According to the MCP model:

  • tools are model-callable actions
  • resources are application-provided context objects with known URIs/templates
  • arbitrary web URL fetching is usually better represented as a tool, not as a resource

A tool-only server returning resources=[] is valid and should not imply that arbitrary URLs can be passed to resources/read.

Workaround that helped

I made two changes in my MCP server.

1. Add explicit tool descriptions

For the fetch tool, I added a strong description:

@mcp.tool()
async def fetch_url(url: str, timeout: int = 20) -> FetchResponse:
    """Fetch arbitrary HTTP/HTTPS URLs and extract readable content.

    Use this tool to read web pages returned by search tools. Do not use
    resources/read for ordinary URLs. This server does not expose arbitrary
    HTTPS URLs as MCP resources; resources are only a compatibility fallback
    for fetch://{encoded_url}.
    """

I also added similar descriptions to other tools to clarify that they are tool-based actions, not resources.

2. Add a narrow resource-template fallback

I kept fetch_url(url, timeout) as the primary interface, but added a compatibility resource template:

fetch://{encoded_url}

Example:

fetch://https%3A%2F%2Fexample.com%2Farticle

Implementation:

from urllib.parse import unquote

@mcp.resource("fetch://{encoded_url}")
async def fetch_url_resource(encoded_url: str) -> str:
    """Compatibility resource for clients that try resources/read before tools."""
    url = unquote(encoded_url)
    response = await _fetch_url(url=url)
    return response.model_dump_json()

This gives clients a valid resource template if they insist on trying resources, but keeps arbitrary HTTPS URLs as tool inputs rather than pretending they are native MCP resources.

Suggested Codex behavior

When an MCP server has tools but returns no resources:

  1. Do not call resources/read with arbitrary URLs unless a matching resource template exists.
  2. Prefer tool invocation when a relevant tool is available, such as fetch_url.
  3. Treat resources=[] as normal for tool-only servers.
  4. Optionally surface a hint that the server is tool-only.

Environment

  • Codex CLI
  • Local model provider through LM Studio
  • MCP server using stdio transport
  • MCP server exposes tools, no arbitrary HTTPS resources

View original on GitHub ↗