From e05d66e838f5404426d57e63577ba9f9e843c28b Mon Sep 17 00:00:00 2001 From: Mubashwer Salman Khurshid Date: Wed, 11 Jun 2025 00:17:30 +1000 Subject: [PATCH] Add pathVariableNames and queryVariableNames getters in UriTemplate Add new getter methods to UriTemplate class to distinguish between path-like and query-like variables based on RFC 6570 expansion operators. This enables better resource browsing in MCP Inspector by identifying which variables are structurally required vs. optional. **New Features:** - `pathVariableNames`: Returns variables used in path-like expansions - Simple expansion: `{var}` - Reserved expansion: `{+var}` - Fragment expansion: `{#var}` - Label expansion: `{.var}` - Path segment expansion: `{/var}` - `queryVariableNames`: Returns variables used in query-like expansions - Form-style query: `{?var}` - Query continuation: `{&var}` **Use Case:** This change will improve MCP Inspector UX by allowing users to browse resource templates with only path variables filled, while treating query variables as optional filters. For example, with template `database://users/{id}{?limit,offset}`: - Path variables (`id`) identify the core resource - Query variables (`limit`, `offset`) are optional filtering parameters **Technical Details:** - Classification based on RFC 6570 expansion operators, not semantic assumptions - Path-like variables affect URI structure when omitted - Query-like variables only affect query parameters when omitted - Maintains backward compatibility with existing `variableNames` getter **Tests:** - Added comprehensive test suite covering all operator types - Tests for edge cases (no variables, only path, only query) - Validates correct classification of mixed templates --- src/shared/uriTemplate.test.ts | 48 ++++++++++++++++++++++++++++++++++ src/shared/uriTemplate.ts | 35 +++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/shared/uriTemplate.test.ts b/src/shared/uriTemplate.test.ts index 8ec4fb73..7582fcee 100644 --- a/src/shared/uriTemplate.test.ts +++ b/src/shared/uriTemplate.test.ts @@ -273,4 +273,52 @@ describe("UriTemplate", () => { expect(() => template.expand(vars)).not.toThrow(); }); }); + + describe("variable classification", () => { + it("should identify path variables", () => { + const template = new UriTemplate( + "/users/{id}/posts/{postId}{?limit,offset}" + ); + expect(template.pathVariableNames).toEqual(["id", "postId"]); + }); + + it("should identify query variables", () => { + const template = new UriTemplate( + "/users/{id}/posts/{postId}{?limit,offset}" + ); + expect(template.queryVariableNames).toEqual(["limit", "offset"]); + }); + + it("should classify different operators correctly", () => { + const template = new UriTemplate( + "{base}{+path}{#fragment}{.ext}{/segments}{?query}{&more}" + ); + expect(template.pathVariableNames).toEqual([ + "base", + "path", + "fragment", + "ext", + "segments", + ]); + expect(template.queryVariableNames).toEqual(["query", "more"]); + }); + + it("should handle templates with only path variables", () => { + const template = new UriTemplate("/users/{id}/profile"); + expect(template.pathVariableNames).toEqual(["id"]); + expect(template.queryVariableNames).toEqual([]); + }); + + it("should handle templates with only query variables", () => { + const template = new UriTemplate("/search{?q,limit,offset}"); + expect(template.pathVariableNames).toEqual([]); + expect(template.queryVariableNames).toEqual(["q", "limit", "offset"]); + }); + + it("should handle templates with no variables", () => { + const template = new UriTemplate("/static/path"); + expect(template.pathVariableNames).toEqual([]); + expect(template.queryVariableNames).toEqual([]); + }); + }); }); diff --git a/src/shared/uriTemplate.ts b/src/shared/uriTemplate.ts index 982589ac..5fbb9324 100644 --- a/src/shared/uriTemplate.ts +++ b/src/shared/uriTemplate.ts @@ -40,6 +40,41 @@ export class UriTemplate { return this.parts.flatMap((part) => typeof part === 'string' ? [] : part.names); } + /** + * Returns variable names used in path-like expansions. + * These include simple expansion, reserved expansion, fragment expansion, + * label expansion, and path segment expansion. + */ + get pathVariableNames(): string[] { + return this.parts + .filter((part) => typeof part !== "string") + .filter((part) => { + // Path-like expansions: simple, reserved, fragment, label, path segments + return ( + part.operator === "" || + part.operator === "+" || + part.operator === "#" || + part.operator === "." || + part.operator === "/" + ); + }) + .flatMap((part) => part.names); + } + + /** + * Returns variable names used in query-like expansions. + * These include form-style query and query continuation expansions. + */ + get queryVariableNames(): string[] { + return this.parts + .filter((part) => typeof part !== "string") + .filter((part) => { + // Query-like expansions: form-style query and continuation + return part.operator === "?" || part.operator === "&"; + }) + .flatMap((part) => part.names); + } + constructor(template: string) { UriTemplate.validateLength(template, MAX_TEMPLATE_LENGTH, "Template"); this.template = template;