Skip to main content
Back to Blog

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.

Shashank Rawlani

Engineering Leader | Builder | Problem Solver | AI Engineer

Published Oct 16, 202610 min read
View AI-ready Markdown
A green tool card with an orange-flawed name sends three paths to three panels: the top path stays green and reaches a ringed node; the other two turn orange and stop at crossed-out circles.
Quick answer: Capture your server's 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.

  • status is an enum of P, S and X with no description. The model knows the three legal values and nothing about which one means shipped.
  • createdAfter is a string with no format. 2026-10-16, 16/10/2026 and last Friday all validate, and the handler has to parse whichever arrives. The rule matches name tokens such as date, time and a trailing at, so timeoutMs does 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: true is OpenAPI 3.0 vocabulary. JSON Schema validators ignore it, so a null from the model fails even though the schema looks as if it allows one. Use "type": ["string", "null"].
  • destructiveHint: false on cancel_order tells 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.

RuleMCP 2026-07-28OpenAIClaude
Tool nameSHOULD 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 modeno strict modenot allowed: every property in required, optional ones typed with nullallowed, but at most 24 across all strict tools in the request
minimum, maxLength in strict modeany 2020-12 keywordnumeric bounds supported; string length not listednot supported; the request fails with a 400
format: "uri" in strict modeallowednot in the supported listsupported

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