Repository navigation
Description
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.
Activity
uuzzrm commentedon Aug 10, 2026
I can take a look at a focused fix in the low-level Server path. I would keep the change scoped to validating a declared tool inputSchema before dispatch, add a regression test for invalid arguments, and preserve the existing behavior for servers that do not declare a schema. I will verify the v2 package tests before opening a PR.
jajazpua commentedon Nov 2, 2026
Hi @Ahmad-Faraj,
The gap in #2628 is particularly important because
tools/listadvertises a contract that the low-leveltools/callhandler does not currently enforce. Yourcode: stringversus numeric-input example makes the distinction concrete.I'm building ChangeProof, a tool for independent, integrity-checked comparisons of public or sanitized MCP/OpenAPI contract snapshots.
Would a free, manually reviewed before/after fixture for the published
inputSchemabe useful alongside the proposed SDK regression tests? I could provide a machine-readable comparison of the advertised tool contracts and clearly identify what changed, without claiming that a schema diff proves enforcement.The actual rejection of invalid arguments and absence of handler side effects must still be demonstrated by the low-level SDK tests—that is outside ChangeProof's static analysis. No credentials, installation, production access or payment needed.
If useful, could you point to two public/synthetic snapshots that reflect the intended before/after contract?
jajazpua commentedon Nov 4, 2026
One distinction worth preserving in the fix for #2628: the advertised
tools/listschema can remain byte-for-byte unchanged, while the enforcement contract becomes correct. A before/after schema diff alone therefore cannot validate the fix.A minimum low-level
Serverregression would call the same handler with{"code":12345}and assert (1) it rejects before handler invocation or side effects, (2) the error is a validation failure rather than an ordinary success result, and (3) valid{"code":"..." }inputs still invoke the handler. Run the correspondingMcpServerpath as a parity control.I can provide a static fixture for schema changes, but the actual bug needs execution-level assertions; I haven't run this SDK test.