Typed Tools in MCP: Using JSON Schema for Predictive Tool Execution

Written by

in

# Typed Tools in MCP: Using JSON Schema for Predictive Tool Execution

![Typed tools in MCP with OnlineCloud](https://www.onlinecloud.dev/assets/hero-company-V8Xps1Ae.png)

*By the engineering team at OnlineCloud — a US-based Agentic AI & MCP engineering studio*

—

## 1. Introduction: The Failure of Natural Language Tool Descriptions

Every agent team hits the same wall around week three. The demo works — the right tool, the right arguments, applause. Then a real user runs the same flow, and the model calls `create_ticket` with `{“severity”: “urgent-ish”, “customer_id”: “the one from the email”}` — faithfully following a description that said *”customer_id: the identifier of the customer.”* The failure was never the model’s. It was the interface’s.

Natural language tool descriptions are seductive because they read like requirements — and they are the weakest contract you can ship. A model does not *understand* a description; it pattern-matches against it during generation, so every ambiguity in your prose becomes a degree of freedom in its output. Worse, prose is unenforceable at runtime: nothing checks that `severity` is one of the three values your paging system accepts, or that `customer_id` is a UUID in your CRM. The description is an aspiration; the schema is the law.

In the Model Context Protocol (MCP), a tool is declared with a `name`, a `description`, and an `inputSchema` written in JSON Schema. The first two aim at the model; the third aims at reality. Across fifteen-plus years of shipping data systems at OnlineCloud — Oracle, MS SQL, Postgres, Snowflake, CRMs — we have learned that the difference between an agent that closes tickets and one that merely appears to is whether its tool surface encodes *structural invariants* the model cannot violate. This post is about designing that boundary — and knowing how much schema each tool needs.

—

## 2. The Role of JSON Schema in MCP: Enforcing Structural Invariants

MCP is, at its core, a stateless JSON-RPC protocol. A host calls `tools/list`, and each tool advertises an `inputSchema` — a JSON Schema document describing the exact shape of arguments it will accept. When the model acts, it emits a `tools/call` request whose `arguments` are validated against that schema *before* your business logic runs. Schema first, side effects second — that ordering is the entire game.

“`json
{
“name”: “refund_order”,
“description”: “Issue a refund against a completed order.”,
“inputSchema”: {
“type”: “object”,
“additionalProperties”: false,
“required”: [“order_id”, “amount_cents”, “reason”],
“properties”: {
“order_id”: {
“type”: “string”,
“pattern”: “^ord_[a-zA-Z0-9]{20}$”
},
“amount_cents”: {
“type”: “integer”,
“minimum”: 1,
“maximum”: 1000000
},
“reason”: {
“type”: “string”,
“enum”: [“duplicate_charge”, “cancellation”, “damaged_goods”, “other”]
}
}
}
}
“`

Notice what this schema does that no description can: `order_id` is *not* “an identifier of the order” but a string matching `^ord_[a-zA-Z0-9]{20}$`; `amount_cents` is an integer — not a float, not a decimal string, not “$29.99″; `reason` is closed to four values, a fifth structurally impossible. Every constraint is checkable in microseconds, deterministic, and independent of which model is calling.

A schema is a *normative* artifact; a description is an *aspirational* one. That distinction drives how we build MCP servers at OnlineCloud: the `inputSchema` is the tool’s executable specification — it gates model calls at runtime, renders into the model’s context, drives our test fixtures, and generates the OpenAPI surface for human consumers. One artifact, three consumers, zero drift. Descriptions, by contrast, are kept deliberately short, because long descriptions encourage the model to *reason about* arguments instead of conforming to their shape.

This is the deeper role of JSON Schema in MCP: make the model’s job *easier by making it more constrained*. Generation under a schema is a search over a smaller space; smaller spaces produce fewer wrong answers. An MCP tool without a strict schema is not a function — it is a suggestion.

—

## 3. Designing Robust Schemas: Enums, Patterns, and Recursive Types

Schema quality is a design discipline, not a serialization detail. The three highest-leverage constructs — enums, patterns, and recursive references — each target a different failure mode.

**Enums close open sets.** Any argument naming one of a fixed set of things should be an `enum`: statuses, severities, channels, currencies, reasons. Faced with `”enum”: [“open”, “in_progress”, “resolved”, “closed”]`, the model is not choosing a string that *resembles* a status; it is choosing among four tokens — far more reliable than open-ended generation. Enums are also a *measurement* instrument: if logs show the model reaching for values outside the enum, the enum is missing a legitimate option — add it, never loosen validation.

**Patterns capture domain grammar.** Identifiers are the classic case: every real system has a generative grammar for its keys — `ord_`-prefixed IDs, UUIDs, E.164 phone numbers, ISO-8601 timestamps, ISO-4217 currency codes. A `pattern` keyword turns that grammar into an enforceable constraint:

“`json
{
“type”: “object”,
“required”: [“phone”, “effective_from”],
“properties”: {
“phone”: { “type”: “string”, “pattern”: “^\\+[1-9]\\d{1,14}$” },
“effective_from”: {
“type”: “string”,
“pattern”: “^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})$”
}
}
}
“`

Two cautions on patterns. First, the model sees the raw regex, so write it for *readers* — anchored, conservative, annotated. Second, prefer the most specific semantic check your stack offers: if the server validates an ID against the database anyway, do that in code and keep the pattern as the cheap syntactic gate. Patterns reject nonsense at the boundary; they are not a substitute for existence checks.

**Recursive types model nested reality.** The most common reason a schema gets loosened to `{“type”: “object”}` is a tree-shaped payload — a filter expression, a workflow DAG, an approval chain. “It’s just a nested object, I can’t type it” is a confession, not an excuse. JSON Schema handles recursion with `$ref` and `$defs`:

“`json
{
“type”: “object”,
“properties”: {
“filter”: { “$ref”: “#/$defs/condition” }
},
“$defs”: {
“condition”: {
“oneOf”: [
{ “type”: “object”, “additionalProperties”: false, “required”: [“field”, “op”, “value”],
“properties”: {
“field”: { “type”: “string”, “pattern”: “^[a-z_]{1,32}$” },
“op”: { “enum”: [“eq”, “neq”, “gt”, “gte”, “lt”, “lte”, “contains”] },
“value”: { “type”: [“string”, “number”, “boolean”] }
} },
{ “type”: “object”, “additionalProperties”: false, “required”: [“logic”, “children”],
“properties”: {
“logic”: { “enum”: [“and”, “or”, “not”] },
“children”: { “type”: “array”, “minItems”: 1, “items”: { “$ref”: “#/$defs/condition” } }
} }
]
}
}
}
“`

Three rules keep recursive schemas safe in production. Guard every recursion with a discriminator (here, `field`/`op` versus `logic`/`children`) so the model always has a deterministic next step. Bound the recursion — `maxItems`, plus a server-side depth ceiling — because an unbounded tree is a prompt-injection and cost-control hole. And add `examples` at each level: for nested structures, a worked example beats a paragraph of description, because models generalize from concrete instances more reliably than from abstract rules.

Whatever your stack — Pydantic in Python, Zod in TypeScript — generate the `inputSchema` from the same typed definition your server code uses. Hand-maintained JSON Schema and hand-written parsers drift apart by the second commit.

—

## 4. Pre-execution Validation: Rejecting Malformed Model Calls at the Server Boundary

The design decision separating serious MCP deployments from demos is *where* validation happens and *what happens when it fails*. Validation is a server-side security and correctness boundary, not a client convenience: assume any payload can arrive — from a confused model, a prompt-injected context, a stale client — and reject what does not conform *before* a single side effect occurs.

In the MCP lifecycle this is natural: `tools/call` arrives, the server validates `arguments` against the `inputSchema`, and a malformed call returns a JSON-RPC `InvalidParams` error (`-32602`) naming the offending fields. The discipline is in what you refuse to do around it. Never coerce silently: turning `”amount”: “29.99”` into `29.99` may feel helpful, but coercion teaches the model that sloppy output is survivable and quietly erases the signal that your schema or prompt is failing. And never partially execute — validate the *entire* payload, then act.

The failure path is a first-class product surface: a rejection must be *actionable by the model*, because in an agentic loop the model is the one that retries. “Validation failed” leaves it guessing; `”amount_cents: expected integer, got string; order_id: does not match pattern ^ord_[a-zA-Z0-9]{20}$”` lets it repair the call in one attempt. We measure this by counting: the *repair ratio* — retries per successful call — is one of the most sensitive early indicators of schema quality we track. It should sit near one.

Those logs are strategic data, not noise: a spike in validation failures is frequently the first symptom of a model upgrade, a drifted system prompt, or prompt-injection probing. Treat the boundary as an observability instrument — log caller, tool, offending fields, and outcome — and alert on anomalies. A team that ignores the rejections is throwing away the diagnosis.

—

## 5. Enhancing Model Accuracy: How Strict Typing Reduces Agentic Hallucination

The claim that JSON Schema improves model accuracy sounds paradoxical — how does removing freedom improve a generative system? Because tool calling is not a creative act; it is *structured prediction*: given a function signature and a goal, emit the argument object that satisfies both. Hallucination here is not inventing facts; it is *sampling from a space never constrained to contain only valid answers*. Every missing `enum`, every loose `pattern`, every `type: “object”` placeholder widens the sampling space — and everything in that widened space is a candidate hallucination.

Strict typing attacks hallucination on four fronts:

**Constrained decoding by construction.** An `enum` of four statuses is not a description that the model *should* choose among four values; it is a constraint that makes other tokens structurally invalid — and models respect structural constraints far more reliably than prose. You are not asking the model to be careful; you are making correctness the only cheap path.

**Fewer repair loops, less drift.** Every rejected call pollutes context with an error and pushes the model into *repair* mode, where it is measurably more likely to over-correct or try an adjacent wrong thing — and it burns tokens and latency. A strict schema collapses the reject-and-retry cycle, so the agent spends its generation budget on the task, not on apologizing.

**Disambiguation of ambiguous intents.** Take `search_orders(query)`: prose invites the model to stuff anything into `query` — a name, a date, a status, a complaint. Explicit typed parameters — `customer_email`, `status`, `date_from`, `date_to` — force the model to *classify* its intent into your domain model before the call, and that classification step is where accuracy is won. Ambiguity that survives belongs in a clarifying question, not in a free-text blob.

**Versioned stability across model generations.** The agents you ship this year will not run the same model forever; descriptions tuned to one model’s quirks rot on the next, while a schema is model-agnostic. Strict typing is why a server built for Claude can serve a GPT-4-class model or an open-weights deployment without behavioral drift. The schema — not the prose — makes tool behavior *predictive*: given a valid payload, the tool does one thing, every time, for every model.

None of this makes schema a substitute for evaluation; it makes schema the substrate *on which* evaluation becomes meaningful. When every call either conforms or is rejected with a named reason, your eval harness can distinguish “the model chose the wrong tool” from “the model mangled the arguments” — two failures with different fixes.

—

## 6. Decision Framework: When to Use Simple vs. Complex Schemas

Strict schemas are a tool, not a theology. Over-constraining is a real failure mode: the model can exhaust its context budget against a schema written for a machine, not a model. The art is matching strictness to the risk and structure of the operation. At OnlineCloud we run every tool through four questions:

**What is the blast radius of a malformed call?** Read-only, reversible operations tolerate looseness; writes, irreversible actions, and anything touching money or PII get maximal constraint. `search_logs` may accept a free-text `query`; `transfer_funds` gets enums, patterns, and explicit amounts — every time.

**Closed or open set?** Closed sets (statuses, reasons, channels, logic operators) become enums, unconditionally. Open sets (a product name, a search term, a note to a human) stay typed but unconstrained — because an enum that does not contain the answer forces the model to fail or to lie.

**Flat or nested?** Flat payloads of two to five typed fields rarely need more than `required`, `type`, and `enum`. For nested trees — filters, DAGs, approval chains — reach for `$defs` and `$ref`, paying the complexity cost to reject structurally meaningless payloads before they touch your engine.

**Who consumes this tool, and how often?** A tool called hundreds of times a day by an unattended agent earns every constraint you can write — each rejection is a failed business transaction. A rarely used, human-in-the-loop tool can afford looser edges; the human absorbs the ambiguity the schema would otherwise carry.

Two rules of thumb anchor the framework. First, default to the *simplest schema that makes invalid calls impossible to misinterpret*: every field typed, every closed set closed, `additionalProperties: false` on objects — nothing more. Complexity is justified only when a constraint removes a failure mode you have observed or can foresee. Second, constrain what you can name and leave a seam for what you cannot: a typed structure with one well-bounded free-text field outperforms both a fully loose and a fully rigid schema.

This is not a one-time decision: schemas must evolve with the failures your logs reveal — tightening where the model invents values, loosening where legitimate calls are rejected. Schema review belongs in the same cadence as eval review, because the schema is the eval that runs on every single call.

—

## 7. Conclusion: The Schema Is the Product

Typed tools are the difference between an agent that *appears* to understand your business and one that *cannot* violate it. Natural language descriptions told the model what you wanted; JSON Schema tells it what is possible, making everything else structurally unreachable. The result is an agent whose tool calls you can predict, test, and audit — not because the model is cleverer, but because you stopped leaving correctness to chance.

At OnlineCloud, we build MCP-native systems for teams that need agents to do real work against real systems — Oracle, MS SQL, Postgres, Snowflake, CRMs — with schema discipline, eval suites, and observability built in from the first commit. If you are designing a tool surface, or debugging an agent that keeps mangling arguments, talk to our architects. We will help you turn your tool boundary into a contract your models cannot break — and your logs will prove it.

**Ready to build typed, production-grade MCP servers?** Contact the OnlineCloud architects today for a free initial consultation — [contact@onlinecloud.dev](mailto:contact@onlinecloud.dev) — or learn more at [https://www.onlinecloud.dev/](https://www.onlinecloud.dev/). US-based senior engineers, response within 24 hours.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *