diff --git a/.changeset/uri-template-multi-variable-encoding.md b/.changeset/uri-template-multi-variable-encoding.md new file mode 100644 index 0000000000..034daa3872 --- /dev/null +++ b/.changeset/uri-template-multi-variable-encoding.md @@ -0,0 +1,7 @@ +--- +'@modelcontextprotocol/core-internal': patch +'@modelcontextprotocol/client': patch +'@modelcontextprotocol/server': patch +--- + +`UriTemplate.expand()` now percent-encodes the values of multi-variable expressions. A template with two or more variables in one expression (`{x,y}`) interpolated its values raw, so a value carrying a space or a reserved character produced a malformed URI — `new UriTemplate('{x,y}').expand({ x: 'value with spaces', y: 'a/b?c&d' })` returned `value with spaces,a/b?c&d` instead of `value%20with%20spaces,a%2Fb%3Fc%26d`. Single-variable expressions already encoded correctly, so the defect only showed once a second variable was added to the same expression. Encoding is operator-aware, matching the single-variable path: `{+x,y}` and `{#x,y}` keep reserved characters and encode the rest. diff --git a/packages/core-internal/src/shared/uriTemplate.ts b/packages/core-internal/src/shared/uriTemplate.ts index 5ffe213acd..2be6363bd2 100644 --- a/packages/core-internal/src/shared/uriTemplate.ts +++ b/packages/core-internal/src/shared/uriTemplate.ts @@ -138,7 +138,7 @@ export class UriTemplate { if (part.names.length > 1) { const values = part.names.map(name => variables[name]).filter(v => v !== undefined); if (values.length === 0) return ''; - return values.map(v => (Array.isArray(v) ? v[0] : v)).join(','); + return values.map(v => this.encodeValue(Array.isArray(v) ? (v[0] ?? '') : v, part.operator)).join(','); } const value = variables[part.name]; diff --git a/packages/core-internal/test/shared/uriTemplate.test.ts b/packages/core-internal/test/shared/uriTemplate.test.ts index bfc3237872..0f7364eba7 100644 --- a/packages/core-internal/test/shared/uriTemplate.test.ts +++ b/packages/core-internal/test/shared/uriTemplate.test.ts @@ -35,6 +35,11 @@ describe('UriTemplate', () => { const template = new UriTemplate('{var}'); expect(template.expand({ var: 'value with spaces' })).toBe('value%20with%20spaces'); }); + + it('should encode reserved characters in multiple variables', () => { + const template = new UriTemplate('{x,y}'); + expect(template.expand({ x: 'value with spaces', y: 'a/b?c&d' })).toBe('value%20with%20spaces,a%2Fb%3Fc%26d'); + }); }); describe('reserved expansion', () => { @@ -43,6 +48,11 @@ describe('UriTemplate', () => { expect(template.expand({ path: '/foo/bar' })).toBe('/foo/bar/here'); expect(template.variableNames).toEqual(['path']); }); + + it('should keep reserved characters but encode spaces for multiple variables with + operator', () => { + const template = new UriTemplate('{+path,name}'); + expect(template.expand({ path: '/foo/bar', name: 'a b' })).toBe('/foo/bar,a%20b'); + }); }); describe('fragment expansion', () => {