Agentic AI Foundation Logo
Improving tool-call reliability with JSON Schema 2020-12

Improving tool-call reliability with JSON Schema 2020-12

Vincent CaldeiraAugust 31, 2026

The promise of autonomous agents lies in their ability to interact with complex systems, yet their integration with existing enterprise APIs is frequently hindered by a fundamental reliability gap. While agents can dynamically navigate workflows, they lack native awareness of the rigid business logic that often governs our underlying infrastructure. When models emit JSON, they function without the benefit of domain-specific type-checking; they frequently hallucinate keys, omit mandatory state-dependent fields, or conflate argument schemas across disparate operations.

At a small scale, these failures are often misdiagnosed as mere 'prompting problems'—solvable by iterative instruction tuning or better examples. However, this perspective overlooks the architectural nature of the issue. At scale, this instability is also architectural, echoing the 'prose-based contract' era of early API development, where documentation served as the only specification, errors were ambiguous, and transport layers lacked the composability required for robust traffic management, security policies, and distributed tracing. Bridging this gap requires moving beyond prompt engineering to treat agents as standard API clients, leveraging strict, machine-enforceable contracts at the network edge.

The recent MCP 2026-07-28 release gives tool authors a richer contract surface. inputSchema and outputSchema now support JSON Schema 2020-12, while inputSchema retains its object-root constraint. Streamable HTTP is stateless: protocol version and client capabilities travel per request in _meta, and Mcp-Method and, where applicable, Mcp-Name let intermediaries route traffic without parsing the JSON-RPC body. Implementations can use the tool schema to reject invalid arguments before business logic runs and return structured feedback for repair.

This article breaks the new design flow and approach into three layers:

  1. Machine-validatable tools via JSON Schema 2020-12
  2. Governed MCP entry with agentgateway (routing, policy, tracing)
  3. Automated error recovery in the orchestrator (here, LangGraph)

To showcase this approach, we also developed an example that uses a banking transfer_funds tool, employing one name and two contracts (“legacy” which is description-only versus “strict” which is based on JSON Schema 2020-12) with the same model and prompt. While the underlying rule—that illegal transfers are never recorded—remains constant, the location of the contract, the naming of failures, and the agent's ability to resolve issues without ledger interaction all change.

In this runnable example, every tools/call goes through agentgateway first. On the strict path, **FastMCP** then validates arguments against the published 2020-12 inputSchema before the ledger runs; LangGraph repairs when that check returns a schema-shaped invalid-params error.

1. Machine-validatable tool definitions (JSON Schema 2020-12)

Earlier MCP schema support covered core object validation but did not allow the full JSON Schema 2020-12 vocabulary, so more complex conditional rules often remained in descriptions or application validation. MCP 2026-07-28 upgrades inputSchema and outputSchema to JSON Schema 2020-12 (SEP-2106). Some constrained-decoding engines can use supported schema constraints during generation to prevent invalid structures from being sampled. The comparison below does not depend on that. It treats the schema as an edge contract: validate arguments after the model emits JSON, return a named failure, then repair.

To minimize runtime parsing failures, tool authors are able to use these 2020-12 properties:

  • Conditional logic. if / then (and else when needed) express state-dependent required fields without stuffing the rule into a description. dependentRequired covers simpler key-to-key dependencies.
  • State-dependent requirements. If the model emits a high-value transfer (amount > 10000), the schema can mandate compliance_approval_code const CMP-DEMO-2026, preventing a missing-field error after the tool has already started work.
  • Composition. oneOf forces exactly one valid argument combination so internal (source/destination accounts) and wire (IBAN/SWIFT) cannot be mixed.
  • Ambiguity elimination. That boundary stops calls that mix parameters from entirely different operations.
  • Reuse. $defs plus $ref keep account, amount, IBAN, and SWIFT fragments in one place, which limits schema drift against the types the rest of the bank already uses.
  • Security bounds. Implementations must not automatically dereference external $ref URIs and should bound schema depth and validation time.

Earlier MCP protocol versions restricted outputSchema to JSON objects. The 2026-07-28 specification lifts that so primitives and arrays can be validated as well, which keeps unexpected backend shapes out of the model context. The example’s success path still returns a structured transfer confirmation object.

