Skip to content

Low-level Server API publishes inputSchema in tools/list but does not enforce it on tools/call #2628

Description

@Ahmad-Faraj

Summary

On @modelcontextprotocol/sdk@1.30.0, a server built with the low-level Server API declares an inputSchema in tools/list, but the SDK does not check tools/call arguments against it before invoking the handler. The call runs and the client receives a well-formed result with no error code and no isError.

McpServer does not have this problem. Its zod-derived schemas are enforced.

The specification's Security Considerations require servers to "Validate all tool inputs", so the low-level path lets a server publish a contract the SDK does not hold it to.

Reproduction

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { ListToolsRequestSchema, CallToolRequestSchema }
  from "@modelcontextprotocol/sdk/types.js";

const server = new Server(
  { name: "demo", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: "scan_code_imports",
    inputSchema: {
      type: "object",
      properties: { code: { type: "string" } },
      required: ["code"],
    },
  }],
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => ({
  content: [{
    type: "text",
    text: `CLEAN (received typeof=${typeof req.params.arguments?.code})`,
  }],
}));

await server.connect(new StdioServerTransport());

Call it with {"name":"scan_code_imports","arguments":{"code":12345}}, where the declared schema requires a string:

{"jsonrpc":"2.0","id":3,"result":{"content":[
  {"type":"text","text":"CLEAN (received typeof=number)"}]}}

The handler ran with a number. The same tool built on McpServer with an equivalent zod schema rejects the identical call:

MCP error -32602: Input validation error: Invalid arguments for tool
scan_code_imports: Invalid input: expected string, received number at code

Scale

I ran a protocol conformance census over the official registry, installing and executing all 6,106 self-contained, locally installable servers. 129 executed a tool on an argument their own declared schema rejects and returned ordinary output. Attributing each to its SDK from package metadata:

SDK family count
official TypeScript SDK 112
no recognized SDK package 7
metadata unavailable 10
official Python SDK 0
FastMCP (Python) 0

Responding denominators are 2,347 for official-ts and 1,020 for the two Python families combined. I have not tested the Python SDKs directly, so I report the zero as an observation rather than claiming a mechanism for it.

109 of the 112 declare a caret range such as ^1.0.0 or ^1.29.0, and none declares an exact version. Because the census installed each server fresh, those resolve to the newest 1.x, so this is current 1.x behaviour observed in the wild rather than a legacy artifact. It also means a fix released on the 1.x line reaches essentially all of them on their next install.

The client-visible effect is that the result is indistinguishable from a correct one. In the census a code-scanning tool asked to scan the integer 12345 answered CLEAN, and a prompt-scoring tool returned a full rubric with per-dimension sub-scores for the same integer.

Suggested fix

Validate tools/call arguments against the tool's declared inputSchema before dispatch on the low-level path, matching what McpServer already does, and report failures as isError consistent with SEP-1303.

On @modelcontextprotocol/server@2.0.0

It exports a JsonSchemaValidator and ships an Ajv implementation at ./validators/ajv. I could not determine from the published types whether a validator is wired in by default. If it is opt-in, the same gap exists there for authors who do not supply one.

Artifacts

Harness, dataset, raw transcripts, and the two probe servers used above: https://github.com/Ahmad-Faraj/mcp-conformance (see sdk_probe/)

I can supply the per-server transcripts behind the 112 figure, or re-run the census against a patched build.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions