We ship an MCP server that exposes about two dozen Amazon data endpoints as
tools. Each tool's inputSchema is the endpoint's request JSON Schema, passed
through untouched — the API contract is the tool contract. That design felt
obviously correct for about three weeks.
Then a user reported that querying a marketplace we support returned an error
saying the marketplace didn't exist. It did exist. We had shipped it for months.
What was actually happening
The MCP SDK validates tool arguments against inputSchema before your handler
ever runs. With @modelcontextprotocol/sdk, registering a tool wires up an
AjvJsonSchemaValidator that checks the incoming arguments. If validation
fails, the handler is never called — you get a generic error back to the model,
and your code sees nothing.
The mechanism is fine. The problem was what we put in the schema.
Our API accepts a set of marketplaces. Upstream documentation lists nine of
them, and we'd published that list into our glossary, so we encoded it as an
enum in the request schema. Except our API supported four more marketplaces
than the upstream docs listed. The moment that enum reached inputSchema, it
stopped being documentation and became a gate:
// Before: the enum looked like helpful, self-documenting validation
inputSchema: {
type: 'object',
properties: {
marketplace: { type: 'string', enum: ['US', 'UK', 'DE', /* ...nine total */] }
}
}
A perfectly valid request for MX now fails in the SDK's validator. Not in our
gateway. Not in our upstream adapter. In a layer we don't control, before any
of our code runs.
Why it took so long to notice
Three things had to go wrong at once, and all three did.
The rejection is unobservable from the server side. Validation happens
before dispatch. Our request log is written after dispatch. Every rejected call
leaves zero trace — no row, no counter, no metric. Our logs showed a healthy
success rate the entire time, because the failures never reached them.
MCP calls are indistinguishable from REST calls in our telemetry. Both get
logged against the same endpoint path, so we couldn't even segment "MCP traffic
vs direct API traffic" to see the discrepancy. Searching our logs for MCP-shaped
rows returns nothing, because there is no such thing.
The model often doesn't surface the error. This is the part that still
bothers me. Asked for MX, a model that hits a rejected argument may quietly
retry with a nearby value from the enum — US, say — and continue. The caller
gets a normal-looking 200 with data for the wrong marketplace. That's worse than
a crash: a crash gets reported, a plausible wrong answer gets shipped.
What we do now
We split the concept in two. A glossary table can be closed — the value set is
genuinely exhaustive, safe to emit as a JSON Schema enum — or described-only,
which lists the values in prose and human-readable columns but never emits an
enum.
Of our nineteen glossary tables, four are closed. The other fifteen are
described in full, with every accepted value documented, and enforced nowhere.
That last part is the counterintuitive bit: an AI agent reading the description
sees exactly the same value list it would have seen in an enum. It just isn't
gated by it. The agent gets to try a value; the API gets to be the thing that
answers. Putting the check where the knowledge actually lives costs you nothing
in accuracy and buys back the ability to see what's happening.
Deciding when an enum is safe
We only promote a table to closed when the value set is corroborated by more
than one independent source. The one we felt best about is a weight-unit field:
// Four canonical units, listed in the appendix and independently in this
// endpoint's own field description, and the only values ever seen in
// production are g and lb — so the set is corroborated three ways rather
// than resting on the appendix alone.
enumPolicy: 'closed',
Three independent confirmations — spec, endpoint docs, observed production
values — for four values that are physically defined. That's the bar. An
upstream doc listing values without claiming the list is exhaustive does not
clear it, and we write down that reasoning next to the table so the next person
doesn't "fix" it back to an enum.
We also keep a kill switch — one boolean that stops emitting any glossary enums
at all — because releasing a bad constraint should not require a deploy.
The general lesson
If your API's schema is also your agent's tool schema, remember that they have
different failure modes. A JSON Schema for a REST API is documentation: clients
read it, and the server is the authority. The same schema as a tool
inputSchema is an enforcement point in a layer you can't instrument.
The asymmetry is brutal. A schema that's too loose costs you a clear validation
error you can see, log, and fix. A schema that's too tight costs you silent
rejections and confident wrong answers.
Write the values down. Enforce only what you'd bet on.
I work on Ecommerce Data API, which exposes
Amazon product, keyword and market data as a REST API and a remote MCP server —
the catalog described above. Our glossary tables are public if you want to see
which four we marked closed.
Top comments (0)