Skip to content

A resource template with a non-ASCII or space literal is advertised but can never be read #3526

Description

@LGUIUX

Initial Checks

  • I confirm that I'm using the newest release of my line (main @ 6affe5c)
  • I confirm that I searched for my issue in the issue tracker before opening this issue

Release line

2.x (current stable)

Description

UriTemplate copies literal runs into the expansion verbatim. RFC 6570 §3.1 requires a literal that the URI grammar does not allow (ucschar such as café, or a space) to be pct-encoded as UTF-8 when the template is expanded, so expand() currently returns a string that is not a valid RFC 3986 URI:

UriTemplate.parse("file:///docs/café/{name}").expand({"name": "a.txt"})
# 'file:///docs/café/a.txt'      (uritemplate-test "Literal Encoding" expects caf%C3%A9)

match() is built from the same verbatim literals, so it accepts only that invalid form and rejects the encoded one:

t = UriTemplate.parse("file:///docs/café/{name}")
t.match("file:///docs/caf%C3%A9/a.txt")   # None
t.match("file:///docs/café/a.txt")        # {'name': 'a.txt'}

That second line is the part that bites in practice. A resource URI crosses the wire as a pydantic AnyUrl, which pct-encodes non-ASCII and spaces (AnyUrl('file:///docs/café/a.txt')file:///docs/caf%C3%A9/a.txt). So the encoded form is the only one a server ever sees, and the template can never match it: the resource is listed in resources/templates/list and is permanently unreadable. The same applies to a literal space (file:///my docs/{name}my%20docs).

I hit this while building resource templates whose paths carry Spanish accents, which is ordinary for non-English servers.

Expected: expand() pct-encodes literals per §3.1, and match() accepts the encoded URI a conforming client sends, so an expanded URI round-trips.

Note the direction of the behavior change this implies: after the fix, the raw (unencoded) form stops matching. I believe that is correct, since it is not a valid RFC 3986 URI and it is not what reaches a server over the wire, but it is a visible change for anyone calling UriTemplate.match() directly with a raw IRI, so it seems worth your call rather than mine. Accepting both forms is possible (a fallback scan over the raw atoms) at the cost of a second pass and an ambiguity to document.

I have a patch (the literal encoding applied in both directions, sharing the existing _encode helper) plus unit tests, a routing test, and the official uritemplate-test "Literal Encoding" vector. The full suite passes (5978 passed, 10 skipped, 1 xfailed; baseline 5968), and the new assertions fail on main. Happy to open a PR if you'd like it and can assign this to me.

Example Code

import anyio
from mcp.client.client import Client
from mcp.server.mcpserver import MCPServer

mcp = MCPServer()


@mcp.resource("file:///docs/café/{name}")
def doc(name: str) -> str:
    return f"contents of {name}"


async def main() -> None:
    async with Client(mcp) as client:
        templates = await client.list_resource_templates()
        print(templates.resource_templates[0].uri_template)
        # file:///docs/café/{name}

        # what a conforming client puts on the wire (AnyUrl pct-encodes it)
        await client.read_resource("file:///docs/caf%C3%A9/a.txt")
        # McpError: Unknown resource: file:///docs/caf%C3%A9/a.txt


anyio.run(main)

Python & MCP Python SDK

Python 3.11.3, mcp-python-sdk main @ 6affe5c0d3588fd1705713b3703dc68015cfe3eb

Disclosure: this was found and written with Claude Code. It ran the official uritemplate-test vectors against mcp.shared.uri_template, reduced the failure to this case, and drafted this report and the patch. I reviewed it before posting and can walk through the change.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    v1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions