From 1361be7a336e478e5d565ea8e2f8e7d0186d0e55 Mon Sep 17 00:00:00 2001 From: Nishchay Mahor Date: Thu, 28 May 2026 13:03:37 -0700 Subject: [PATCH 1/2] fix(core): match multi-variable URI templates like `{a,b}` (#2166) `UriTemplate.expand` already joins multi-name expansions with commas, but `match` only emitted one regex capture per part and assigned the whole captured run to the first name. Anything past the first variable silently never matched and resources registered against templates like `data://users/{userId,format}` were unreachable. Emit one capture per name with literal commas between them in `partToRegExp`, mirroring what `expandPart` produces, so round tripping through expand and match recovers the original variables. Path / label / fragment operators get their existing literal prefix on the first capture; the bare and reserved cases just sit at the current position. --- packages/core/src/shared/uriTemplate.ts | 35 +++++++++++++++++++ packages/core/test/shared/uriTemplate.test.ts | 19 ++++++++++ 2 files changed, 54 insertions(+) diff --git a/packages/core/src/shared/uriTemplate.ts b/packages/core/src/shared/uriTemplate.ts index 5ffe213acd..1ef335f258 100644 --- a/packages/core/src/shared/uriTemplate.ts +++ b/packages/core/src/shared/uriTemplate.ts @@ -222,6 +222,41 @@ export class UriTemplate { return patterns; } + // Multi-variable expressions like `{a,b}` or `{/a,b}` expand each value + // and join them with commas (see `expandPart`). Mirror that on the way + // back by emitting one capture per name with literal commas between + // them. Without this, only the first name was assigned and the rest of + // the path silently never matched (#2166). + if (part.names.length > 1) { + let firstPrefix: string; + switch (part.operator) { + case '/': { + firstPrefix = '/'; + break; + } + case '.': { + firstPrefix = String.raw`\.`; + break; + } + case '#': { + firstPrefix = '#'; + break; + } + default: { + firstPrefix = ''; + } + } + for (let i = 0; i < part.names.length; i++) { + const name = part.names[i]!; + const prefix = i === 0 ? firstPrefix : ','; + patterns.push({ + pattern: prefix + '([^/,]+)', + name + }); + } + return patterns; + } + let pattern: string; const name = part.name; diff --git a/packages/core/test/shared/uriTemplate.test.ts b/packages/core/test/shared/uriTemplate.test.ts index 3954901c4f..c6df82e393 100644 --- a/packages/core/test/shared/uriTemplate.test.ts +++ b/packages/core/test/shared/uriTemplate.test.ts @@ -98,6 +98,25 @@ describe('UriTemplate', () => { expect(match).toEqual({ username: 'fred', postId: '123' }); }); + it('should match a comma-separated multi-variable expression (#2166)', () => { + const template = new UriTemplate('/users/{userId,format}'); + const match = template.match('/users/42,json'); + expect(match).toEqual({ userId: '42', format: 'json' }); + }); + + it('should round-trip expand and match for a multi-variable expression (#2166)', () => { + const template = new UriTemplate('data://users/{userId,format}'); + const expanded = template.expand({ userId: '42', format: 'json' }); + expect(expanded).toBe('data://users/42,json'); + expect(template.match(expanded)).toEqual({ userId: '42', format: 'json' }); + }); + + it('should match a multi-variable expression with the path operator (#2166)', () => { + const template = new UriTemplate('{/userId,format}'); + const match = template.match('/42,json'); + expect(match).toEqual({ userId: '42', format: 'json' }); + }); + it('should return null for non-matching URIs', () => { const template = new UriTemplate('/users/{username}'); const match = template.match('/posts/123'); From 17e64350bacecd8c39bdfa901431574cfa64051e Mon Sep 17 00:00:00 2001 From: Nishchay Mahor Date: Thu, 28 May 2026 14:25:45 -0700 Subject: [PATCH 2/2] chore: add changeset for uri template multi-variable fix --- .changeset/uri-template-multi-variable-match.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/uri-template-multi-variable-match.md diff --git a/.changeset/uri-template-multi-variable-match.md b/.changeset/uri-template-multi-variable-match.md new file mode 100644 index 0000000000..e0d61d541a --- /dev/null +++ b/.changeset/uri-template-multi-variable-match.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/core": patch +--- + +fix(core): match multi-variable URI templates like `{a,b}` (#2166)