diff --git a/.changeset/decode-uri-template-matches.md b/.changeset/decode-uri-template-matches.md new file mode 100644 index 0000000000..e217cc428c --- /dev/null +++ b/.changeset/decode-uri-template-matches.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/core-internal': patch +--- + +Decode percent-encoded values returned by URI template matching while preserving malformed encodings. diff --git a/packages/core-internal/src/shared/uriTemplate.ts b/packages/core-internal/src/shared/uriTemplate.ts index 5ffe213acd..4594b3f4de 100644 --- a/packages/core-internal/src/shared/uriTemplate.ts +++ b/packages/core-internal/src/shared/uriTemplate.ts @@ -197,6 +197,15 @@ export class UriTemplate { return str.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); } + private decodeValue(value: string): string { + try { + return decodeURIComponent(value); + } catch { + // Preserve malformed percent-encoded values rather than making URI matching throw. + return value; + } + } + private partToRegExp(part: { name: string; operator: string; @@ -282,7 +291,8 @@ export class UriTemplate { const value = match[i + 1]!; const cleanName = name.replace('*', ''); - result[cleanName] = exploded && value.includes(',') ? value.split(',') : value; + result[cleanName] = + exploded && value.includes(',') ? value.split(',').map(value => this.decodeValue(value)) : this.decodeValue(value); } return result; diff --git a/packages/core-internal/test/shared/uriTemplate.test.ts b/packages/core-internal/test/shared/uriTemplate.test.ts index bfc3237872..13c5f2e54d 100644 --- a/packages/core-internal/test/shared/uriTemplate.test.ts +++ b/packages/core-internal/test/shared/uriTemplate.test.ts @@ -109,6 +109,23 @@ describe('UriTemplate', () => { const match = template.match('/red,green,blue'); expect(match).toEqual({ list: ['red', 'green', 'blue'] }); }); + + it('should percent-decode matched values', () => { + const template = new UriTemplate('/users/{username}{?query}{/tags*}'); + const match = template.match('/users/Ada%20Lovelace?query=machine%20learning/r%26d,AI%2FML'); + + expect(match).toEqual({ + username: 'Ada Lovelace', + query: 'machine learning', + tags: ['r&d', 'AI/ML'] + }); + }); + + it('should preserve malformed percent-encoded values', () => { + const template = new UriTemplate('/users/{username}'); + + expect(template.match('/users/Ada%ZZ')).toEqual({ username: 'Ada%ZZ' }); + }); }); describe('edge cases', () => {