Check MCP Tool Schemas Before an Agent Calls Them
A renamed property left in required makes every call fail, and an x-mcp-header with a space gets a tool dropped from tools/list. Run a sample server's tool list through the MCP Tool Schema Checker and see what changes when the same tools go to OpenAI or Claude.

tools/list response and paste it into the MCP Tool Schema Checker. Fix the errors first: each one means a tool is rejected, dropped from the list, or cannot be called successfully. Then set Check against to the model APIs your agent uses, because a name and schema that are valid MCP can still be refused by OpenAI or Claude. The warnings are the places a model will guess. The checker reads the definitions only; it does not call your server or a model.An agent is asked for a customer's recent orders. It calls orders.search with a customer id, a status and a date. The server answers with a validation error. The agent retries with different wording, gets the same error, and apologises to the user. Nobody changed the handler. Last week someone renamed customer_id to customerId in properties and left the old name in required. The model was never told a customer_id key exists, so it never sends one, and every call fails.
That bug sits in the tool definition, where a unit test of the handler never looks. A handful of mistakes like it cost real calls, and all of them can be read off the tools/list response before an agent sees it.
Capture what the client actually receives
Check the list the server sends, not the source that builds it. SDKs turn Zod or Pydantic models into JSON Schema, and that conversion is where some of these defects appear. With the TypeScript client, await client.listTools() returns the result object; JSON.stringify it and paste it in. The checker also accepts the raw JSON-RPC response, a bare array of tools, one tool on its own, and OpenAI or Anthropic tool definitions, including a whole request body with a tools array.
Everything runs in the browser, so the definitions are not uploaded. The page opens with a sample: a three-tool order-support server with a few realistic defects in each tool. It is the example used below, and Load sample brings it back after you have pasted your own.
What the sample server gets wrong
The relevant parts of the three tools, abridged from the sample:
{ "name": "orders.search",
"inputSchema": { "type": "object",
"properties": {
"customerId": { "type": "string", "description": "Customer id, e.g. cus_8f2k1" },
"status": { "type": "string", "enum": ["P", "S", "X"] },
"createdAfter": { "type": "string", "description": "Only orders created after this moment" } },
"required": ["customer_id", "status"] } }
{ "name": "get_order",
"inputSchema": { "type": "object",
"properties": { "orderId": { "type": "string", "x-mcp-header": "Order Id" } } } }
{ "name": "cancel_order", "description": "Cancel an order.",
"inputSchema": { "type": "object",
"properties": { "refundAmount": { "type": "float" },
"reason": { "type": "string", "nullable": true } } },
"annotations": { "destructiveHint": false } }
With Check against left on MCP spec only, the report opens with 3 errors, 5 warnings and 3 info. The errors, in the order the tool lists them:
orders.search required names "customer_id", which is not in properties
result.tools[0].inputSchema.required
get_order x-mcp-header "Order Id" is not an HTTP token
result.tools[1].inputSchema.properties.orderId["x-mcp-header"]
cancel_order "float" is not a JSON Schema type
result.tools[2].inputSchema.properties.refundAmount.type
Each finding carries the path, an explanation of what goes wrong at call time, and a fix. Copy report and Download .md export the findings as Markdown; the export contains rule names, paths and explanations, not your pasted definitions.
Three errors, three different failures
The required mismatch is the opening incident. A validator accepts the schema, the model reads it, and no call it produces can pass. If the same object also had additionalProperties: false, no input at all could pass, and the checker says so in that case.
The header is the least visible of the three. The 2026-07-28 revision lets a property carry x-mcp-header, which mirrors its value into an Mcp-Param-* HTTP header so a proxy can route on it. The value must be an RFC 9110 token, and a space is not allowed. The specification says a Streamable HTTP client that finds an invalid value MUST leave that tool out of tools/list. So get_order does not fail when it is called; the model is never offered it.
This one gets past a quick local test. The TypeScript server SDK 2.2.0 registers the tool and logs a warning that conforming Streamable HTTP clients will exclude it. The 2.2.0 client does the exclusion only after it has negotiated the 2026-07-28 revision, and its versionNegotiation mode defaults to legacy. Run against that server with an in-memory transport and default options, listTools() returned both tools, while a modern HTTP client would show one.
"float" is a Python type name leaking into a hand-written schema. JSON Schema has number and integer; the fix line says which one to use. Validators reject the schema outright, so depending on the client the tool is dropped or every call fails.
Where the model starts guessing
Warnings do not stop a call. They mark places where the schema allows several readings and the model will pick one.
statusis an enum ofP,SandXwith no description. The model knows the three legal values and nothing about which one means shipped.createdAfteris a string with noformat.2026-10-16,16/10/2026andlast Fridayall validate, and the handler has to parse whichever arrives. The rule matches name tokens such asdate,timeand a trailingat, sotimeoutMsdoes not trigger it.Cancel an order.is three words. Anthropic's tool-use guide asks for at least three or four sentences per tool, covering when to use it and what it does not do.nullable: trueis OpenAPI 3.0 vocabulary. JSON Schema validators ignore it, so anullfrom the model fails even though the schema looks as if it allows one. Use"type": ["string", "null"].destructiveHint: falseoncancel_ordertells an honest host the tool only adds things, which may remove a confirmation step.
The last one needs care, because the obvious fix is wrong. The schema sets destructiveHint to true by default and readOnlyHint to false. Leaving annotations off makes a tool look more dangerous to a host, not less. The specification also says clients must treat annotations as untrusted unless the server is trusted. So correct the hint, and keep the real protection in the handler. Validating agent tool calls in TypeScript covers that side: identity from the session, ownership checks, and a human approval step the model cannot call.
The same list, sent to OpenAI or Claude
Many agents do not speak MCP to the model. The host lists the server's tools and forwards them to a model API as function definitions, and the provider applies its own rules. Switch Check against to MCP + OpenAI + Claude and the error count on the sample rises from three to five: orders.search is valid MCP and invalid for both providers.
| Rule | MCP 2026-07-28 | OpenAI | Claude |
|---|---|---|---|
| Tool name | SHOULD be 1 to 128 of A-Z a-z 0-9 _ - . | letters, digits, _, -, up to 64 | ^[a-zA-Z0-9_-]{1,128}$ |
| Optional parameters in strict mode | no strict mode | not allowed: every property in required, optional ones typed with null | allowed, but at most 24 across all strict tools in the request |
minimum, maxLength in strict mode | any 2020-12 keyword | numeric bounds supported; string length not listed | not supported; the request fails with a 400 |
format: "uri" in strict mode | allowed | not in the supported list | supported |
Dots are the common casualty. The MCP specification uses admin.tools.list as an example of a valid name, and the server SDK's name check accepts it without a warning. A host forwarding it to either provider has to rename it or have the request rejected. If the host renames, the model's calls no longer match the server's name, and the mapping becomes one more thing to test.
Tick Strict mode to apply the stricter subsets to every tool. Tools that already set strict: true get these rules regardless. On the sample, strict mode adds a missing additionalProperties: false for each provider on two tools, and OpenAI's all-properties-required rule on the same two. Claude's limits are totals for the whole request: 20 strict tools, 24 optional parameters and 16 parameters with union types. The checker adds them up across everything pasted, so check the same set of tools the agent sends in one request.
Check a result against its outputSchema
An outputSchema is a promise about structuredContent, and servers MUST keep it. Below the findings, pick a tool and paste one tools/call result. Load sample fills in a get_order result whose handler drifted:
"structuredContent": { "orderId": "ord_19ab", "total": "49.90",
"currency": "INR", "status": "Shipped" }
structuredContent.total is string, expected number
structuredContent.status "Shipped" is not one of "pending", "shipped", "cancelled"
The result check also warns when the JSON in the text block disagrees with structuredContent. A model typically reads the text while client code reads the structured value. For a tool with no outputSchema, it infers a starting schema from the result with the JSON to Schema Generator's engine. One result shows one shape, so review the required fields before publishing it; reviewing required fields in a generated schema walks through what to tighten.
One rule changed recently, and it is easy to trip over. In 2026-07-28, outputSchema may be any JSON Schema and structuredContent any JSON value, so an array result is valid. Revision 2025-11-25 typed both as objects. The checker reports an array root as information rather than an error, because whether it breaks depends on which revision your clients speak.
What a clean report does not prove
The rules read JSON. They cannot tell whether the handler enforces what the schema promises, whether readOnlyHint: true is true, or whether a model calls the tool well. A clean report means the contract is well formed.
Some rules are heuristics and say so. Date-like names, thin descriptions, near-duplicate descriptions and verb-based annotation checks can produce a false alarm or miss a case. Claude also documents grammar-size limits beyond its published numbers, and a schema can pass every check here and still get "Schema is too complex for compilation". The provider rules match the documentation as of 1 October 2026; confirm a surprising finding against the linked page before rewriting a schema around it.
After the schema is clean, test the calls themselves. Designing negative tests from the contract applies directly: each required field, enum and format in the report is a case the handler should reject with an error the model can act on.
Should every MCP tool have additionalProperties: false? MCP does not require it; the spec recommends it only for tools with no parameters. Without it, an invented userID passes validation, and a Zod default object then drops it silently. The checker reports that as information for MCP and as an error when a provider's strict mode requires it.
Can I paste an OpenAI or Anthropic request instead? Yes. The checker detects Chat Completions, Responses, the legacy functions array and Anthropic's input_schema, and applies that provider's name rules automatically.
Run it against the tools your agent sees today
Capture tools/list from the server, using the same credentials the agent uses, because the list can vary by authorization. Paste it, set Check against to your model providers, and turn on Strict mode if you use it. Fix every error, then decide on each warning. Save the Markdown report with the server version, and paste one real result per tool that declares an outputSchema.
MCP, OpenAI and Claude references
- MCP specification 2026-07-28, Tools: tool name rules, the no-parameter schema forms,
x-mcp-headerconstraints and client exclusion,outputSchemaandstructuredContent, and untrusted annotations. - MCP schema.ts, 2026-07-28:
type: "object"at the input schema root, annotation defaults, and the widened output types; the 2025-11-25 file shows the older object-only types. - MCP basic protocol, JSON Schema usage: 2020-12 as the default dialect and no automatic fetching of network
$refs. - MCP Streamable HTTP transport: which properties are statically reachable for
x-mcp-header. - MCP TypeScript SDK: version 2.2.0 of
@modelcontextprotocol/serverand@modelcontextprotocol/client, used for the name check, the header warning, the legacy negotiation default and the exclusion logic. - OpenAI function calling guide: strict mode requirements, Responses and Chat Completions defaults, and the fewer-than-20-functions advice.
- OpenAI Structured Outputs, supported schemas: supported formats, unsupported keywords, nesting depth, property and enum limits.
- OpenAI Chat Completions API reference: the function name character set and 64-character limit.
- Claude tool use, defining tools: the tool name pattern and description guidance.
- Claude structured outputs, JSON Schema limitations: unsupported strict-mode keywords, request-wide complexity limits, and enum capitalisation.