From ac4d4dffdbd12d99d1b11b34ad4566cf453d54ea Mon Sep 17 00:00:00 2001 From: Sravan Vadla <44364763+sravan-v@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:25:29 -0700 Subject: [PATCH] fix(core): match URI template fragment delimiters correctly --- .changeset/uri-template-fragment-delimiter.md | 5 +++++ packages/core-internal/src/shared/uriTemplate.ts | 7 +++++-- .../core-internal/test/shared/uriTemplate.test.ts | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 .changeset/uri-template-fragment-delimiter.md 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', () => {