modelcontextprotocol / modelcontextprotocol/typescript-sdk

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

Open
#1,585 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug potentially close
Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 15h
Merged PRs (30d)
4

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:

  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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read mcp.js at server.tool() and isZodRawShapeCompat(), then reproduce the provided example and inspect the tools/list output. Done means the plain JSON Schema is either handled as inputSchema or rejected with a descriptive error instead of being silently treated as annotations.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.