A legacy description-only schema admits the illegal high-value payload. The amount “needs a compliance code” only in prose—the same pattern as documenting a required header in a README instead of in OpenAPI:

  "amount": {
    "type": "number",
    "description": "Transfer amount in USD. Amounts over 10000 need a compliance approval code."
  },
  "compliance_approval_code": {
    "type": "string",
    "description": "Required when amount is over 10000. Use CMP-DEMO-2026."
  }
}

A strict 2020-12 schema makes the same rule machine-checkable: oneOf for internal vs wire, $defs for shared fragments, if / then so amount > 10000 requires compliance_approval_code:

  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "unevaluatedProperties": false,
  "oneOf": [
    { "$ref": "#/$defs/internal" },
    { "$ref": "#/$defs/wire" }
  ],
  "if": {
    "type": "object",
    "required": ["amount"],
    "properties": {
      "amount": { "type": "number", "exclusiveMinimum": 10000 }
    }
  },
  "then": {
    "required": ["compliance_approval_code"]
  }
}

Defining $defs.compliance_approval_code with const: "CMP-DEMO-2026" enforces a structural contract: when amount > 10000, the field must be present and must equal the published demo value. Invented strings fail validation with a named error. For the purpose of the demo, CMP-DEMO-2026 is a non-secret demo placeholder, published in the schema and prompt so the example can show conditional requirements and how the agent can repair a missing field once an authorized source names the value. In a real production environment, the authorization decision would be verified server-side and not exposed as a const in a client-visible schema.

2. Governed MCP entry via agentgateway

A machine-validatable schema becomes an enforcement contract when a validator evaluates it before business logic runs. In production, validation can be enforced at the gateway, the tool server, or both, depending on the deployment. In this demo, agentgateway is the governed entry point as all MCP traffic goes through it, but we implemented the JSON schema enforcement on the strict path in the FastMCP server, not as a separate inline policy in /src/gateway/config.yaml. The gateway provides OpenAI-compatible /v1 toward the model, MCP reverse proxy toward the backends, route split (/mcp/strict, /mcp/legacy), CEL allow-listing, stateless mode, and tracing. The agent never calls the tool processes directly.

  • Traffic interception. The gateway intercepts tools/call on separate routes (/mcp/strict, /mcp/legacy) before the request reaches the tool process.
  • Schema enforcement. On the strict path the FastMCP server validates arguments against its published JSON Schema 2020-12 definition. The gateway forwards the call; it does not perform the schema check in the committed configuration.
  • Immediate halt on failure. Malformed arguments or failed conditionals stop before the ledger writes.
  • Structured feedback. The strict path surfaces a named validation failure (for example, missing compliance_approval_code). FastMCP may return this as an HTTP 200 tool result with isError: true; the orchestrator classifies that schema-shaped message as an invalid-params failure so the repair path can run.
  • Opaque contrast. On the legacy path the weak schema admits the call. The tool still does not record and returns transfer rejected. That must not start the invalid-params repair loop.

By pairing schema validation with CEL, platform teams can evaluate parsed argument fields alongside identity claims and enforce role-based authorization at L7. This example uses CEL only as a coarse allow-list (transfer_funds). It does not attach JWT policies.

For session-based upstreams, statefulMode: stateless tells agentgateway not to create Mcp-Session-Id affinity. The 2026-07-28 protocol itself is already sessionless. the protocol version and _meta, plus Mcp-Method and, where applicable, Mcp-Name. That is what lets MCP traffic sit behind the same L7 fleet as the REST APIs the tool is wrapping.

3. Error recovery and context optimization

When a schema-shaped FastMCP error promoted to invalid params comes back, the agent orchestration should not stay idle. Instead of an unhandled exception, the orchestrator should feed the structured error back into the model, prompt it to correct the payload, and re-issue the call without recording an illegal transfer.

Invalid-params errors in this flow are recoverable: the orchestrator parses the error, updates the arguments, and retries the call within a defined budget, while preventing redundant attempts with the same invalid payload. When a high-value compliance code is missing, the agent copies CMP-DEMO-2026 from the user prompt; it must not invent a code. This distinction is vital because the agent may only fulfill a schema-defined field, not manufacture a business-level compliance approval. In production, this value would be obtained via server-issued approval or a step-up flow, rather than from the published schema.

