fix: validate the whole varspec, and derive the TUI's message from its gate

Addresses Copilot's round-7 review on #2035. Two suppressed comments, both
real; each reproduced against this branch before acting.

1. `parseVarSpec` was a "strip the first `*`, trim, split on `:`" pass, so
   it accepted shapes RFC 6570's varspec production does not admit --
   measured on the previous commit:

     x://{*id}     name "id"  =>  "x://abcdef"   explode is a TRAILING modifier
     x://{id*:3}   name "id"  =>  "x://abc"      the two modifiers are exclusive
     x://{ id }    name "id"  =>  "x://abcdef"   whitespace is not a varchar
     x://{a b}     name "a b" =>  "x://1"        ditto -- and it emits into the URI

   `{id*:3}` is the worst of them: it silently truncated the value through a
   combination the grammar forbids. Parsing is now one anchored regex --
   a varname, then at most one trailing `*` or `:1-9999`.

   The name set is `varchar` widened by `-` and `~`, deliberately. A name is
   the one thing expansion emits into the URI WITHOUT encoding it (`;`, `?`
   and `&` write `name=value`), so the rule it has to satisfy is "needs no
   encoding" -- RFC 3986 unreserved plus pct-encoded, of which the RFC's own
   varchar is a subset. Strict varchar would refuse `{user-id}`, which real
   servers publish and which the SDK's matcher round-trips; refusing to read
   a resource that demonstrably works is a worse outcome for a debugging
   tool than a name one character outside the production. Everything now
   rejected is a shape we would otherwise have to guess at.

2. The TUI built its "Missing required template variable(s): ..." list with
   its own bare `values[name]` filter while gating on `hasRequiredValues`,
   which reads own properties only. For a variable legitimately named
   `constructor` the two disagreed: `({})["constructor"]` is `Object`, whose
   `.length` is 1, so the list came out empty and the error named no field
   at all while the submit stayed blocked. Both now come from one exported
   `unmetRequiredGroups`, with `hasRequiredValues` defined in terms of it.

Also renames a test whose title still said multi-name query expressions were
left "to the SDK, which handles them" -- expansion has been owned here since
935113d1; the behavior it asserts is naming, not delegation.

npm run ci passes.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-16 22:48:36 -04:00
commit 1c24eda429

View file

@ -3,7 +3,10 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { InspectorClient } from "@inspector/core/mcp/inspectorClient.js";
import { createTransportNode } from "@inspector/core/mcp/node/transport.js";
import { expandUriTemplate } from "@inspector/core/mcp/uriTemplate.js";
import {
expandUriTemplate,
tryExpandUriTemplate,
} from "@inspector/core/mcp/uriTemplate.js";
import {
createTestServerHttp,
type TestServerHttp,
@ -83,15 +86,31 @@ describe("RFC 6570 resource templates over the wire (#1919)", () => {
return connected;
}
it("advertises both templates from the checked-in config", async () => {
it("advertises every template from the checked-in config", async () => {
const connected = await connectToShowcase();
const { resourceTemplates } = await connected.listAllResourceTemplates();
expect(resourceTemplates.map((entry) => entry.uriTemplate).sort()).toEqual([
// The malformed one is advertised precisely so a client has to decide
// what to do with it; the SDK's own constructor accepts it, so nothing
// upstream of the Inspector rejects it. See the refusal test below.
"foobar://events/{topic:abc}",
"foobar://events/{topic}",
"foobar://events{?topic}",
]);
});
it("refuses to expand the malformed template rather than guessing", async () => {
// `abc` is not RFC 6570's `max-length` production, so there is no URI to
// send. Guessing `{topic}` would read something the server never
// advertised -- and this server would answer it, which is exactly why the
// check has to happen client-side.
const result = tryExpandUriTemplate("foobar://events/{topic:abc}", {
topic: "news",
});
expect(result.uri).toBeUndefined();
expect(result.error).toMatch(/Invalid RFC 6570 varspec/);
});
it("resolves the encoded URI the simple expression expands to", async () => {
const connected = await connectToShowcase();
const uri = expandUriTemplate("foobar://events/{topic}", {