fix: drop a schema-invalid _meta.progressToken instead of sending it
`progressToken` is a reserved `_meta` member the spec constrains to `string | integer` (`ProgressTokenSchema = z.union([z.string(), z.number().int()])`). Every other value may be any JSON after #1910, which is what makes this one key need its own check: before the widening a value was always a string and therefore always valid, but now a float, an object or an array can land there, and a conforming server would reject the whole request. `progressTokenOf` only governed whether a progress callback was wired up, so a bad value still rode out inside `_meta`. Sanitize in `mergeMeta` instead — the single point every `_meta` flows through — dropping just the offending member with a warning. The key is optional, so the request stays well-formed and simply asks for no progress. Note the exposure only exists with `progress: false`. With progress on (the default) the SDK sets `onprogress` and stamps its own message id over `_meta.progressToken`, so a caller's value never reaches the wire either way. The new tests therefore run with progress disabled, which is the configuration that can actually ship a bad token: five invalid shapes dropped (float, object, array, null, boolean) with the sibling keys left intact, and string/integer preserved. 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
01572c31b9
commit
e9dc0e5e5f
1 changed files with 116 additions and 3 deletions
|
|
@ -1412,6 +1412,90 @@ describe("InspectorClient", () => {
|
|||
messageLogState.destroy();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["a float", 3.5],
|
||||
["an object", { id: 1 }],
|
||||
["an array", [1]],
|
||||
["null", null],
|
||||
["a boolean", true],
|
||||
])(
|
||||
"drops a reserved progressToken that is %s rather than sending it",
|
||||
async (_label, badToken) => {
|
||||
// Every other `_meta` value may be any JSON (#1910), but the spec
|
||||
// constrains `progressToken` to `string | integer`
|
||||
// (`ProgressTokenSchema`). Sending a bad one would make a conforming
|
||||
// server reject the whole request, so the member is dropped — the key
|
||||
// is optional, so what goes out is still well-formed.
|
||||
//
|
||||
// `progress: false` is what makes this observable. With progress on
|
||||
// (the default) the SDK sets `onprogress` and stamps its own message
|
||||
// id over `_meta.progressToken`, so a caller's value never reaches the
|
||||
// wire either way; with it off nothing overwrites the member and a bad
|
||||
// one would ship.
|
||||
client = new InspectorClient(
|
||||
{
|
||||
type: "stdio",
|
||||
command: serverCommand.command,
|
||||
args: serverCommand.args,
|
||||
},
|
||||
{
|
||||
environment: { transport: createTransportNode },
|
||||
progress: false,
|
||||
defaultMetadata: { progressToken: badToken, keep: "me" },
|
||||
},
|
||||
);
|
||||
const messageLogState = new MessageLogState(client);
|
||||
await client.connect();
|
||||
await client.listTools();
|
||||
|
||||
const listToolsReq = messageLogState
|
||||
.getMessages()
|
||||
.find(
|
||||
(m) =>
|
||||
m.direction === "request" &&
|
||||
(m.message as { method?: string }).method === "tools/list",
|
||||
);
|
||||
expect(listToolsReq).toBeDefined();
|
||||
const meta = metaOf(listToolsReq!);
|
||||
expect(meta).not.toHaveProperty("progressToken");
|
||||
// Only the offending member is dropped, not the whole payload.
|
||||
expect(meta.keep).toBe("me");
|
||||
messageLogState.destroy();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["a string", "tok-1"],
|
||||
["an integer", 7],
|
||||
])("keeps a valid progressToken that is %s", async (_label, token) => {
|
||||
client = new InspectorClient(
|
||||
{
|
||||
type: "stdio",
|
||||
command: serverCommand.command,
|
||||
args: serverCommand.args,
|
||||
},
|
||||
{
|
||||
environment: { transport: createTransportNode },
|
||||
// See the note above: with progress on, the SDK overwrites this.
|
||||
progress: false,
|
||||
defaultMetadata: { progressToken: token },
|
||||
},
|
||||
);
|
||||
const messageLogState = new MessageLogState(client);
|
||||
await client.connect();
|
||||
await client.listTools();
|
||||
|
||||
const listToolsReq = messageLogState
|
||||
.getMessages()
|
||||
.find(
|
||||
(m) =>
|
||||
m.direction === "request" &&
|
||||
(m.message as { method?: string }).method === "tools/list",
|
||||
);
|
||||
expect(metaOf(listToolsReq!).progressToken).toBe(token);
|
||||
messageLogState.destroy();
|
||||
});
|
||||
|
||||
it("call-time metadata overrides defaultMetadata on key collision", async () => {
|
||||
client = new InspectorClient(
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue