MCP SEP-2106 — Full JSON Schema 2020-12 in tool I/O
AgentThe news. On May 18, 2026, SEP-2106 merged into the MCP specification. The change widens the schema vocabulary that tools may use to describe their input and output:
inputSchemanow allows the full JSON Schema 2020-12 keyword set inside its requiredtype: "object"root,outputSchemadrops the object-root constraint entirely and accepts any 2020-12 schema, andstructuredContentis retyped from object-only to plainunknown. Loosening on paper — but the SEP is explicit that compatibility is asymmetric: a newer server emitting a non-objectstructuredContentor a composition-rich schema may be rejected by an older client that hasn't been updated, so the SEP recommends servers also emit a serializedTextContentfallback for non-object results during the transition.
Picture a job application that, until last week, only let you fill in plain text fields and checkboxes — name, address, "married?" yes/no. If the form needed something conditional ("if married, also provide spouse name") or alternative ("attach exactly ONE of passport, driver's license, or state ID"), the only way to express it was a paragraph of free-text instructions at the bottom of the page. SEP-2106 hands the form designer a richer template language: now the conditional, the alternatives, and the cross-references to other subforms are spelled out on the form itself, in a way the form's automated validator can actually check before the application gets routed.
The technical reason mirrors the metaphor. Before SEP-2106, the MCP wire spec implied a narrow JSON Schema subset — basically the keywords a 2014-era schema validator would understand: type, properties, required, items, enum, additionalProperties. If a tool needed to express "either a one-way booking (no return date) or a round-trip booking (return date required)," the schema author had two bad options: split it into two separate tools (now the model has to pick), or leave it as one tool with a permissive schema and a paragraph of natural-language instructions in description. The first option inflates the agent's tool registry; the second relies on the model honoring prose constraints that the runtime can't enforce.
Three surfaces, three changes
SEP-2106 touches three places on the wire, with slightly different shapes of change.
| Surface | Before SEP-2106 | After SEP-2106 |
|---|---|---|
inputSchema root | must be type: "object" (SEP-2106 commit) | must be type: "object" (unchanged) |
inputSchema keywords inside the object root | restricted vocabulary the spec named — type / properties / required (SDKs typically also accepted items, enum, additionalProperties) | full JSON Schema 2020-12 — adds oneOf / anyOf / allOf / not, if / then / else, $ref / $defs, and the rest of the 2020-12 keyword set (SEP-2106 commit) |
outputSchema | basic, object-rooted (mirrored inputSchema) (SEP-2106 commit) | fully flexible — any 2020-12 schema, including array roots, primitive roots, and composition (SEP-2106 commit) |
structuredContent TypeScript type | { [key: string]: unknown } — object only (SEP-2106 commit) | unknown — array, primitive, union, object all wire-legal (SEP-2106 commit) |
The root constraint on inputSchema is preserved because every tool call still ships a JSON-RPC arguments object — the call is arguments: { ... }, not arguments: 7. What changed is everything inside that object, plus the symmetric story for what a tool can return.
A worked example
Picture a book_flight tool. Before SEP-2106, its inputSchema could declare four fields — from, to, departure, optional return — using the restricted vocabulary the spec named (type, properties, required). To express "round-trip flights require return, one-way flights forbid it," the author had three options: split into two tools (book_one_way, book_round_trip), leave a permissive schema and write a paragraph of description prose, or both. After SEP-2106, the same tool fits in one schema using composition:
{
"type": "object",
"properties": { "from": {...}, "to": {...}, "departure": {...}, "return": {...}, "roundTrip": { "type": "boolean" } },
"required": ["from", "to", "departure", "roundTrip"],
"oneOf": [
{ "properties": { "roundTrip": { "const": true } }, "required": ["return"] },
{ "properties": { "roundTrip": { "const": false } }, "not": { "required": ["return"] } }
]
}
The new schema reaches for oneOf, two branch subschemas with their own properties and required, a not, and two const guards — every one of those keywords lived in the JSON Schema 2020-12 standard already, but none were in the wire vocabulary MCP would accept before this SEP. The runtime can now reject a malformed call before it ever reaches the tool, instead of relying on the LLM to read and honor a paragraph of English in the description field.
Why this lands now
Two pressures converged. First, tool authors kept hitting the prose-vs-schema boundary: every nontrivial real-world tool grew a description paragraph explaining what its schema couldn't say, and that paragraph then needed to be re-explained to every model that called the tool. Second, the structured tool I/O step of the agent stack — where output validation lives — assumed an object-rooted structuredContent shape that forced tools returning a list (list_files) or a scalar (count_rows) to wrap their result in { "value": ... }. Both pressures land at the schema vocabulary, so SEP-2106 widens both at once.
The rollout story is more nuanced than "strictly loosening." Existing tools that already used only the previously-allowed keywords keep working unchanged, and the wire protocol stays backward-compatible at the schema vocabulary level — composition keywords like oneOf are legal JSON either way, so an older client that doesn't validate them will simply skip the extra checks (the schema still parses, just with weaker validation). The friction is asymmetric: a newer server emitting a non-object structuredContent or a primitive-rooted outputSchema may be rejected by an older client whose type checks still expect an object, which is why the SEP recommends servers also emit a serialized TextContent fallback for non-object results during the transition. SDK consumers also see one TypeScript source break — the narrower { [k]: unknown } type loses to plain unknown, and any code that depended on the narrower type needs to widen its own annotations to match.
Goes deeper in: AI Agents → Tool Use → Structured tool I/O and AI Agents → Tool Use → MCP
Related explainers
- MCP SEP-2468 — RFC 9207 iss parameter for OAuth mix-up defense
- MCP SEP-2577 — Three deprecations and a one-year migration window
- MCP SEP-2663 lands Tasks extension — async task handles for long-running tool calls