Skip to content

Commit 2575042

Browse files
committed
feat: reject duplicate variable names in URI templates
RFC 6570 requires repeated variables to expand to the same value. Enforcing this at match time would require backreferences with potentially exponential cost. We reject at parse time instead, following the recommendation in #697. Previously a template like {x}/{x} would parse and silently return only the last captured value on match.
1 parent a5afb98 commit 2575042

File tree

2 files changed

+33
-1
lines changed

2 files changed

+33
-1
lines changed

src/mcp/shared/uri_template.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,7 @@ def _parse(template: str, *, max_expressions: int) -> tuple[tuple[_Part, ...], t
500500
i = end + 1
501501

502502
_check_adjacent_explodes(template, parts)
503+
_check_duplicate_variables(template, variables)
503504
return tuple(parts), tuple(variables)
504505

505506

@@ -570,6 +571,27 @@ def _parse_expression(template: str, body: str, pos: int) -> _Expression:
570571
return _Expression(operator=operator, variables=tuple(variables))
571572

572573

574+
def _check_duplicate_variables(template: str, variables: list[Variable]) -> None:
575+
"""Reject templates that use the same variable name more than once.
576+
577+
RFC 6570 requires repeated variables to expand to the same value,
578+
which would require backreference matching with potentially
579+
exponential cost. Rather than silently returning only the last
580+
captured value, we reject at parse time.
581+
582+
Raises:
583+
InvalidUriTemplate: If any variable name appears more than once.
584+
"""
585+
seen: set[str] = set()
586+
for var in variables:
587+
if var.name in seen:
588+
raise InvalidUriTemplate(
589+
f"Variable {var.name!r} appears more than once; repeated variables are not supported",
590+
template=template,
591+
)
592+
seen.add(var.name)
593+
594+
573595
def _check_adjacent_explodes(template: str, parts: list[_Part]) -> None:
574596
"""Reject templates with adjacent same-operator explode variables.
575597

tests/shared/test_uri_template.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,15 @@ def test_parse_rejects_adjacent_explodes_same_operator():
121121
UriTemplate.parse("{/a*}{/b*}")
122122

123123

124+
@pytest.mark.parametrize(
125+
"template",
126+
["{x}/{x}", "{x,x}", "{a}{b}{a}", "{+x}/foo/{x}"],
127+
)
128+
def test_parse_rejects_duplicate_variable_names(template: str):
129+
with pytest.raises(InvalidUriTemplate, match="appears more than once"):
130+
UriTemplate.parse(template)
131+
132+
124133
def test_invalid_uri_template_is_value_error():
125134
with pytest.raises(ValueError):
126135
UriTemplate.parse("{}")
@@ -195,7 +204,8 @@ def test_parse_rejects_too_many_expressions():
195204

196205

197206
def test_parse_custom_limits_allow_larger():
198-
tmpl = UriTemplate.parse("{a}" * 20, max_expressions=20)
207+
template = "".join(f"{{v{i}}}" for i in range(20))
208+
tmpl = UriTemplate.parse(template, max_expressions=20)
199209
assert len(tmpl.variables) == 20
200210

201211

0 commit comments

Comments
 (0)