diff --git a/.changeset/uri-template-fragment-delimiter.md b/.changeset/uri-template-fragment-delimiter.md new file mode 100644 index 0000000000..4462d01bce --- /dev/null +++ b/.changeset/uri-template-fragment-delimiter.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/core': patch +--- + +Match URI template fragment expressions with their required `#` delimiter outside the captured variable, including empty fragment values. diff --git a/packages/core-internal/src/shared/uriTemplate.ts b/packages/core-internal/src/shared/uriTemplate.ts index 5ffe213acd..820145d049 100644 --- a/packages/core-internal/src/shared/uriTemplate.ts +++ b/packages/core-internal/src/shared/uriTemplate.ts @@ -230,11 +230,14 @@ export class UriTemplate { pattern = part.exploded ? '([^/,]+(?:,[^/,]+)*)' : '([^/,]+)'; break; } - case '+': - case '#': { + case '+': { pattern = '(.+)'; break; } + case '#': { + pattern = '#(.*)'; + break; + } case '.': { pattern = String.raw`\.([^/,]+)`; break; diff --git a/packages/core-internal/test/shared/uriTemplate.test.ts b/packages/core-internal/test/shared/uriTemplate.test.ts index bfc3237872..81412bac49 100644 --- a/packages/core-internal/test/shared/uriTemplate.test.ts +++ b/packages/core-internal/test/shared/uriTemplate.test.ts @@ -51,6 +51,21 @@ describe('UriTemplate', () => { expect(template.expand({ var: '/test' })).toBe('X#/test'); expect(template.variableNames).toEqual(['var']); }); + + it.each(['intro', '/chapter?view=full', ''])('should match fragment value %j without its delimiter', section => { + const template = new UriTemplate('file:///doc{#section}'); + expect(template.match(template.expand({ section }))).toEqual({ section }); + }); + + it('should reject a fragment value without the fragment delimiter', () => { + const template = new UriTemplate('file:///doc{#section}'); + expect(template.match('file:///docintro')).toBeNull(); + }); + + it('should preserve reserved expansion matching without a fragment delimiter', () => { + const template = new UriTemplate('file:///doc{+section}'); + expect(template.match('file:///docintro')).toEqual({ section: 'intro' }); + }); }); describe('label expansion', () => {