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.

ResourceTemplate.matches() built its regex with a naive string
substitution:

    pattern = self.uri_template.replace('{', '(?P<').replace('}', '>[^/]+)')

The literal portions of the template were never re.escape-d, so regex
metacharacters in the template text were interpreted as operators. A
template like 'api://v1.0/{version}' treated '.' as 'any character' and
wrongly matched 'api://v1X0/abc' (false positive routing a URI to the
wrong template), while a template with '+', '(', '[' etc. in a literal
segment failed to match its own valid URIs (false negative).

Tokenize the template into literal/placeholder parts, re.escape the
literals, and turn '{param}' into named capture groups. Adds a
regression test.

Signed-off-by: Vidit Patankar <vidit.patankar16@gmail.com>
@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).

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