Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/uri-template-multi-variable-encoding.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion packages/core-internal/src/shared/uriTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
10 changes: 10 additions & 0 deletions packages/core-internal/test/shared/uriTemplate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down
Loading