fix(mcp): align runtime unknown argument validation

This commit is contained in:
morluto 2026-07-16 19:01:35 +08:00
parent 8a1c128b67
commit d1e72207f4
3 changed files with 26 additions and 2 deletions

View File

@ -6,6 +6,7 @@ import {
} from "../domain/errors.js";
import { projectInputIssues } from "../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../domain/result.js";
import { closeObjectSchemas } from "./toolRegistrationOptions.js";
/** Parse one MCP request exactly once and retain only schema-owned correction data. */
export const safeParseToolInput = <Schema extends z.ZodType>(
@ -41,7 +42,7 @@ const unknownArgumentIssues = (
throw new TypeError(
"Tool input schema does not expose Standard JSON Schema",
);
jsonSchema = converter({ target: "draft-2020-12" });
jsonSchema = closeObjectSchemas(converter({ target: "draft-2020-12" }));
schemaCache.set(schema, jsonSchema);
}
return scanUnknownArguments(input, jsonSchema, jsonSchema, []);

View File

@ -67,7 +67,8 @@ const inputJsonSchema = (schema: z.ZodObject): Record<string, unknown> => {
return converter({ target: "draft-2020-12" });
};
const closeObjectSchemas = (
/** Close fixed-shape JSON Schema objects while preserving explicit maps. */
export const closeObjectSchemas = (
value: Readonly<Record<string, unknown>>,
): Record<string, unknown> => {
const closed = Object.fromEntries(

View File

@ -629,6 +629,28 @@ describe("enhanced MCP tools", () => {
},
},
});
const nestedMisspelled = await client.callTool({
name: "analyze_function",
arguments: {
procedure: "0x1",
collection_offset: { commentz: 1 },
},
});
expect(nestedMisspelled).toMatchObject({
isError: true,
structuredContent: {
error: {
details: {
issues: [
{
path: ["collection_offset", "commentz"],
reason: "unknown_argument",
},
],
},
},
},
});
const bounded = await client.callTool({
name: "trace_feature",
arguments: { query: "needle", max_operations: 101 },