# 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.

- Author: [Shashank Rawlani](https://shashank.rawlani.com)

- Published: 2026-10-16T05:30:00.000Z

- Updated: 2026-10-14T10:26:51.179Z

- Category: Tools & Reviews

- Canonical source: https://automationtester.in/blog/tools-reviews/check-mcp-tool-schemas-before-agents-call-them

- Tags: mcp, ai-agents, json-schema, tool-calling

**Quick answer:** Capture your server's `tools/list` response and paste it into the [MCP Tool Schema Checker](https://automationtester.in/tools/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](https://automationtester.in/blog/automation-tutorials/validate-ai-agent-tool-calls-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](https://automationtester.in/tools/json-schema)'s engine. One result shows one shape, so review the required fields before publishing it; [reviewing required fields in a generated schema](https://automationtester.in/blog/tools-reviews/review-generated-json-schema-required-fields) 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](https://automationtester.in/blog/automation-tutorials/design-negative-api-tests-from-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](https://modelcontextprotocol.io/specification/2026-07-28/server/tools): tool name rules, the no-parameter schema forms, `x-mcp-header` constraints and client exclusion, `outputSchema` and `structuredContent`, and untrusted annotations.

- [MCP schema.ts, 2026-07-28](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2026-07-28/schema.ts): `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](https://modelcontextprotocol.io/specification/2026-07-28/basic): 2020-12 as the default dialect and no automatic fetching of network `$ref`s.

- [MCP Streamable HTTP transport](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http): which properties are statically reachable for `x-mcp-header`.

- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk): version 2.2.0 of `@modelcontextprotocol/server` and `@modelcontextprotocol/client`, used for the name check, the header warning, the legacy negotiation default and the exclusion logic.

- [OpenAI function calling guide](https://developers.openai.com/api/docs/guides/function-calling): strict mode requirements, Responses and Chat Completions defaults, and the fewer-than-20-functions advice.

- [OpenAI Structured Outputs, supported schemas](https://developers.openai.com/api/docs/guides/structured-outputs): supported formats, unsupported keywords, nesting depth, property and enum limits.

- [OpenAI Chat Completions API reference](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create): the function name character set and 64-character limit.

- [Claude tool use, defining tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use): the tool name pattern and description guidance.

- [Claude structured outputs, JSON Schema limitations](https://platform.claude.com/docs/en/build-with-claude/structured-outputs): unsupported strict-mode keywords, request-wide complexity limits, and enum capitalisation.

## Continue reading on AutomationTester.in

- [Validate AI Agent Tool Calls in TypeScript: Schema, Authorization, Approval](https://automationtester.in/blog/ai-in-testing/validate-ai-agent-tool-calls-typescript)
- [Review Required Fields in a Generated JSON Schema](https://automationtester.in/blog/tools-reviews/review-generated-json-schema-required-fields)
- [Build Deterministic Python API Test Data Factories](https://automationtester.in/blog/automation-tutorials/python-api-test-data-factories-deterministic)
- [Review an AI-Generated OpenAPI Description Before You Publish It](https://automationtester.in/blog/ai-in-testing/review-ai-generated-openapi-specifications)
- [Design Negative API Tests from the Contract](https://automationtester.in/blog/automation-tutorials/design-negative-api-tests-from-contract)

Source: [Check MCP Tool Schemas Before an Agent Calls Them](https://automationtester.in/blog/tools-reviews/check-mcp-tool-schemas-before-agents-call-them) by Shashank Rawlani.
