Skip to content

Commit 2f19cb5

Browse files
committed
Match URI template literals literally in ResourceTemplate.matches
The template-to-regex conversion only substituted the `{param}` braces and left the surrounding literal text unescaped, so any regex metacharacter in a template (`.`, `+`, `(`, ...) was interpreted as a pattern rather than a literal. A template like `resource://a.b+c/{id}` would also match `resource://aXbYc/...`. Escape the template with `re.escape` before substituting placeholders so literal characters match literally; only `{param}` segments become capture groups.
1 parent e9cd169 commit 2f19cb5

2 files changed

Lines changed: 24 additions & 2 deletions

File tree

src/mcp/server/mcpserver/resources/templates.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,10 @@ def matches(self, uri: str) -> dict[str, Any] | None:
9393
9494
Extracted parameters are URL-decoded to handle percent-encoded characters.
9595
"""
96-
# Convert template to regex pattern
97-
pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)")
96+
# Escape the template so literal text matches literally, then turn {param}
97+
# placeholders into capture groups. re.escape may backslash the braces, so
98+
# the placeholder pattern tolerates an optional backslash on each side.
99+
pattern = re.sub(r"\\?\{(\w+)\\?\}", r"(?P<\1>[^/]+)", re.escape(self.uri_template))
98100
match = re.match(f"^{pattern}$", uri)
99101
if match:
100102
# URL-decode all extracted parameter values

tests/server/mcpserver/resources/test_resource_template.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,3 +331,23 @@ def blocking_fn(name: str) -> str:
331331
assert isinstance(resource, FunctionResource)
332332
assert await resource.read() == "hello world"
333333
assert fn_thread[0] != main_thread
334+
335+
336+
def test_matches_treats_template_specials_literally():
337+
def fn(id: str) -> str: # pragma: no cover
338+
return id
339+
340+
template = ResourceTemplate.from_function(fn=fn, uri_template="resource://a.b+c/{id}", name="t")
341+
342+
assert template.matches("resource://a.b+c/42") == {"id": "42"}
343+
assert template.matches("resource://aXbYc/42") is None
344+
345+
346+
def test_matches_does_not_interpret_template_as_regex():
347+
def fn(id: str) -> str: # pragma: no cover
348+
return id
349+
350+
template = ResourceTemplate.from_function(fn=fn, uri_template="resource://(.*)/{id}", name="t")
351+
352+
assert template.matches("resource://(.*)/7") == {"id": "7"}
353+
assert template.matches("resource://anything/7") is None

0 commit comments

Comments
 (0)