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
Copy link
Copy Markdown

@vidigoat vidigoat commented Jun 1, 2026

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
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.

1 participant