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) 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');