Skip to content

server.tool() silently drops inputSchema when passed plain JSON Schema objects instead of Zod schemas #1585

@jonnycoder1

Description

@jonnycoder1

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:

  1. The plain JSON Schema object is accepted and used as the tool's input schema (since JSON Schema is the wire format anyway), or
  2. 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

  1. 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.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions