{"slug": "your-mcp-tool-schema-is-silently-rejecting-valid-calls", "title": "Your MCP Tool Schema Is Silently Rejecting Valid Calls", "summary": "A developer building an MCP server that exposes roughly two dozen Amazon data endpoints as tools found that encoding an incomplete marketplace list as a JSON Schema enum in each tool's inputSchema caused the MCP SDK's AjvJsonSchemaValidator to silently reject valid calls before any handler ran. Because validation occurs before dispatch, the rejections left no server-side logs, and models often retried with a nearby enum value, returning plausible but wrong data. The team now splits glossary tables into closed sets, safe to emit as enums, and described-only sets documented in prose but enforced nowhere, promoting a table to closed only when its value set is corroborated by multiple independent sources.", "body_md": "We ship an MCP server that exposes about two dozen Amazon data endpoints as\n\ntools. Each tool's `inputSchema` is the endpoint's request JSON Schema, passed\n\nthrough untouched — the API contract *is* the tool contract. That design felt\n\nobviously correct for about three weeks.\n\nThen a user reported that querying a marketplace we support returned an error\n\nsaying the marketplace didn't exist. It did exist. We had shipped it for months.\n\nThe MCP SDK validates tool arguments against `inputSchema` before your handler\n\never runs. With `@modelcontextprotocol/sdk`, registering a tool wires up an\n\n`AjvJsonSchemaValidator` that checks the incoming arguments. If validation\n\nfails, the handler is never called — you get a generic error back to the model,\n\nand your code sees nothing.\n\nThe mechanism is fine. The problem was what we put *in* the schema.\n\nOur API accepts a set of marketplaces. Upstream documentation lists nine of\n\nthem, and we'd published that list into our glossary, so we encoded it as an\n\n`enum` in the request schema. Except our API supported four more marketplaces\n\nthan the upstream docs listed. The moment that `enum` reached `inputSchema`, it\n\nstopped being documentation and became a gate:\n\n```\n// Before: the enum looked like helpful, self-documenting validation\ninputSchema: {\n  type: 'object',\n  properties: {\n    marketplace: { type: 'string', enum: ['US', 'UK', 'DE', /* ...nine total */] }\n  }\n}\n```\n\nA perfectly valid request for MX now fails in the SDK's validator. Not in our\n\ngateway. Not in our upstream adapter. In a layer we don't control, before any\n\nof our code runs.\n\nWhy it took so long to notice\n\nThree things had to go wrong at once, and all three did.\n\nThe rejection is unobservable from the server side. Validation happens\n\nbefore dispatch. Our request log is written after dispatch. Every rejected call\n\nleaves zero trace — no row, no counter, no metric. Our logs showed a healthy\n\nsuccess rate the entire time, because the failures never reached them.\n\nMCP calls are indistinguishable from REST calls in our telemetry. Both get\n\nlogged against the same endpoint path, so we couldn't even segment \"MCP traffic\n\nvs direct API traffic\" to see the discrepancy. Searching our logs for MCP-shaped\n\nrows returns nothing, because there is no such thing.\n\nThe model often doesn't surface the error. This is the part that still\n\nbothers me. Asked for MX, a model that hits a rejected argument may quietly\n\nretry with a nearby value from the enum — US, say — and continue. The caller\n\ngets a normal-looking 200 with data for the wrong marketplace. That's worse than\n\na crash: a crash gets reported, a plausible wrong answer gets shipped.\n\nWhat we do now\n\nWe split the concept in two. A glossary table can be closed — the value set is\n\ngenuinely exhaustive, safe to emit as a JSON Schema enum — or described-only,\n\nwhich lists the values in prose and human-readable columns but never emits an\n\nenum.\n\nOf our nineteen glossary tables, four are closed. The other fifteen are\n\ndescribed in full, with every accepted value documented, and enforced nowhere.\n\nThat last part is the counterintuitive bit: an AI agent reading the description\n\nsees exactly the same value list it would have seen in an enum. It just isn't\n\ngated by it. The agent gets to try a value; the API gets to be the thing that\n\nanswers. Putting the check where the knowledge actually lives costs you nothing\n\nin accuracy and buys back the ability to see what's happening.\n\nDeciding when an enum is safe\n\nWe only promote a table to closed when the value set is corroborated by more\n\nthan one independent source. The one we felt best about is a weight-unit field:\n\n// Four canonical units, listed in the appendix and independently in this\n\n// endpoint's own field description, and the only values ever seen in\n\n// production are g and lb — so the set is corroborated three ways rather\n\n// than resting on the appendix alone.\n\nenumPolicy: 'closed',\n\nThree independent confirmations — spec, endpoint docs, observed production\n\nvalues — for four values that are physically defined. That's the bar. An\n\nupstream doc listing values without claiming the list is exhaustive does not\n\nclear it, and we write down that reasoning next to the table so the next person\n\ndoesn't \"fix\" it back to an enum.\n\nWe also keep a kill switch — one boolean that stops emitting any glossary enums\n\nat all — because releasing a bad constraint should not require a deploy.\n\nThe general lesson\n\nIf your API's schema is also your agent's tool schema, remember that they have\n\ndifferent failure modes. A JSON Schema for a REST API is documentation: clients\n\nread it, and the server is the authority. The same schema as a tool\n\ninputSchema is an enforcement point in a layer you can't instrument.\n\nThe asymmetry is brutal. A schema that's too loose costs you a clear validation\n\nerror you can see, log, and fix. A schema that's too tight costs you silent\n\nrejections and confident wrong answers.\n\nWrite the values down. Enforce only what you'd bet on.\n\nI work on [Ecommerce Data API](https://ecommercedataapi.com), which exposes\n\nAmazon product, keyword and market data as a REST API and a remote MCP server —\n\nthe catalog described above. Our glossary tables are public if you want to see\n\nwhich four we marked closed.", "url": "https://wpnews.pro/news/your-mcp-tool-schema-is-silently-rejecting-valid-calls", "canonical_source": "https://dev.to/cloudy_c97250d0fc460db760/your-mcp-tool-schema-is-silently-rejecting-valid-calls-1495", "published_at": "2026-09-22 07:16:01+00:00", "updated_at": "2026-09-22 07:22:47.070140+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "developer-tools", "ai-tools"], "entities": ["Amazon", "Model Context Protocol", "@modelcontextprotocol/sdk", "AjvJsonSchemaValidator"], "alternates": {"html": "https://wpnews.pro/news/your-mcp-tool-schema-is-silently-rejecting-valid-calls", "markdown": "https://wpnews.pro/news/your-mcp-tool-schema-is-silently-rejecting-valid-calls.md", "text": "https://wpnews.pro/news/your-mcp-tool-schema-is-silently-rejecting-valid-calls.txt", "jsonld": "https://wpnews.pro/news/your-mcp-tool-schema-is-silently-rejecting-valid-calls.jsonld"}}