Skip to content

Commit a8f488e

Browse files
committed
fix: preserve empty list items in explode matching
_extract_path was dropping all empty segments when splitting an explode capture, but only the first empty item comes from the leading operator prefix. Subsequent empties are legitimate values: {/path*} with ['a', '', 'c'] expands to /a//c and must match back to the same list. Split by separator, strip only items[0] if empty, then iterate. The ; operator is unaffected since empty values use the bare-name form which is a non-empty segment.
1 parent dcfd67a commit a8f488e

File tree

2 files changed

+25
-3
lines changed

2 files changed

+25
-3
lines changed

src/mcp/shared/uri_template.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -522,9 +522,14 @@ def _extract_path(m: re.Match[str], variables: Sequence[Variable]) -> dict[str,
522522
continue
523523
segments: list[str] = []
524524
prefix = f"{var.name}="
525-
for seg in raw.split(spec.separator):
526-
if not seg: # leading separator produces an empty first item
527-
continue
525+
# Splitting on the separator yields an empty first item from
526+
# the leading prefix. Strip only that one; subsequent empty
527+
# items are legitimate empty values ({/path*} with ["a","","c"]
528+
# expands to /a//c and must round-trip).
529+
items = raw.split(spec.separator)
530+
if items and not items[0]:
531+
items = items[1:]
532+
for seg in items:
528533
if spec.named:
529534
# Named explode emits name=value per item (or bare
530535
# name for ; with empty value). Validate the name

tests/shared/test_uri_template.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,19 @@ def test_match_adjacent_vars_with_prefix_names():
512512
assert t.match("abcd") == {"var": "abcd", "vara": ""}
513513

514514

515+
def test_match_explode_preserves_empty_list_items():
516+
# Splitting the explode capture on its separator yields a leading
517+
# empty item from the operator prefix; only that one is stripped.
518+
# Subsequent empties are legitimate values from the input list.
519+
t = UriTemplate.parse("{/path*}")
520+
assert t.match("/a//c") == {"path": ["a", "", "c"]}
521+
assert t.match("//a") == {"path": ["", "a"]}
522+
assert t.match("/a/") == {"path": ["a", ""]}
523+
524+
t = UriTemplate.parse("host{.labels*}")
525+
assert t.match("host.a..c") == {"labels": ["a", "", "c"]}
526+
527+
515528
def test_match_adjacent_vars_disambiguated_by_literal():
516529
# A literal between vars resolves the ambiguity.
517530
t = UriTemplate.parse("{a}-{b}")
@@ -609,6 +622,10 @@ def test_match_explode_encoded_separator_in_segment():
609622
("x{name}y", {"name": ""}),
610623
("item{;keys*}", {"keys": ["a", "b", "c"]}),
611624
("item{;keys*}", {"keys": ["a", "", "b"]}),
625+
# Empty strings in explode lists round-trip for unnamed operators
626+
("{/path*}", {"path": ["a", "", "c"]}),
627+
("{/path*}", {"path": ["", "a"]}),
628+
("host{.labels*}", {"labels": ["a", "", "c"]}),
612629
# Partial query expansion round-trips: expand omits undefined
613630
# vars, match leaves them absent from the result.
614631
("logs://{service}{?since,level}", {"service": "api"}),

0 commit comments

Comments
 (0)