A capable local model will often include the code on the first try, which hides the contrast. In our example, the comparison withholds compliance_approval_code on the first tools/call so both routes see the same underspecified payload—the failure mode of smaller models and of description-only contracts.

Finally, loading every tool definition into model context creates a separate scaling problem for large catalogs. Progressive disclosure can reduce that cost by fetching a full tool schema only when it is needed. This example exposes one tool, so it does not cover that pattern. We plan to cover dynamic tool discovery in greater depth in a future article.

What the example shows

As explained earlier, I built a simple working example to illustrate the design approach that you can find at caldeirav/mcp-2026-07-28-schema-reliability-demo. The demo does not showcase an entire service mesh but rather focuses on the design and migration aspects for a fictitious transactional banking service. It isolates one service-design change: the same transfer_funds handler, the same ledger invariant, two published contracts. Setup and the full walkthrough live in the repository README.

The test workload is a high-value internal transfer (amount 12500) that is legal only with compliance_approval_code const CMP-DEMO-2026. Wire transfers are a different object shape (IBAN/SWIFT patterns, not an internal destination account). That is typical of wrapping a core banking API: discriminators, identifier patterns, and state-dependent approvals already exist in the application. The question is whether the MCP tool advertises them as a contract the gateway and the agent can use, or only as prose the model might follow.

Layering. Orchestration, governance, and execution stay in separate processes, analogous to client → API gateway → service:

ProcessResponsibility
LangGraphTool selection; bounded invalid-params repair; never invents a compliance code
agentgatewayGoverned entry point: LLM reverse proxy and MCP reverse proxy; route-split /mcp/strict and /mcp/legacy; CEL allow-list; stateless MCP; tracing. Does not run JSON Schema validation in the demo.
FastMCPOne tool name. Strict server enforces JSON Schema 2020-12 before the ledger. Legacy loads a weak, description-only schema.

The ledger is the system of record. It refuses illegal payloads on both paths. That is defense in depth: even when schema validation runs in FastMCP today, the ledger still enforces the same business rules. In production, the same 2020-12 check can also be enforced at a gateway where appropriate.

How 2020-12 changes the tool. On the legacy server, transfer_funds assembles arguments, calls Ledger.record, and on ValueError raises an opaque ToolError("transfer rejected"). The published inputSchema admits extra properties and has no if / then. The business rule still holds—nothing is recorded—but the agent sees an unstructured reject. It cannot tell “invalid params” from “insufficient funds,” and retrying the same JSON is wasted work.

On the strict server, the same function validates against the 2020-12 schema before record. A missing high-value code is caught before Ledger.record and surfaced as a path-qualified validation message: (root): 'compliance_approval_code' is a required property. The handler stays a thin adapter: assemble arguments, validate contract, call the domain object. The schema, not the docstring, is what moved the conditional approval and the internal/wire split out of application-error handling.

We can visualize this contrast using the agentgateway MCP playground by inspecting the raw JSON-RPC responses. Both paths return HTTP 200, but the strict path provides a field-specific validation message that the orchestrator can classify and use for repair, whereas the legacy path returns only an opaque error string.

To see this in action, we first call transfer_funds without a compliance code:

  "transfer_type": "internal",
  "source_account": "ACC1001",
  "destination_account": "ACC2002",
  "amount": 12500
}

The “strict” path names the missing field and does not record:

Strict playground: high-value transfer rejected for missing compliance_approval_code

The “legacy” path admits the object, still does not record, and returns an opaque reject:

Legacy playground: opaque transfer rejected for the same payload

Add "compliance_approval_code": "CMP-DEMO-2026" on the strict route. The call records a transfer_id—the ledger only runs after the contract is satisfied:

Strict playground: transfer recorded after CMP-DEMO-2026

What the agent can do with that. On legacy, error_kind=opaque, repair_attempts=0, nothing recorded. On the “strict” path, the first hop is invalid params. In this case, the graph can elicit a compliance approval from a user, copy the CMP-DEMO-2026 code from the prompt (it must not invent a code), retry with a changed argument fingerprint, and record. That is the delta between schema as documentation for one app and schema as an edge contract the agent can close.

