mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 20:52:01 +03:00
fix(core): drop malformed top-level MCP input schema fields
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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" } })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user