From 44a4fefb5e226d2ac23195b43f9a85fe50cced09 Mon Sep 17 00:00:00 2001 From: Vidit Patankar Date: Mon, 1 Jun 2026 10:58:10 +0530 Subject: [PATCH] Fix ResourceTemplate.matches not escaping literal regex metacharacters 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 --- .../server/mcpserver/resources/templates.py | 13 ++++++-- .../resources/test_resource_template.py | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/mcpserver/resources/templates.py b/src/mcp/server/mcpserver/resources/templates.py index f1ee29a37f..859b508b8f 100644 --- a/src/mcp/server/mcpserver/resources/templates.py +++ b/src/mcp/server/mcpserver/resources/templates.py @@ -89,8 +89,17 @@ def matches(self, uri: str) -> dict[str, Any] | None: Extracted parameters are URL-decoded to handle percent-encoded characters. """ - # Convert template to regex pattern - pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)") + # Convert template to regex pattern. Literal portions of the template are + # escaped so that regex metacharacters (e.g. ".", "+") are matched literally, + # while "{param}" placeholders become named capture groups. Without escaping, + # a template like "api://v1.0/{x}" would treat "." as "any character" and + # wrongly match "api://v1X0/...". + 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) match = re.match(f"^{pattern}$", uri) if match: # URL-decode all extracted parameter values diff --git a/tests/server/mcpserver/resources/test_resource_template.py b/tests/server/mcpserver/resources/test_resource_template.py index 2a7ba8d503..3a91e1e847 100644 --- a/tests/server/mcpserver/resources/test_resource_template.py +++ b/tests/server/mcpserver/resources/test_resource_template.py @@ -49,6 +49,36 @@ def my_func(key: str, value: int) -> dict[str, Any]: # pragma: no cover assert template.matches("test://foo") is None assert template.matches("other://foo/123") is None + def test_template_matches_escapes_literal_regex_metacharacters(self): + """Literal regex metacharacters in the template must be matched literally. + + Without escaping, "." would match any character and "+" would act as a + quantifier, causing both false positives and false negatives. + """ + + def my_func(version: str) -> dict[str, Any]: # pragma: no cover + return {"version": version} + + # A "." in the literal portion must match a literal dot, not any character. + template = ResourceTemplate.from_function( + fn=my_func, + uri_template="api://v1.0/{version}", + name="test", + ) + # Exact literal matches and extracts the parameter. + assert template.matches("api://v1.0/abc") == {"version": "abc"} + # A different character where the literal dot is must NOT match. + assert template.matches("api://v1X0/abc") is None + + # A "+" in the literal portion must match a literal plus, not act as a quantifier. + plus_template = ResourceTemplate.from_function( + fn=my_func, + uri_template="res://a+b/{version}", + name="test", + ) + assert plus_template.matches("res://a+b/x") == {"version": "x"} + assert plus_template.matches("res://aaab/x") is None + @pytest.mark.anyio async def test_create_resource(self): """Test creating a resource from a template."""