From cd1322cbceff2e13a0876807a371c17db8b2926b Mon Sep 17 00:00:00 2001 From: nyxst4ck <289980115+nyxst4ck@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:40:16 -0300 Subject: [PATCH] fix(core-internal): percent-encode multi-variable URI template expansions UriTemplate.expandPart() interpolated the values of a multi-variable expression raw, never calling encodeValue(). A template such as {x,y} therefore emitted spaces and reserved characters verbatim, producing a malformed URI, while the single-variable path encoded correctly. Encode each value with the same operator-aware helper the single-variable branch uses, so {x,y} percent-encodes reserved characters and {+x,y} / {#x,y} keep them. --- .changeset/uri-template-multi-variable-encoding.md | 7 +++++++ packages/core-internal/src/shared/uriTemplate.ts | 2 +- packages/core-internal/test/shared/uriTemplate.test.ts | 10 ++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 .changeset/uri-template-multi-variable-encoding.md 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', () => {