Merge pull request #2035 from modelcontextprotocol/v2/fix/1919-rfc6570-uri-template-expansion
fix(web): expand resource templates per RFC 6570
This commit is contained in:
commit
f8d8c77783
3 changed files with 3060 additions and 59 deletions
25
test-servers/configs/rfc6570-templates-http.json
Normal file
25
test-servers/configs/rfc6570-templates-http.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"serverInfo": {
|
||||
"name": "rfc6570-template-showcase",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"resourceTemplates": [
|
||||
{
|
||||
"preset": "rfc6570_templates"
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"preset": "echo"
|
||||
}
|
||||
],
|
||||
"transport": {
|
||||
"type": "streamable-http",
|
||||
"port": 6603
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"preset": "rfc6570_base"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -63,6 +63,8 @@ import {
|
|||
createFileResourceTemplate,
|
||||
createUserResourceTemplate,
|
||||
createNumberedResourceTemplates,
|
||||
createRfc6570ResourceTemplates,
|
||||
createRfc6570BaseResource,
|
||||
createSimplePrompt,
|
||||
createArgsPrompt,
|
||||
createNumberedPrompts,
|
||||
|
|
@ -245,6 +247,8 @@ function resolveResourcePreset(
|
|||
return createNumberedResources(Number(get("count")) || 3);
|
||||
case "mcp_app_demo_widget":
|
||||
return createMcpAppDemoResource();
|
||||
case "rfc6570_base":
|
||||
return createRfc6570BaseResource();
|
||||
default:
|
||||
throw new Error(`Unknown resource preset: ${name}`);
|
||||
}
|
||||
|
|
@ -263,6 +267,8 @@ function resolveResourceTemplatePreset(
|
|||
return createUserResourceTemplate();
|
||||
case "numbered_resource_templates":
|
||||
return createNumberedResourceTemplates(Number(get("count")) || 3);
|
||||
case "rfc6570_templates":
|
||||
return createRfc6570ResourceTemplates();
|
||||
default:
|
||||
throw new Error(`Unknown resource template preset: ${name}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1518,6 +1518,92 @@ export function createFileResourceTemplate(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource templates that exercise RFC 6570 expansion (#1919).
|
||||
*
|
||||
* `events_by_topic` is the simple `{topic}` expression from the issue: a `/`,
|
||||
* `?`, `#` or space in the value MUST be percent-encoded, or the URI gains a
|
||||
* path segment and a conforming matcher rejects it. `events_by_query` is the
|
||||
* `{?topic}` form, which the web client could not even render an input for.
|
||||
*
|
||||
* Both handlers echo the URI they were matched against plus the variables the
|
||||
* server decoded, so the round-trip is visible in the read result.
|
||||
*/
|
||||
/**
|
||||
* The plain `foobar://events` resource that a blank `{?topic}` read lands on.
|
||||
*
|
||||
* Registering it is not decoration. RFC 6570 omits a query expression whose
|
||||
* variable is undefined, so leaving `topic` blank legitimately requests the
|
||||
* unfiltered collection -- but the SDK's own matcher cannot serve that from the
|
||||
* template: `UriTemplate.partToRegExp` compiles `{?topic}` to a **required**
|
||||
* `\?topic=([^&]+)`, so `match("foobar://events")` returns null and the read
|
||||
* would 404. A real server would expose the collection as its own resource;
|
||||
* this fixture does the same so the showcase's "read it blank" step works.
|
||||
*/
|
||||
export function createRfc6570BaseResource(): ResourceDefinition {
|
||||
return {
|
||||
uri: "foobar://events",
|
||||
name: "events",
|
||||
description: "All events - what a blank `{?topic}` read resolves to",
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify({ collection: "events", filtered: false }, null, 2),
|
||||
};
|
||||
}
|
||||
|
||||
export function createRfc6570ResourceTemplates(): ResourceTemplateDefinition[] {
|
||||
const echo =
|
||||
(label: string) => async (uri: URL, params: Record<string, unknown>) => ({
|
||||
contents: [
|
||||
{
|
||||
uri: uri.toString(),
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(
|
||||
{ template: label, matchedUri: uri.toString(), variables: params },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
name: "events_by_topic",
|
||||
uriTemplate: "foobar://events/{topic}",
|
||||
description:
|
||||
"Simple expression - a reserved character in `topic` must be percent-encoded",
|
||||
inputSchema: { topic: z.string().describe("Topic name") },
|
||||
handler: echo("foobar://events/{topic}"),
|
||||
},
|
||||
{
|
||||
name: "events_by_query",
|
||||
uriTemplate: "foobar://events{?topic}",
|
||||
description:
|
||||
"Query expression - optional, and omitted entirely when `topic` is blank",
|
||||
inputSchema: { topic: z.string().describe("Topic name") },
|
||||
handler: echo("foobar://events{?topic}"),
|
||||
},
|
||||
{
|
||||
// A template no client can honor: `abc` is not RFC 6570's `max-length`
|
||||
// production (`%x31-39 0*3DIGIT`), so `{topic:abc}` is a malformed
|
||||
// *template* rather than one with an ignorable modifier. The SDK's
|
||||
// `UriTemplate` constructor accepts it, which is exactly why the
|
||||
// Inspector checks the grammar itself and refuses the read -- guessing
|
||||
// `{topic}` would send a URI this server never advertised.
|
||||
//
|
||||
// The handler is therefore unreachable through the Inspector by design.
|
||||
// It is registered anyway so the template appears in
|
||||
// `resources/templates/list`, which is what puts the refusal on screen.
|
||||
name: "events_malformed",
|
||||
uriTemplate: "foobar://events/{topic:abc}",
|
||||
description:
|
||||
"Malformed template - an out-of-grammar prefix modifier; the read is withheld",
|
||||
inputSchema: { topic: z.string().describe("Topic name") },
|
||||
handler: echo("foobar://events/{topic:abc}"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a "user" resource template that returns user data by ID
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue