You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
UriTemplate.match() cannot read back prefix{.ext} when ext holds a dot, though nothing else can claim it #3693
A resource registered as backup://nightly{.ext} cannot be read when the extension holds a dot. UriTemplate.expand({"ext": "tar.gz"}) gives backup://nightly.tar.gz, because . is unreserved. match() on that same string returns None, and resources/read answers "Unknown resource" for a URI the template itself produced.
The match() docstring lists this exact case as an inherent RFC 6570 §1.4 limitation. I think that overstates it. The ambiguity needs a second capture that could claim the dot, as in {name}{.ext}. When {.ext} is the first capture after a literal, the anchored branch of _scan_suffix knows where the value starts. Nothing else can take that dot. A client that reads the template from resources/templates/list also cannot know it must send %2E.
The fix bounds a non-explode {.var} in that branch by /, ? and # alone. The docstring sentence would then cover only the case with a capture on the left, which stays unmatched. With the change, tests/shared/test_uri_template.py plus tests/server/mcpserver go from 949 passed on c9356a2 to 950. The new case is ("file{.ext}", {"ext": "tar.gz"}) in test_roundtrip_expand_then_match, and it fails without the change.
$ .venv/bin/python -I repro_dot.py
expand: backup://nightly.tar.gz
backup://nightly.zip -> archive in zip
backup://nightly.tar.gz -> MCPError Unknown resource: backup://nightly.tar.gz
# not run: the full test suite, only the two test paths named above
Python & MCP Python SDK
Python 3.14.7
mcp at main c9356a2; uri_template.py is unchanged since v2.3.0
@maxisbey, this is the template module from #2356. As the reporter I would like to fix it, and the change is ready. Would you narrow the limitation to the anchored case?
The scoping in your fix looks right, with two boundary cases worth pinning before it lands:
Make the new bound expression-specific, not just explode-specific. {.var} (label) and {/var} (path segment) can both bound on '/', '?' and '#', but {+var} (reserved) must not gain the same restriction, since reserved expansion legitimately carries '/', '?' and '#' inside the value. If _scan_suffix keys only on the explode flag, a shared branch will over-restrict reserved expansions the first time someone reads the diff.
Round-trip as a property, not a case. The one added test pins file{.ext} with a dot; the class is "expand then match returns the original variables" for every expression form and a value set that includes: empty string, embedded dots, characters equal to the form's own separator ('.' for label, '/' for path), percent-encoded octets, and a value that would be empty. A small parametrized cartesian of (expression form, value) locks the class and would have caught the docstring-level ambiguity you are removing. Note the %2E side: expand does not encode '.', so match must accept the literal form; if a future change makes expand encode it, match should accept both.
Overlap precedence. Once the anchored branch matches greedily, two registered templates can both match the same URI (file{.ext} versus file{.name}{.ext} on tar.gz, or file{a} versus file{.b}). Define the resolution order - first registered wins, or longest literal prefix wins - and add a test with two overlapping templates, because otherwise the result depends on registration order and read_resource silently answers from whichever the manager tries first.
That last one is also the note for the security-minded reader: if a server uses matched values for access decisions, match() becoming more permissive widens the accepted URI surface, so the precedence rule belongs in the same paragraph as the fix.
One cost I left out of the report. With the anchored branch bounded by /?#, file{.ext} matches file.tar.gz, which file{+path} already matches, so a server registering both resolves by registration order at resource_manager.py:119. That overlap exists on 2af9e30 today for a single dot:
@maxisbey, you wrote that limitation in #2356. I'd narrow the docstring limitation to the unanchored case, because this widens an overlap that already exists and {name}{.ext} stays unmatched. Would you take that, or keep the docstring as written? @Kludex, copying you since you're merging this week.
Activity
Ben-Home commentedon Oct 13, 2026
The scoping in your fix looks right, with two boundary cases worth pinning before it lands:
Make the new bound expression-specific, not just explode-specific.
{.var}(label) and{/var}(path segment) can both bound on '/', '?' and '#', but{+var}(reserved) must not gain the same restriction, since reserved expansion legitimately carries '/', '?' and '#' inside the value. If _scan_suffix keys only on the explode flag, a shared branch will over-restrict reserved expansions the first time someone reads the diff.Round-trip as a property, not a case. The one added test pins file{.ext} with a dot; the class is "expand then match returns the original variables" for every expression form and a value set that includes: empty string, embedded dots, characters equal to the form's own separator ('.' for label, '/' for path), percent-encoded octets, and a value that would be empty. A small parametrized cartesian of (expression form, value) locks the class and would have caught the docstring-level ambiguity you are removing. Note the %2E side: expand does not encode '.', so match must accept the literal form; if a future change makes expand encode it, match should accept both.
Overlap precedence. Once the anchored branch matches greedily, two registered templates can both match the same URI (file{.ext} versus file{.name}{.ext} on tar.gz, or file{a} versus file{.b}). Define the resolution order - first registered wins, or longest literal prefix wins - and add a test with two overlapping templates, because otherwise the result depends on registration order and read_resource silently answers from whichever the manager tries first.
That last one is also the note for the security-minded reader: if a server uses matched values for access decisions, match() becoming more permissive widens the accepted URI surface, so the precedence rule belongs in the same paragraph as the fix.
chrikrah commentedon Oct 20, 2026
One cost I left out of the report. With the anchored branch bounded by
/?#,file{.ext}matchesfile.tar.gz, whichfile{+path}already matches, so a server registering both resolves by registration order atresource_manager.py:119. That overlap exists on2af9e30today for a single dot:@maxisbey, you wrote that limitation in #2356. I'd narrow the docstring limitation to the unanchored case, because this widens an overlap that already exists and
{name}{.ext}stays unmatched. Would you take that, or keep the docstring as written? @Kludex, copying you since you're merging this week.