Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/mcp/shared/uri_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,18 +210,18 @@ def _encode(value: str, *, allow_reserved: bool) -> str:
followed by two hex digits is still encoded to ``%25``.
"""
if not allow_reserved:
return quote(value, safe="")
return quote(value, safe="", errors="surrogateescape")

# Reserved expansion: walk the string, pass through triplets as-is,
# quote the gaps between them. A bare % with no triplet lands in a
# gap and gets encoded normally.
out: list[str] = []
last = 0
for m in _PCT_TRIPLET_RE.finditer(value):
out.append(quote(value[last : m.start()], safe=_RESERVED))
out.append(quote(value[last : m.start()], safe=_RESERVED, errors="surrogateescape"))
out.append(m.group())
last = m.end()
out.append(quote(value[last:], safe=_RESERVED))
out.append(quote(value[last:], safe=_RESERVED, errors="surrogateescape"))
return "".join(out)


Expand Down Expand Up @@ -604,7 +604,7 @@ def _parse_query(query: str) -> dict[str, str]:
for pair in query.split("&"):
name, _, value = pair.partition("=")
if name and name not in result:
result[name] = unquote(value)
result[name] = unquote(value, errors="surrogateescape")
return result


Expand All @@ -622,7 +622,7 @@ def _extract_greedy(var: Variable, raw: str) -> str | list[str] | None:
if not var.explode:
if any(c in stops for c in raw):
return None
return unquote(raw)
return unquote(raw, errors="surrogateescape")

sep = spec.separator
if not raw:
Expand Down Expand Up @@ -654,7 +654,7 @@ def _extract_greedy(var: Variable, raw: str) -> str | list[str] | None:
seg = ""
else:
return None
segments.append(unquote(seg))
segments.append(unquote(seg, errors="surrogateescape"))
return segments


Expand Down Expand Up @@ -1012,7 +1012,7 @@ def _scan_suffix(
eq = uri.find("=", earliest, pos)
if eq == -1:
return None
result[var.name] = unquote(uri[eq + 1 : pos])
result[var.name] = unquote(uri[eq + 1 : pos], errors="surrogateescape")
pos = eq
i -= 1
continue
Expand Down Expand Up @@ -1045,7 +1045,7 @@ def _scan_suffix(
return None
start = idx + len(prev.text)

result[var.name] = unquote(uri[start:pos])
result[var.name] = unquote(uri[start:pos], errors="surrogateescape")
pos = start
i -= 1
return result, pos
Expand Down Expand Up @@ -1111,6 +1111,6 @@ def _scan_prefix(
if end == -1:
return None

result[var.name] = unquote(uri[pos:end])
result[var.name] = unquote(uri[pos:end], errors="surrogateescape")
pos = end
return result, pos
22 changes: 22 additions & 0 deletions tests/shared/test_uri_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,28 @@ def test_match_decodes_percent_encoding():
assert t.match("file://docs/hello%20world.txt") == {"name": "hello world.txt"}


@pytest.mark.parametrize(
("template", "uri", "expected"),
[
("file://{name}", "file://caf%E9", {"name": "caf\udce9"}),
("file://{+path}", "file://caf%E9", {"path": "caf\udce9"}),
("x{#frag}", "x#caf%E9", {"frag": "caf\udce9"}),
("x{.ext}", "x.caf%E9", {"ext": "caf\udce9"}),
("x{/seg}", "x/caf%E9", {"seg": "caf\udce9"}),
("x{;p}", "x;p=caf%E9", {"p": "caf\udce9"}),
("x{?q}", "x?q=caf%E9", {"q": "caf\udce9"}),
("x{/list*}", "x/a/caf%E9", {"list": ["a", "caf\udce9"]}),
("prefix-{id}-greedy/{+path}", "prefix-caf%E9-greedy/rest", {"id": "caf\udce9", "path": "rest"}),
],
)
def test_match_non_utf8_percent_encoding_surrogateescape(template: str, uri: str, expected: dict[str, str | list[str]]):
t = UriTemplate.parse(template)
matched = t.match(uri)
assert matched == expected
assert t.expand(matched) == uri
assert t.match(t.expand(matched)) == matched


def test_match_escapes_template_literals():
# Regression: previous impl didn't escape . in literals, making it
# a regex wildcard. "fileXtxt" should NOT match "file.txt/{id}".
Expand Down
Loading