From 31eaf72fb4098e8eccd7580d76a4362515fe0409 Mon Sep 17 00:00:00 2001 From: Vidit Patankar Date: Mon, 1 Jun 2026 10:58:10 +0530 Subject: [PATCH] Pin literal regex-metacharacter matching in ResourceTemplate.matches The original fix escaped literal template text before building the matching regex (#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. --- .../resources/test_resource_template.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/server/mcpserver/resources/test_resource_template.py b/tests/server/mcpserver/resources/test_resource_template.py index 42a1099537..df1f52eead 100644 --- a/tests/server/mcpserver/resources/test_resource_template.py +++ b/tests/server/mcpserver/resources/test_resource_template.py @@ -157,6 +157,47 @@ def test_matches_escapes_template_literals(): assert t.matches("data://v1X0/42") is None +def test_matches_literal_dot_does_not_route_to_wrong_template(): + """A "." in a template's literal text matches only a literal dot (#2961). + + The old regex-based matcher let "api://v1.0/{v}" match "api://v1X0/abc", + so an earlier registered template could capture URIs meant for a later one. + """ + t = _make("api://v1.0/{version}") + assert t.matches("api://v1.0/abc") == {"version": "abc"} + assert t.matches("api://v1X0/abc") is None + + +def test_matches_literal_regex_metacharacters_carry_no_regex_meaning(): + """Quantifiers and grouping characters in literal text match only themselves (#2961). + + Under the old regex-based matcher, "+" / "*" / "?" acted as quantifiers and + "(" ")" created groups, producing false negatives on the template's own URIs. + """ + plus = _make("res://a+b/{x}") + assert plus.matches("res://a+b/v") == {"x": "v"} + assert plus.matches("res://aaab/v") is None # "+" is not one-or-more + + star = _make("res://a*b/{x}") + assert star.matches("res://a*b/v") == {"x": "v"} + assert star.matches("res://b/v") is None # "*" is not zero-or-more + + optional = _make("res://a?b/{x}") + assert optional.matches("res://a?b/v") == {"x": "v"} + assert optional.matches("res://ab/v") is None # "?" is not optionality + + parens = _make("res://a(b)c/{x}") + assert parens.matches("res://a(b)c/v") == {"x": "v"} + assert parens.matches("res://abc/v") is None # "(" ")" is not a group + + +def test_matches_template_of_every_regex_metacharacter_matches_itself(): + """A literal run of all the regex metacharacters matches only itself (#2961).""" + t = _make("data:.+*?^$|()[]{name}") + assert t.matches("data:.+*?^$|()[]hello") == {"name": "hello"} + assert t.matches("data:Xhello") is None + + class TestResourceTemplate: """Test ResourceTemplate functionality."""