fix(web): expand resource templates per RFC 6570

The Resources screen discovered and substituted template variables with a
bare `/\{(\w+)\}/g` regex, which is wrong two ways: it cannot see an
expression carrying an operator, so `foobar://events{?topic}` rendered no
input at all; and it splices the raw value in, so a `/`, `?`, `#`, `%`,
space or non-ASCII character in a simple `{topic}` landed unencoded and
changed the URI's structure -- `foo/bar` produced an extra path segment
that a conforming matcher rejects with `-32602 Resource not found`.

Delegate expansion to the SDK's `UriTemplate`, the same implementation
`InspectorClient.readResourceFromTemplate` (and so the TUI) already
expands through, so the two clients cannot disagree about what a template
means. The new `utils/uriTemplate` supplies only what that class does
not: which variables to render an input for, which of them a read cannot
proceed without, and a partially-expanded preview.

Required-ness follows the operator. Under `?`, `&`, `.` or `/` the whole
expression is omitted when the variable is undefined, so those fields are
marked Optional and reading with them blank is a legitimate request for
the unfiltered resource; under `""`, `+` or `#` the variable sits mid-URI,
so it stays required. Blank fields are dropped before expanding so an
untouched optional field reads as undefined rather than as the empty
string, which would expand to a valueless `?topic=`.

Adds the `rfc6570_templates` preset and a `rfc6570-templates-http.json`
showcase server serving the two templates from the issue.

Closes #1919

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-16 18:31:35 -04:00
commit 83658db2dd
2 changed files with 578 additions and 34 deletions

View file

@ -62,6 +62,7 @@ import {
createFileResourceTemplate,
createUserResourceTemplate,
createNumberedResourceTemplates,
createRfc6570ResourceTemplates,
createSimplePrompt,
createArgsPrompt,
createNumberedPrompts,
@ -260,6 +261,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}`);
}

View file

@ -1474,6 +1474,53 @@ 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.
*/
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}"),
},
];
}
/**
* Create a "user" resource template that returns user data by ID
*/