fix(core): drop malformed top-level MCP input schema fields

This commit is contained in:
Aiden Cline
2026-10-30 17:13:06 -05:00
Unverified
parent 3905c297a3
commit 1cca77a7bd
2 changed files with 19 additions and 9 deletions
+9 -2
View File
@@ -474,8 +474,8 @@ const OPTIONAL_TOOL_FIELDS = {
/**
* Returns why a listed tool cannot be used, or undefined when it can. A malformed optional field is
* deleted instead, keeping the tool. Schemas are kept as the server sent them: the legacy SDK schema
* walked every property schema, which cost most of the time on large catalogs.
* deleted instead, keeping the tool. Property schemas are not walked: the legacy SDK schema did, which
* cost most of the time on large catalogs.
*/
function checkTool(tool: unknown) {
if (!isRecord(tool)) return "not an object"
@@ -483,6 +483,13 @@ function checkTool(tool: unknown) {
if (!isRecord(tool.inputSchema)) return "inputSchema is not an object"
if (tool.inputSchema.type !== undefined && tool.inputSchema.type !== "object")
return 'inputSchema type is not "object"'
// Providers that check tool parameters against the JSON Schema metaschema reject the whole request
// over a malformed top level, so drop it; nested property schemas are left as sent.
if (tool.inputSchema.properties !== undefined && !isRecord(tool.inputSchema.properties))
delete tool.inputSchema.properties
const required = tool.inputSchema.required
if (required !== undefined && !(Array.isArray(required) && required.every((key) => typeof key === "string")))
delete tool.inputSchema.required
for (const [key, kind] of Object.entries(OPTIONAL_TOOL_FIELDS)) {
const value = tool[key]
if (value === undefined) continue
+10 -7
View File
@@ -99,8 +99,8 @@ function resourceServer(
urlElicitation?: boolean
/**
* Ahead of the valid tools, list entries that cannot be used (not an object, no name, an input
* schema that is not an object), one whose `required` is a string, which only the 2026-07-28 SDK
* schema accepts, and one with a malformed description.
* schema that is not an object), one whose input schema has a string `required`, and one with a
* malformed description.
*/
malformedTool?: boolean
/** Legacy servers give up on the URL elicitation after this many milliseconds. */
@@ -160,7 +160,7 @@ function resourceServer(
{ inputSchema: { type: "object" } },
{ name: "no-schema" },
{ name: "malformed", inputSchema: { type: "string" } },
{ name: "modern-only", inputSchema: { type: "object", required: "text" } },
{ name: "bad-required", inputSchema: { type: "object", required: "text" } },
{ name: "bad-description", description: 42, inputSchema: { type: "object" } },
] as unknown as McpClient.Tool[])
: []),
@@ -1213,10 +1213,13 @@ describe.each([
)
const tools = yield* connection.tools()
expect(tools.map((tool) => tool.name)).toEqual(["modern-only", "bad-description", "echo"])
// A malformed optional field is dropped, not the tool.
expect(tools[1]).toEqual({ name: "bad-description", inputSchema: { type: "object" } })
for (const name of ["modern-only", "bad-description", "echo"]) {
expect(tools.map((tool) => tool.name)).toEqual(["bad-required", "bad-description", "echo"])
// A malformed optional field or top-level `required` is dropped, not the tool.
expect(tools.slice(0, 2)).toEqual([
{ name: "bad-required", inputSchema: { type: "object" } },
{ name: "bad-description", inputSchema: { type: "object" } },
])
for (const name of ["bad-required", "bad-description", "echo"]) {
yield* connection.callTool({ name, args: { text: "hi" } })
expect(server.state.toolCalls.at(-1)).toMatchObject({ name, arguments: { text: "hi" } })
}