Skip to content

UriTemplate.match() misparses {;var} when following literal begins with '=' #3852

Open

Description

@xgbtxy

Initial Checks

Release line

2.x (current stable)

Description

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:

if uri.startswith(nxt.text, pos):
    result[var.name] = ""
    continue
if pos < limit and uri[pos] == "=":
    pos += 1

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).

Example Code

from mcp.shared.uri_template import UriTemplate

tpl = UriTemplate.parse("{;a}=end{+rest}")
variables = {"a": "end", "rest": "/x"}

expanded = tpl.expand(variables)
matched = tpl.match(expanded)

print("expanded:", expanded)  # ';a=end=end/x'
print("matched :", matched)   # {'a': '', 'rest': '=end/x'}  (expected {'a': 'end', 'rest': '/x'})
assert matched == variables

Python & MCP Python SDK

  • mcp: 2.3.0 (also verified on main 77001c96f9 in src/mcp/shared/uri_template.py)
  • Python: 3.12.0

Activity

  1. ZYZ666-RGB commented on Nov 13, 2026

    @ZYZ666-RGB

    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).

  2. added
    bugSomething isn't working
    v2Affects the v2 line (2.x on main)
    on Nov 13, 2026
  3. media-ctrl commented on Nov 14, 2026

    @media-ctrl

    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} with a=end, rest=/x -> ;a=end=end/x -> {'a': '', 'rest': '=end/x'}. Broken.
    • /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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions