-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
Summary
server.tool(name, description, inputSchema, callback) silently misinterprets a plain JSON Schema object as ToolAnnotations, resulting in the tool being registered with no parameters. There is no error, no warning, and no TypeScript compile-time error. The schema is simply dropped.
Reproduction
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ name: "test", version: "1.0.0" });
server.tool(
"my.tool",
"A tool that requires a directory_id",
{
type: "object",
properties: {
directory_id: {
type: "string",
format: "uuid",
description: "The UUID of the directory",
},
},
required: ["directory_id"],
},
async (args) => ({
content: [{ type: "text", text: JSON.stringify(args) }],
})
);
When a client calls tools/list, the tool is returned with:
{ "type": "object", "properties": {} }
The directory_id parameter is gone. Calling the tool with { "directory_id": "..." } does not pass the argument to the handler.
Expected behavior
Either:
- The plain JSON Schema object is accepted and used as the tool's input schema (since JSON Schema is the wire format anyway), or
- An error is thrown indicating that a Zod schema is required
Actual behavior
The JSON Schema object silently falls through isZodRawShapeCompat() (returns false because no values have .parse()/.safeParse() methods) and is assigned to the annotations variable instead of inputSchema:
// mcp.js, inside tool()
if (isZodRawShapeCompat(firstArg)) {
inputSchema = rest.shift(); // ← not taken
} else if (typeof firstArg === 'object' && firstArg !== null) {
annotations = rest.shift(); // ← JSON Schema lands here
}
The tool is registered with inputSchema = undefined, which serializes as { type: "object", properties: {} } via EMPTY_OBJECT_JSON_SCHEMA.
Root cause
isZodRawShapeCompat() checks whether at least one object value passes isZodTypeLike() (has .parse() and .safeParse() methods). A plain JSON Schema object's values are strings, arrays, and plain objects — none of which have these methods — so it fails
the check. The fallback else branch unconditionally treats any non-Zod object as ToolAnnotations without validating that it actually is one.
Impact
This is a silent data-loss bug. Tools appear to register successfully, compile without errors, and respond to tools/list — but with empty parameter schemas. The failure is only observable at runtime when an MCP client can't pass arguments to the tool.
The TypeScript types don't catch this either: the generic Args extends ZodRawShapeCompat resolves to Record<string, AnySchema>, which is loose enough to accept Record<string, unknown> without complaint.
Suggested fix
- Validate the fallback: before assigning to annotations, check that the object actually conforms to ToolAnnotations. If it matches neither ZodRawShapeCompat nor ToolAnnotations, throw a descriptive error.
- Consider accepting JSON Schema directly: since the wire format is JSON Schema, it would be natural to support { type: "object", properties: { ... } } as a valid input format alongside Zod shapes.
Environment
- @modelcontextprotocol/sdk version: 1.26.0
- Node.js 18+
- TypeScript 5.x, ESM