Skip to content

Commit ed84090

Browse files
committed
fix: fall back to strict regex when path contains {#...} or literal #
_split_query_tail enabled lenient matching for page{#section}{?q}, but lenient matching's partition('#') stripped the fragment before the path regex (which expects #section) could see it, causing fullmatch to always fail. Extended the path-portion fallback check to also bail on {#...} expressions and literal # characters, mirroring the existing ? check. Such templates are semantically unusual (query-after-fragment is not valid URI structure) but now round-trip correctly via strict regex.
1 parent 7c34c12 commit ed84090

File tree

2 files changed

+10
-5
lines changed

2 files changed

+10
-5
lines changed

src/mcp/shared/uri_template.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -579,14 +579,15 @@ def _split_query_tail(parts: list[_Part]) -> tuple[list[_Part], list[Variable]]:
579579
if first.operator != "?":
580580
return parts, []
581581

582-
# If the path portion contains a literal ? or a {?...} expression,
583-
# the URI's ? split won't align with our template boundary. Fall
584-
# back to strict regex.
582+
# If the path portion contains a literal ?/# or a {?...}/{#...}
583+
# expression, lenient matching's partition("#") then partition("?")
584+
# would strip content the path regex expects to see. Fall back to
585+
# strict regex.
585586
for part in parts[:split]:
586587
if isinstance(part, str):
587-
if "?" in part:
588+
if "?" in part or "#" in part:
588589
return parts, []
589-
elif part.operator == "?":
590+
elif part.operator in ("?", "#"):
590591
return parts, []
591592

592593
query_vars: list[Variable] = []

tests/shared/test_uri_template.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,10 @@ def test_expand_rejects_invalid_value_types(value: object):
452452
("api?x{?page}", "api?x?page=2", {"page": "2"}),
453453
# {?...} expression in path portion also falls through
454454
("api{?q}x{?page}", "api?q=1x?page=2", {"q": "1", "page": "2"}),
455+
# {#...} or literal # in path portion falls through: lenient
456+
# matching would strip the fragment before the path regex sees it
457+
("page{#section}{?q}", "page#intro?q=x", {"section": "intro", "q": "x"}),
458+
("page#lit{?q}", "page#lit?q=x", {"q": "x"}),
455459
# Empty & segments in query are skipped
456460
("search{?q}", "search?&q=hello&", {"q": "hello"}),
457461
# Duplicate query keys keep first value

0 commit comments

Comments
 (0)