[legacy] error_kind=opaque repair_attempts=0 recorded=no transfer_id=-
  1. POST /mcp/legacy  http=200  rpc=-  args={ amount=12500}  resp="transfer rejected"
[strict] error_kind=none repair_attempts=1 recorded=yes transfer_id=
  1. POST /mcp/strict  http=200  invalid params  args={ amount=12500}  resp="(root): 'compliance_approval_code' is a required property"
  2. POST /mcp/strict  http=200  rpc=-  args={ compliance_approval_code=CMP-DEMO-2026}  resp=ok transfer_id=

Note that the example does not implement a complex tool mesh, progressive disclosure, constrained decoding, or identity-based CEL. Those are other important and real 2026-07-28 capabilities; they are out of scope so the two-route comparison stays as a simple illustrative example.

Migrating existing MCP servers

Most MCP servers already wrap REST, gRPC, or in-process domain objects. MCP 2026-07-28 does not require rewriting your application logic. Instead, it asks you to formalize the contracts your APIs already enforce—making them machine-readable for gateways and agents—and to move away from using sticky MCP sessions for state management.

Map these design principles onto your existing stack:

  1. Inventory description-only rules. Identify every business constraint currently hidden in prose or descriptions (e.g., "if amount > 10,000, compliance code is required"). If these rules already exist in your OpenAPI spec, Protobuf, or Pydantic models, that is your source of truth. Stop re-documenting them in tool descriptions.
  2. Publish a formal 2020-12 inputSchema. Define tool schemas using JSON Schema 2020-12. Use $defs to align with your existing domain types (e.g., account, amount, IBAN), leverage oneOf or if/then for state-dependent logic, and set unevaluatedProperties: false to prevent the model from injecting unexpected data. Include an outputSchema to ensure backend responses remain predictable.
  3. Keep handlers as thin adapters. Your tool handler should only validate input and call your underlying service. Treat service-level validation as a defense-in-depth measure, but do not rely on it as your primary contract, as application ValueError strings are opaque to agents.
  4. Distinguish validation errors from business rejects. Return invalid params errors for schema or type violations, surfacing the specific field or path that failed. Reserve opaque errors for actual business rejections (e.g., insufficient funds, policy denials). This distinction is critical: it tells the agent whether to attempt a repair or stop.
  5. Use a gateway where appropriate. For remote MCP deployments, centralize routing, authorization, and tracing at the edge. Where gateway-side schema validation is available, it can reject invalid tool arguments before they reach a tool replica; keep server-side validation as defense in depth.
  6. Implement bounded repair loops. Orchestrators should treat invalid params as recoverable. If a tool call fails due to invalid parameters, the agent should parse the error, apply a fix (using data from the user prompt or verified prior context), and retry. Do not allow infinite retries; fingerprint arguments to forbid identical failed attempts and cap the number of retries.
  7. Use A/B testing before cutover. Deploy the strict 2020-12 contract on a separate route alongside your existing description-only contract. Compare the illegal-call rate, invalid-params rate, and repair success. Once the strict path consistently rejects illegal work while enabling successful repairs, decommission the legacy route.

Once you have established these foundations, you can scale reliability with more advanced patterns. Beyond this example, you might attach identity-aware CEL to validate arguments against JWT claims, enable progressive disclosure for large catalogs, compile schemas into constrained decoding grammars, or utilize InputRequiredResult for human-in-the-loop step-ups.

Crucially, however, none of these optimizations replace the fundamental prerequisite: the tool’s published schema must strictly mirror the contract enforced by the underlying service.

By combining JSON Schema 2020-12 at the tool level, pre-execution validation, and a bounded orchestrator repair loop, you can distinguish reparable input errors from business rejections. This gives agents clearer, machine-checkable boundaries around existing APIs while keeping server-side business rules as the final authority.

Repo: https://github.com/caldeirav/mcp-2026-07-28-schema-reliability-demo

Share

Author

subscription section bg
Subscribe

Subscribe to the AAIF Briefing

Weekly signal on standards, governance, and the people building the future. No fluff. Just what matters.

About AAIF