Skip to content

Commit 094ffba

Browse files
committed
fix uri template query matching
1 parent ddba550 commit 094ffba

3 files changed

Lines changed: 59 additions & 9 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@modelcontextprotocol/core': patch
3+
'@modelcontextprotocol/core-internal': patch
4+
---
5+
6+
Match URI template query operators with omitted, partial, or out-of-order query parameters.

packages/core-internal/src/shared/uriTemplate.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -211,14 +211,6 @@ export class UriTemplate {
211211
}
212212

213213
if (part.operator === '?' || part.operator === '&') {
214-
for (let i = 0; i < part.names.length; i++) {
215-
const name = part.names[i]!;
216-
const prefix = i === 0 ? '\\' + part.operator : '&';
217-
patterns.push({
218-
pattern: prefix + this.escapeRegExp(name) + '=([^&]+)',
219-
name
220-
});
221-
}
222214
return patterns;
223215
}
224216

@@ -227,7 +219,7 @@ export class UriTemplate {
227219

228220
switch (part.operator) {
229221
case '': {
230-
pattern = part.exploded ? '([^/,]+(?:,[^/,]+)*)' : '([^/,]+)';
222+
pattern = part.exploded ? '([^/?#,]+(?:,[^/?#,]+)*)' : '([^/?#,]+)';
231223
break;
232224
}
233225
case '+':
@@ -256,10 +248,18 @@ export class UriTemplate {
256248
UriTemplate.validateLength(uri, MAX_TEMPLATE_LENGTH, 'URI');
257249
let pattern = '^';
258250
const names: Array<{ name: string; exploded: boolean }> = [];
251+
const queryNames: Array<{ name: string; exploded: boolean }> = [];
259252

260253
for (const part of this.parts) {
261254
if (typeof part === 'string') {
262255
pattern += this.escapeRegExp(part);
256+
} else if (part.operator === '?' || part.operator === '&') {
257+
for (const name of part.names) {
258+
UriTemplate.validateLength(name, MAX_VARIABLE_LENGTH, 'Variable name');
259+
queryNames.push({ name, exploded: part.exploded });
260+
}
261+
262+
pattern += part.operator === '?' ? String.raw`(?:\?[^#]*)?` : '(?:&[^#]*)?';
263263
} else {
264264
const patterns = this.partToRegExp(part);
265265
for (const { pattern: partPattern, name } of patterns) {
@@ -285,6 +285,22 @@ export class UriTemplate {
285285
result[cleanName] = exploded && value.includes(',') ? value.split(',') : value;
286286
}
287287

288+
if (queryNames.length > 0) {
289+
const queryStart = uri.indexOf('?');
290+
if (queryStart !== -1) {
291+
const queryEnd = uri.indexOf('#', queryStart);
292+
const query = uri.slice(queryStart + 1, queryEnd === -1 ? undefined : queryEnd);
293+
const params = new URLSearchParams(query);
294+
295+
for (const { name, exploded } of queryNames) {
296+
const value = params.get(name);
297+
if (value !== null) {
298+
result[name] = exploded && value.includes(',') ? value.split(',') : value;
299+
}
300+
}
301+
}
302+
}
303+
288304
return result;
289305
}
290306
}

packages/core-internal/test/shared/uriTemplate.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,34 @@ describe('UriTemplate', () => {
191191
expect(template.variableNames).toEqual(['q', 'page']);
192192
});
193193

194+
it('should match omitted query parameters', () => {
195+
const template = new UriTemplate('/search{?q,page}');
196+
const match = template.match('/search');
197+
expect(match).toEqual({});
198+
expect(template.variableNames).toEqual(['q', 'page']);
199+
});
200+
201+
it('should match a subset of query parameters', () => {
202+
const template = new UriTemplate('/search{?q,page,limit}');
203+
const match = template.match('/search?q=test&limit=10');
204+
expect(match).toEqual({ q: 'test', limit: '10' });
205+
expect(template.variableNames).toEqual(['q', 'page', 'limit']);
206+
});
207+
208+
it('should match query parameters in any order', () => {
209+
const template = new UriTemplate('/search{?q,page,limit}');
210+
const match = template.match('/search?limit=10&page=1&q=test');
211+
expect(match).toEqual({ q: 'test', page: '1', limit: '10' });
212+
expect(template.variableNames).toEqual(['q', 'page', 'limit']);
213+
});
214+
215+
it('should stop simple path variables before query parameters', () => {
216+
const template = new UriTemplate('dom://{pageId}{?selector,includeText}');
217+
const match = template.match('dom://page-1?includeText=true');
218+
expect(match).toEqual({ pageId: 'page-1', includeText: 'true' });
219+
expect(template.variableNames).toEqual(['pageId', 'selector', 'includeText']);
220+
});
221+
194222
it('should handle partial matches correctly', () => {
195223
const template = new UriTemplate('/users/{id}');
196224
expect(template.match('/users/123/extra')).toBeNull();

0 commit comments

Comments
 (0)