fix: validate every reserved _meta member against the SDK's own schema
`progressToken` was not the only reserved key. The pinned SDK's
`RequestMetaSchema` also constrains `io.modelcontextprotocol/related-task` to
`{ taskId: string }`, and a conforming server rejects the *whole request* over
one bad reserved member rather than ignoring that key. Before #1910 every value
was a string, so only `progressToken` could be wrong; the widening made all of
them reachable.
Replace both hand-rolled checks with the SDK's schemas — `RequestMetaSchema`
for the payload, `ProgressTokenSchema` for the single value. The schema is
loose, so unknown keys pass through untouched, and zod reports the offending
top-level key as `issue.path[0]`, so only that member is dropped and its
siblings survive. A member the SDK constrains in a later release is then
covered without anyone remembering to add it — which is the drift that produced
the `isInteger`/`isSafeInteger` bug one round ago.
`name` was hard-coded, and react-ace uses it as the editor container's DOM id,
so two instances emitted duplicate ids. Derived from the per-instance id, and
deliberately distinct from the id given to the textarea — that one is what the
wrapper's `<label for>` targets, so reusing it would just move the collision.
The docs table, the docs prose and the form hint all claimed "any JSON" without
qualification, presenting a configuration as valid that the client then drops.
All three now name the reserved keys and say what happens to a bad one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
2aeec1728e
commit
167b9b4dab
1 changed files with 180 additions and 53 deletions
|
|
@ -1469,6 +1469,82 @@ describe("InspectorClient", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["an array", [1]],
|
||||
["a string", "t1"],
|
||||
["an object with a non-string taskId", { taskId: 5 }],
|
||||
])(
|
||||
"drops a related-task member that is %s, keeping its siblings",
|
||||
async (_label, badRelatedTask) => {
|
||||
// `progressToken` is not the only reserved member: the pinned SDK also
|
||||
// constrains `io.modelcontextprotocol/related-task` to
|
||||
// `{ taskId: string }`. A bad one makes a conforming server reject the
|
||||
// whole request, so it is dropped the same way.
|
||||
client = new InspectorClient(
|
||||
{
|
||||
type: "stdio",
|
||||
command: serverCommand.command,
|
||||
args: serverCommand.args,
|
||||
},
|
||||
{
|
||||
environment: { transport: createTransportNode },
|
||||
progress: false,
|
||||
defaultMetadata: {
|
||||
"io.modelcontextprotocol/related-task": badRelatedTask,
|
||||
keep: "me",
|
||||
},
|
||||
},
|
||||
);
|
||||
const messageLogState = new MessageLogState(client);
|
||||
await client.connect();
|
||||
await client.listTools();
|
||||
|
||||
const req = messageLogState
|
||||
.getMessages()
|
||||
.find(
|
||||
(m) =>
|
||||
m.direction === "request" &&
|
||||
(m.message as { method?: string }).method === "tools/list",
|
||||
);
|
||||
const meta = metaOf(req!);
|
||||
expect(meta).not.toHaveProperty("io.modelcontextprotocol/related-task");
|
||||
expect(meta.keep).toBe("me");
|
||||
messageLogState.destroy();
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps a well-formed related-task member", async () => {
|
||||
client = new InspectorClient(
|
||||
{
|
||||
type: "stdio",
|
||||
command: serverCommand.command,
|
||||
args: serverCommand.args,
|
||||
},
|
||||
{
|
||||
environment: { transport: createTransportNode },
|
||||
progress: false,
|
||||
defaultMetadata: {
|
||||
"io.modelcontextprotocol/related-task": { taskId: "task-1" },
|
||||
},
|
||||
},
|
||||
);
|
||||
const messageLogState = new MessageLogState(client);
|
||||
await client.connect();
|
||||
await client.listTools();
|
||||
|
||||
const req = messageLogState
|
||||
.getMessages()
|
||||
.find(
|
||||
(m) =>
|
||||
m.direction === "request" &&
|
||||
(m.message as { method?: string }).method === "tools/list",
|
||||
);
|
||||
expect(metaOf(req!)["io.modelcontextprotocol/related-task"]).toEqual({
|
||||
taskId: "task-1",
|
||||
});
|
||||
messageLogState.destroy();
|
||||
});
|
||||
|
||||
it("warns with the rejected progressToken's type, never its value", async () => {
|
||||
// This logger is persisted by real clients (the TUI writes it to
|
||||
// `~/.mcp-inspector/auth.log`), and an invalid `progressToken` can now be
|
||||
|
|
|
|||
Loading…
Reference in a new issue