Skip to content

fix(core-internal): honour the RFC 6570 explode modifier in expand and match - #2892

Open
feiiiiii5 wants to merge 2 commits into
modelcontextprotocol:mainfrom
feiiiiii5:fix/uritemplate-honour-explode-modifier
Open

feiiiiii5 wants to merge 2 commits into
modelcontextprotocol:mainfrom
feiiiiii5:fix/uritemplate-honour-explode-modifier

Conversation

@feiiiiii5

Copy link
Copy Markdown

Motivation and Context

UriTemplate parses the RFC 6570 explode modifier (*) into part.exploded, and match() reads it — but expand() never did, and match() built the wrong pattern for an exploded path part. The result is that this SDK advertises a template in resources/templates/list that it cannot read back itself:

const template = new UriTemplate('db://{/path*}');
template.expand({ path: ['users', 'alice'] }); // 'db:///users/alice'
template.match('db:///users/alice');             // null

That null is what McpServer turns into ResourceNotFoundError on resources/read, so a client that expands the advertised template correctly is told the resource does not exist.

expression RFC 6570 before
{?keys*} ?keys=semi&keys=dot&keys=comma ?keys=semi,dot,comma
{&keys*} &keys=semi&keys=dot &keys=semi,dot
{/list} /red,green,blue /red/green/blue

{/list*} and {keys*} happened to be right, because for those two styles the exploded form is what the non-exploded code already produced. match() had the same gap from the other side: it built ([^/,]+(?:,[^/,]+)*) for {/list*}, matching the comma form that expand() never produced. Both sides now follow the RFC, and agree with each other:

db://{/path*}  -> db:///users/alice      match: { path: [ 'users', 'alice' ] }
db://{/path}   -> db:///users,alice      match: { path: 'users,alice' }
{/list*}       -> /red/green/blue        match: { list: [ 'red', 'green', 'blue' ] }
{/list}       -> /red,green,blue        match: { list: 'red,green,blue' }

Two existing tests pinned the non-conformant output — {/list*} matching the comma form, and {?tags*} expanding to ?tags=a,b,c. Both are updated with the RFC section they contradict in a comment, and the plain {?tags} form is now pinned separately so it stays covered.

How Has This Been Tested?

  • vitest run in core-internal: 1467 passed. The five tests covering this (two corrected, three new) fail on main without the change.
  • vitest run in server, which routes resources/read through match: 525 passed.
  • tsc --noEmit in core-internal clean; prettier --check clean on both files; the pre-push hook ran typecheck, build and lint across the workspace.

Breaking Changes

None to the API. expand() output changes for the three non-conformant forms above, which is the point: any client that expanded {?tags*} per RFC already disagreed with what the server matched. A client relying on the old comma form for an exploded query will now see repeated names; that combination was never matchable by this SDK.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Tests added or updated
  • Documentation updated

Checklist

  • Tests added or updated for the behaviour change.
  • Changeset added (patch).
  • Lint, format and typecheck pass locally, plus the package test suites named above.
  • Documentation updated (none of the prose described the old expansion).

Additional context

Three open PRs touch the same file for a different reason each: #2216 (multi-variable path expressions in match) and #2732 / #2810 (percent-decoding the values match extracts). This one is the explode modifier only, so expect a textual conflict in partToRegExp with those, not a semantic overlap.

Matching behaviour in general is the subject of #1079. This change only makes * mean what RFC 6570 says and leaves that issue's questions alone: a query-only template ({?keys*}) still does not match its own expansion, because match() treats the input as a URI path — that is a separate question about what a template may match.

…d match

parse() read the `*` modifier into part.exploded and match() used it,
but expand() never looked at it: `{?keys*}` and `{&keys*}` joined the
values with commas instead of repeating the name, and `{/list}` put one
path segment per value instead of joining them. match() had the same
gap from the other side, building the comma form for an exploded path
part, so a template this SDK advertises in `resources/templates/list`
could not read back its own expansion:

    new UriTemplate('db://{/path*}').expand({ path: ['users', 'alice'] })
    // 'db:///users/alice'
    new UriTemplate('db://{/path*}').match('db:///users/alice')
    // null -> ResourceNotFoundError on resources/read

Both sides now follow RFC 6570: an exploded path part spans segments and
splits back into a list, an exploded query repeats the name, and a plain
part keeps the comma-joined value (matched as one segment, commas
included). Two existing tests pinned the non-conformant output and are
updated with the section they contradict.
@feiiiiii5
feiiiiii5 requested a review from a team as a code owner September 29, 2026 13:07
@changeset-bot

changeset-bot Bot commented Sep 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4d346a6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/core Patch
@modelcontextprotocol/client Patch
@modelcontextprotocol/core-internal Patch
@modelcontextprotocol/server-legacy Patch
@modelcontextprotocol/server Patch
@modelcontextprotocol/codemod Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Sep 29, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2892

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2892

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2892

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2892

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2892

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2892

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2892

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2892

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2892

commit: 4d346a6

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant