fix(core-internal): percent-decode values UriTemplate.match() extracts by sanjibani · Pull Request #2810 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content

fix(core-internal): percent-decode values UriTemplate.match() extracts - #2810

Open
sanjibani wants to merge 1 commit into
modelcontextprotocol:mainfrom
sanjibani:fix/issue-2728-uri-template-match-decode
Open

sanjibani wants to merge 1 commit into
modelcontextprotocol:mainfrom
sanjibani:fix/issue-2728-uri-template-match-decode

Conversation

@sanjibani

Copy link
Copy Markdown
Contributor

Closes #2728.

What

UriTemplate.match() returned the still-percent-encoded substring for every captured value, so a handler routed through ResourceTemplate received the wrong value for any resource whose template variable contains a reserved or non-ASCII character.

The encoder that expand() applied depends on the operator:

  • + and # use encodeURI, which leaves reserved characters such as /, :, and ? alone.
  • Every other operator uses encodeURIComponent.

match() now applies the matching inverse per element:

  • decodeURIComponent for the default, ., /, ?, and & operators.
  • decodeURI for + and #.

Each element of an exploded array is decoded independently. A malformed escape sequence (e.g. %ZZ) is returned unchanged rather than thrown, so a value that was already raw on the wire does not raise out of match().

Tests

  • 9 new cases under describe('percent-decoding on match') cover every operator that expand() produces, plus the malformed-escape pass-through.
  • The default-operator matrix exercises spaces, /, ?, &, =, %, and a non-ASCII character (matching the differential the issue reporter surfaced: 147/200 round-trip cases fail on main).
  • Full suite: pnpm --filter @modelcontextprotocol/core-internal test is green (1466/1466), and the server and client packages still pass.

Out of scope

The reporter's matrix also flagged two pre-existing regex bugs that this PR deliberately does not touch:

  • The # operator's regex is (.+) rather than #(.+), so the # prefix ends up captured as part of the value. A separate PR can lift # to a literal prefix in partToRegExp.
  • The / operator's exploded regex uses [^/,]+(?:,[^/,]+)*, but expand() joins exploded / values with /, not ,. Same idea: separate PR, separate change.

Both round-trip tests for those operators fail on main regardless of percent-decoding, and the percent-decoding fix does not regress them.

Checklist

  • Tests added for the bug
  • All checks pass locally (pnpm --filter @modelcontextprotocol/core-internal test)
  • Lint and format clean (pnpm --filter @modelcontextprotocol/core-internal lint)
  • Changeset added (@modelcontextprotocol/core-internal patch)
  • No em-dashes anywhere in the diff

modelcontextprotocol#2728)

match() previously returned the still-encoded substring, so a handler
routed through ResourceTemplate received the wrong value for any
resource whose template variable contains a reserved or non-ASCII
character.

The encoder that expand() applied depends on the operator: + and #
use encodeURI; every other operator uses encodeURIComponent. match()
now applies the matching inverse (decodeURI / decodeURIComponent)
per element, including for each entry of an exploded array, and
passes malformed escapes through unchanged rather than throwing.
@sanjibani
sanjibani requested a review from a team as a code owner September 13, 2026 17:33
@changeset-bot

changeset-bot Bot commented Sep 13, 2026

Copy link
Copy Markdown

@pkg-pr-new

pkg-pr-new Bot commented Sep 13, 2026

Copy link
Copy Markdown

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.

UriTemplate.match() does not percent-decode extracted values

1 participant