You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
UriTemplate.match() misparses {;var} when following literal begins with '=' #3852
UriTemplate.match() breaks match(expand(variables)) == variables when a {;var} path-style parameter expression (ifemp = True, RFC 6570 §3.2.7) is immediately followed by a literal that begins with "=" (for example "{;a}=end{+rest}" or "/api{;mode}=fast{/id}").
In src/mcp/shared/uri_template.py (_scan_prefix), the atom.ifemp check runs:
Checking uri.startswith(nxt.text, pos) unconditionally before uri[pos] == "=" handles the empty-value case (a == "" -> ";a=end/x"), except when a equals the stem of nxt.text (a == "end" -> expand() emits ";a=end=end/x"). At pos right after ";a", uri[pos:] is "=end=end/x", which also starts with nxt.text ("=end"). _scan_prefix prematurely treats a as "" and leaves the second "=end" to be absorbed by a downstream greedy variable {+rest} (or fails suffix matching when no greedy variable follows).
I'd like to take this issue and ask to be assigned.
UriTemplate.match() currently misparses {;var} when the following literal starts with =, because the empty-value check in _scan_prefix can treat that literal as an empty path-style value. I plan to fix that check in src/mcp/shared/uri_template.py and add tests so match(expand(variables)) == variables holds for this case (including examples like {;a}=end{+rest}).
Please assign this issue to me. Once it is assigned, I will open a PR with Fixes #3852, as described in the contributing guide (external PRs are reviewed when assigned or labeled help wanted).
/api{;mode}=fast{/id} with mode=fast -> {'mode': 'fast', 'id': '7'}. Correct, and it is correct for slow too.
{;a}=end{+rest} with a=x -> correct.
{;a}=end/z, {;a}=end, {;a}=end{#f}, {;a}=end{?q} -> all correct.
So the second example in the report does not reproduce, and the reason is in _scan_prefix. After the premature empty capture, the next atom is a literal =fast which consumes the first =fast; the leftover =fast/7 then fails the /id literal, _scan_prefix returns None, and the caller retries with a different suffix split, which recovers the right parse. A reserved expansion absorbs whatever is left instead of failing, so no retry happens and the wrong capture survives. That makes the class: an ifemp capture followed by a literal, where the value equals the literal's stem and the remainder is absorbable downstream.
On the fix itself, reordering the two checks moves the bug rather than removing it. ;a=end=end/x genuinely has two readings: a empty followed by the literal =end, or a equal to end. Checking uri[pos] == '=' first breaks the case of an empty value followed by a literal that begins with =. What the scanner needs is ordered alternatives with a fallback: try the = reading, and if the rest of the atom sequence fails, retry that position with the empty-value reading. That keeps expand(match(x)) == x without needing a heuristic about which reading looks more likely.
A property test over a small template corpus would pin all of this better than individual cases: for each template and value set, assert match(expand(v)) == v, plus a round-trip on the string. The suite appears to cover the empty-value form of ; and the plain {;id} case, but not the stem collision, and the corpus would have caught it because the template and the value only collide for one value.
For what it is worth, this matters in practice for servers that build path-style templates around a mode or namespace segment, where the value and the literal after it can repeat.
Activity
ZYZ666-RGB commentedon Nov 13, 2026
I'd like to take this issue and ask to be assigned.
UriTemplate.match()currently misparses{;var}when the following literal starts with=, because the empty-value check in_scan_prefixcan treat that literal as an empty path-style value. I plan to fix that check insrc/mcp/shared/uri_template.pyand add tests somatch(expand(variables)) == variablesholds for this case (including examples like{;a}=end{+rest}).Please assign this issue to me. Once it is assigned, I will open a PR with
Fixes #3852, as described in the contributing guide (external PRs are reviewed when assigned or labeled help wanted).media-ctrl commentedon Nov 14, 2026
Reproduced at 77001c9, and the failing set is narrower than the report suggests, which helps whoever fixes it.
Measured on that tree, expanding and then matching:
{;a}=end{+rest}witha=end, rest=/x->;a=end=end/x->{'a': '', 'rest': '=end/x'}. Broken./api{;mode}=fast{/id}withmode=fast->{'mode': 'fast', 'id': '7'}. Correct, and it is correct forslowtoo.{;a}=end{+rest}witha=x-> correct.{;a}=end/z,{;a}=end,{;a}=end{#f},{;a}=end{?q}-> all correct.So the second example in the report does not reproduce, and the reason is in
_scan_prefix. After the premature empty capture, the next atom is a literal=fastwhich consumes the first=fast; the leftover =fast/7 then fails the /id literal,_scan_prefixreturns None, and the caller retries with a different suffix split, which recovers the right parse. A reserved expansion absorbs whatever is left instead of failing, so no retry happens and the wrong capture survives. That makes the class: anifempcapture followed by a literal, where the value equals the literal's stem and the remainder is absorbable downstream.On the fix itself, reordering the two checks moves the bug rather than removing it.
;a=end=end/xgenuinely has two readings:aempty followed by the literal=end, oraequal toend. Checkinguri[pos] == '='first breaks the case of an empty value followed by a literal that begins with=. What the scanner needs is ordered alternatives with a fallback: try the=reading, and if the rest of the atom sequence fails, retry that position with the empty-value reading. That keepsexpand(match(x)) == xwithout needing a heuristic about which reading looks more likely.A property test over a small template corpus would pin all of this better than individual cases: for each template and value set, assert
match(expand(v)) == v, plus a round-trip on the string. The suite appears to cover the empty-value form of;and the plain{;id}case, but not the stem collision, and the corpus would have caught it because the template and the value only collide for one value.For what it is worth, this matters in practice for servers that build path-style templates around a mode or namespace segment, where the value and the literal after it can repeat.