Skip to content

fix(resources): escape literal regex metacharacters in ResourceTemplate.matches#2749

Open
vidigoat wants to merge 1 commit into
modelcontextprotocol:mainfrom
vidigoat:fix-resource-template-regex-escape
Open

fix(resources): escape literal regex metacharacters in ResourceTemplate.matches#2749
vidigoat wants to merge 1 commit into
modelcontextprotocol:mainfrom
vidigoat:fix-resource-template-regex-escape

Conversation

@vidigoat

@vidigoat vidigoat commented Jun 1, 2026

Copy link
Copy Markdown

Problem

ResourceTemplate.matches() (src/mcp/server/mcpserver/resources/templates.py) builds its matching regex by naive string substitution:

pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)")
match = re.match(f"^{pattern}$", uri)

The literal portions of the template are never re.escape-d, so any regex metacharacter in the template text is interpreted as a regex operator. This causes both:

  • False positivesapi://v1.0/{version} treats . as "any character" and wrongly matches api://v1X0/abc, routing a URI to a template it shouldn't match (returning {'version': 'abc'} instead of None).
  • False negatives — a template with +, (, [, etc. in a literal segment (e.g. res://a+b/{x}) fails to match its own valid URI.

Fix

Tokenize the template into literal/placeholder parts, re.escape the literal text, and turn each {param} into a named capture group:

parts: list[str] = []
for literal, param in re.findall(r"([^{]*)(?:\{(\w+)\})?", self.uri_template):
    parts.append(re.escape(literal))
    if param:
        parts.append(f"(?P<{param}>[^/]+)")
pattern = "".join(parts)

Verification

before:  pytest tests/server/mcpserver/resources/test_resource_template.py -k escapes_literal_regex
         1 failed   (matches('api://v1X0/abc') returned {'version': 'abc'}, expected None)
after :  16 passed  (full test_resource_template.py)
no regressions: tests/server/mcpserver/resources/ -> 60 passed;
                tests/issues/test_129/test_141 resource templates -> green

ruff check and ruff format --check pass on both changed files. Adds a regression test asserting metacharacters in literal segments are matched literally.

@vidigoat vidigoat changed the title Fix ResourceTemplate.matches not escaping literal regex metacharacters fix(resources): escape literal regex metacharacters in ResourceTemplate.matches Jun 1, 2026

@StantonMatt StantonMatt left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this against the resource-manager routing surface, since the bad . behavior can let an earlier template catch a URI intended for a later one. With api://v1.0/{version} registered before api://v1X0/{version}, ResourceManager.get_resource(api://v1X0/abc) now returns the v1X0 template result (x:abc), while api://v1.0/abc still returns the dotted template result (dot:abc).

Local checks:

  • uv run --frozen pytest tests/server/mcpserver/resources/test_resource_template.py tests/issues/test_973_url_decoding.py -q
  • uv run --frozen ruff check src/mcp/server/mcpserver/resources/templates.py tests/server/mcpserver/resources/test_resource_template.py
  • git diff --check origin/main...HEAD

@vidigoat

vidigoat commented Jun 9, 2026

Copy link
Copy Markdown
Author

Gentle nudge on this one whenever a maintainer has a moment. CI is green, and @StantonMatt independently reproduced the template-routing issue and verified the fix against the resource-manager surface. It's a small, self-contained change (escaping literal regex metacharacters in ResourceTemplate.matches) with a regression test. Happy to adjust anything — thanks for your time!

@Robin1987China Robin1987China left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix logic is correct: re.findall splits the template into literal/param pairs, re.escape() on literals avoids metacharacter interpretation. The (\w+) for parameter names properly constrains to valid Python identifiers (matching MCPServer's function parameter constraint).

The test covers . and + — good minimal coverage. Suggestion: consider adding a few more boundary cases for completeness, since the fix changes regex behavior for ALL special characters:

  • * (asterisk) — could match zero-or-more incorrectly
  • ? (question mark) — could act as optional quantifier
  • ( ) (parentheses) — could create false capture groups
  • A full-special-char regression test (e.g., data:.+*?^$|()[]{name}) to validate all metacharacters are escaped

These would prevent regressions if the regex construction is refactored later. Happy to share test cases if helpful.

AI assistance disclosure: Reviewed with AI assistance (opencode). I have verified the fix logic against the original bug report (#2961).

The original fix escaped literal template text before building the matching
regex (modelcontextprotocol#2961: "api://v1.0/{v}" wrongly matched "api://v1X0/abc", and "+"
in a literal segment broke matching of the template's own URIs). main's
RFC 6570 UriTemplate rewrite has since replaced the regex construction and
subsumes the code change, so this is now regression tests only: the dot
mis-routing case, quantifier/grouping metacharacters (+ * ? ( )), and a
template containing every regex metacharacter, as requested in review.
@vidigoat vidigoat force-pushed the fix-resource-template-regex-escape branch from 44a4fef to 31eaf72 Compare July 8, 2026 11:39
@vidigoat

vidigoat commented Jul 8, 2026

Copy link
Copy Markdown
Author

@StantonMatt Rebased onto latest main, and there's a wrinkle worth flagging for the routing scenario you verified: main's RFC 6570 rewrite (mcp.shared.uri_template) replaced the regex construction in ResourceTemplate.matches entirely, and the new matcher compares literal text literally — so api://v1.0/{version} no longer captures api://v1X0/abc even without my code change. I've reduced this PR to the regression tests (as top-level functions, per current test conventions) so that behavior stays pinned if matching is ever refactored again.

@vidigoat

vidigoat commented Jul 8, 2026

Copy link
Copy Markdown
Author

@Robin1987China Added the boundary cases you listed: *, ?, and () each get a literal-match plus a no-regex-meaning assertion, and there's a full run of every metacharacter (data:.+*?^$|()[]{name}) asserting it matches only itself. After rebasing onto main, the RFC 6570 UriTemplate rewrite has replaced the regex construction and handles literals without regex at all, so the code change is subsumed — the PR is now regression tests only, which pins exactly the refactor-safety concern you raised. All pass against the new matcher.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants