Schema Design
Well-typed schemas reduce agent errors, prevent hallucinated arguments, and make your server predictable.
Why This Matters
Every tool's inputSchema is a contract between your server and the AI agent. When the schema is precise, the model generates valid arguments on the first try. When it's vague or missing constraints, the model guesses — and guesses wrong. Schema quality directly impacts tool call success rates.
The Spec Foundation
MCP uses JSON Schema (Draft 2020-12) for inputSchema and outputSchema. The top-level type is always "object", with parameters defined as properties.
What the Spec Defines
| Field | Required | Purpose |
|---|---|---|
inputSchema | Yes | JSON Schema describing the tool's parameters |
outputSchema | No | JSON Schema describing structured return data (added June 2025) |
structuredContent | Conditional | Must be present when outputSchema is declared |
Core Principles
1. Flatten Your Schemas
Flat schemas (top-level primitives) dramatically outperform nested objects for LLM comprehension. Deep nesting increases token overhead and reduces adherence.
| Approach | Example |
|---|---|
| Flat | { "email": "...", "status": "shipped", "limit": 20 } |
| Nested | { "filters": { "user": { "email": "..." }, "order": { "status": "..." } } } |
2. Use Enums for Categorical Fields
Never use free-text strings for fields that have a known set of valid values. Enums prevent hallucination and give strong runtime validation.
"status": { "type": "string", "enum": ["pending", "shipped", "delivered"] }"priority": { "type": "string", "enum": ["low", "medium", "high", "critical"] }
3. Constrain Numeric Fields
Always set bounds on numbers. Without minimum/maximum, a model may generate absurd values.
"limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }"page": { "type": "integer", "minimum": 1 }
4. Mark Required Fields Explicitly
The required array tells the model which parameters are mandatory. Omitting it implies everything is optional, which is rarely what you want.
5. Describe Every Property
Add a description to every property in your schema. This is where you put format expectations, default behavior, and usage guidance.
6. Use additionalProperties: false
When strict validation is needed, set additionalProperties: false to reject unexpected fields. This catches model hallucinations at the protocol level.
Output Schemas (June 2025+)
The 2025-06-18 spec added outputSchema as a first-class field. When declared:
- Servers MUST populate
structuredContentconforming to the schema - For backward compatibility, also include the same data as a
TextContentblock - Clients SHOULD validate
structuredContentagainst the schema
This enables machine-to-machine tool chaining — downstream tools can consume structured output without parsing text.
SDK Patterns
TypeScript (Zod)
The TypeScript SDK uses Zod for schema definition. Use .describe() on every field:
server.tool(
"search_orders",
{
email: z.string().email()
.describe("Customer email address"),
status: z.enum(["pending", "shipped", "delivered"])
.describe("Filter by order status"),
limit: z.number().int().min(1).max(100).default(20)
.describe("Max results to return")
},
async (args) => { /* handler */ }
);
Python (Pydantic / FastMCP)
FastMCP auto-generates schemas from type hints and docstrings. Use Field(description=...) and Literal types:
from fastmcp import FastMCP
from pydantic import Field
from typing import Literal
mcp = FastMCP("My Server")
@mcp.tool()
def search_orders(
email: str = Field(description="Customer email"),
status: Literal["pending", "shipped", "delivered"]
= Field(description="Filter by order status"),
limit: int = Field(
default=20, ge=1, le=100,
description="Max results to return"
),
) -> list[dict]:
"""Search customer orders by email and filters.
Use when a user asks about order history or wants
to find specific orders.
"""
...
FastMCP automatically dereferences $ref entries (inlining $defs) for clients that don't support JSON Schema references.
Pagination Pattern
Any tool that returns a list should support pagination. The standard MCP pattern:
- Accept
limit(default 20–50) and optionalcursoras input parameters - Return
next_cursorandhas_morein the response - Never return unbounded result sets — they flood the context window
Handling Complex Inputs
When you need structured inputs (arrays of objects, nested configs), consider these strategies:
- Break it up. Split complex operations into multiple simpler tools rather than one tool with many optional branches.
- Accept JSON strings. For unavoidably complex inputs, accept a JSON string and parse internally. This avoids deeply nested schema definitions.
- Limit tool count. Keep 5–15 tools per server. Beyond 15, split into domain-specific servers.
- Lazy-load schemas. For very large schemas, consider the lazy-loading pattern where the full schema is fetched on demand.
Testing Your Schemas
- Validate example payloads against your
inputSchemain CI to catch accidental breaking changes - Use the MCP Inspector to verify your tool definitions look correct
- Test with real LLMs — have an agent call your tools and check argument quality
- Verify
outputSchemamatches actualstructuredContentresponses
Quick Checklist
- Every tool has an
inputSchemawith"type": "object" - All required parameters are listed in the
requiredarray - Every property has a
description - Categorical fields use
enumconstraints, not free text - Numeric fields have
minimum/maximumbounds - Schemas are flat — no unnecessary nesting
- List-returning tools accept
limitandcursorparameters - Optional fields have sensible
defaultvalues additionalProperties: falseis set where strict validation is neededoutputSchemais declared for tools with machine-readable output
Essential Resources
Official Specification
SDKs & Frameworks
- MCP TypeScript SDK (GitHub)
- TypeScript SDK API Reference
- MCP Python SDK (GitHub)
- FastMCP — Tools Documentation
- FastMCP — Official Docs
- Zod — TypeScript Schema Validation
Schema Design Guides
- MCP is Not the Problem, It's Your Server (Philipp Schmid)
- 54 Patterns for Building Better MCP Tools (Arcade.dev)
- Less is More: 4 Design Patterns for MCP Servers (Klavis)
- MCP Tool Schema: What It Is, How It Works & Examples (Merge.dev)
- Writing Effective Tools for Agents
- MCP Best Practices: Architecture & Implementation Guide
- Implementing MCP: Tips, Tricks & Pitfalls (Nearform)
Structured Output & outputSchema
- MCP Spec Updated: Structured Tool Output (Socket.dev)
- MCP 2025-06-18 Spec Update: Structured Output & More (ForgeCode)
- Structured Model Outputs (OpenAI) — Enum & Strict Mode Patterns
JSON Schema for LLMs
- How JSON Schema Works for LLM Tools (PromptLayer)
- How to Map an Existing API into MCP Tool Definitions (ScaleKit)
- Defining Tool Schemas in MCP (APXML)
- Lazy Loading Input Schemas (Open-MCP)