Skip to content

SEP-2106: Tools inputSchema & outputSchema Conform to JSON Schema 2020-12#2106

Merged
mcp-commander[bot] merged 29 commits into
modelcontextprotocol:mainfrom
olaservo:sep-834-json-schema-2020-12
May 18, 2026
Merged

SEP-2106: Tools inputSchema & outputSchema Conform to JSON Schema 2020-12#2106
mcp-commander[bot] merged 29 commits into
modelcontextprotocol:mainfrom
olaservo:sep-834-json-schema-2020-12

Conversation

@olaservo

@olaservo olaservo commented Jan 17, 2026

Copy link
Copy Markdown
Member

Summary

Note: This PR converts John McBride's original SEP proposal (PR #881) from issue #834 to the new SEP format established by SEP-1850. The original proposal and community discussion remain in PR #881—this PR formalizes it under the new process with a reference implementation.


  • inputSchema: Keep type: "object" required (since tool arguments are objects), but allow any additional JSON Schema 2020-12 keywords — composition (oneOf/anyOf/allOf/not), conditional (if/then/else), reference ($ref/$defs/$anchor), etc.
  • outputSchema: Fully support JSON Schema 2020-12 (arrays, primitives, objects, compositions)
  • structuredContent: Accept any JSON value validated by outputSchema
  • Include reference implementations: TypeScript SDK fork and demo server with example tools

Motivation

The current MCP specification restricts tool schemas in ways that conflict with full JSON Schema support and create real friction in the ecosystem.

The Problem: A weather API tool that returns hourly forecasts cannot return the natural response:

[
  { "hour": "09:00", "temp": 68, "conditions": "sunny" },
  { "hour": "10:00", "temp": 72, "conditions": "partly cloudy" }
]

Instead, developers must wrap arrays in unnecessary container objects—conflicting with common REST API patterns (GitHub Events API, AccuWeather, standard collection endpoints).

SDK Evidence: This isn't theoretical friction. FastMCP, one of the most popular Python SDKs, has implemented extensive workarounds with error messages that explicitly cite "MCP spec limitations." These workarounds have caused real bugs:

  • Issue #2455: $ref schemas without type: object broke ALL tools on a server
  • Issue #2421: Unexpected {"result": ...} wrapping confused users

Industry Precedent: OpenAPI went through this same evolution. OpenAPI 3.0's "extended subset" of JSON Schema caused years of tooling confusion. OpenAPI 3.1 made the strategic decision to fully align with JSON Schema 2020-12—MCP can learn from this rather than repeating the same journey.

Backward Compatibility

The change is wire-format backward compatible, but compatibility is directional:

New client (post-SEP) Old client (pre-SEP)
New server (post-SEP) Fully compatible Compatible only for object-typed structuredContent. Arrays/primitives may break old clients
Old server (pre-SEP) Fully compatible Unchanged

To stay interoperable with old clients, servers using array or primitive structuredContent MUST also emit a TextContent block with the serialized JSON.

Source-breaking for typed SDKs: Widening structuredContent from { [key: string]: unknown } to unknown breaks property access on typed consumers without narrowing guards. The SEP includes migration guidance for SDK maintainers.

Security Implications

Allowing the full JSON Schema 2020-12 vocabulary surfaces two areas covered explicitly in the SEP:

  • $ref SSRF: Implementations MUST NOT automatically dereference $ref URIs that aren't same-document JSON Pointers (e.g. #/$defs/Foo). Opt-in modes for fetching external $refs MUST default off and SHOULD enforce host allowlists, timeouts, size limits, and reject loopback/link-local/private addresses.
  • Composition cost: Implementations SHOULD bound schema depth, total subschemas, and per-validation time to prevent a malicious tool definition from acting as a CPU DoS vector.

Reference Implementation

Testing

Reference implementation tested with demo tools demonstrating:

  • get-weather-forecast: Raw array output (matches SEP motivation example)
  • get-count: Raw primitive output
  • find-by-id-or-name: Flexible input patterns using oneOf with type: "object"

AI disclosure

This PR was written with assistance from Claude. Recent revisions addressing review feedback (asymmetric compat matrix, TypeScript migration guidance, $ref SSRF / composition-DoS security implications, and the reference example fix) were paired with 🦉 Claude Opus 4.7 (1M context).

Related issue: #834
Original proposal: #881

@olaservo olaservo added the SEP label Jan 17, 2026
@olaservo olaservo self-assigned this Jan 17, 2026
@olaservo olaservo added the draft SEP proposal with a sponsor. label Jan 17, 2026
@olaservo olaservo force-pushed the sep-834-json-schema-2020-12 branch from 876b01c to 0d23ecc Compare January 17, 2026 21:55
olaservo and others added 9 commits January 17, 2026 15:50
Add SEP proposing to loosen inputSchema, outputSchema, and
structuredContent restrictions to enable full JSON Schema 2020-12
compliance. This allows array responses and schema composition
keywords (anyOf, oneOf, allOf).

Relates to modelcontextprotocol#834

Co-Authored-By: John McBride <[email protected]>
Co-Authored-By: Claude Opus 4.5 <[email protected]>
… support

- inputSchema: Allow any valid JSON Schema (enables oneOf/anyOf/allOf at root)
- outputSchema: Allow any valid JSON Schema (enables array schemas)
- structuredContent: Allow any JSON value (enables array responses)
- Update tools.mdx documentation
- Add example files demonstrating new capabilities

Reference: modelcontextprotocol#834

Co-Authored-By: John McBride <[email protected]>
Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Updated inputSchema spec to allow any valid JSON Schema (full 2020-12 compliance)
- Replaced get-array-content with get-weather-forecast tool
- Weather forecast matches the exact example from Motivation section
- Updated npm package references to latest versions:
  - SDK: @olaservo/[email protected]
  - Server: @olaservo/[email protected]

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Renamed 0000-json-schema-2020-12.md to 2106-json-schema-2020-12.md
- Updated SEP number to 2106
- Updated PR link to modelcontextprotocol#2106

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@olaservo olaservo force-pushed the sep-834-json-schema-2020-12 branch from 9eb42d9 to 7250539 Compare January 17, 2026 22:51
- Update title to match original: "Tools inputSchema & outputSchema Conform to JSON Schema 2020-12"
- inputSchema: Keep type: "object" required, but allow additional JSON Schema properties
- outputSchema: Fully flexible (any valid JSON Schema)
- structuredContent: Any JSON value (unknown)
- Update examples to show composition with type: "object"

Co-Authored-By: John McBride <[email protected]>
Co-Authored-By: Claude Opus 4.5 <[email protected]>
@olaservo olaservo changed the title SEP: Full JSON Schema 2020-12 Support for Tool Schemas SEP-2106: Tools inputSchema & outputSchema Conform to JSON Schema 2020-12 Jan 17, 2026
olaservo and others added 3 commits January 17, 2026 16:09
Aligns schema.ts with John's original PR modelcontextprotocol#881:
- inputSchema: Keep type: "object" required, allow additional properties
- outputSchema: Any valid JSON Schema (with $schema field)
- Regenerate schema.json and schema.mdx

Co-Authored-By: John McBride <[email protected]>
Co-Authored-By: Claude Opus 4.5 <[email protected]>
@sep-automation-bot

Copy link
Copy Markdown

Maintainer Activity Check

Hi @olaservo!

You're assigned to this SEP but there hasn't been any activity from you in 14 days.

Please provide an update on:

  • Current status of your review/work
  • Any blockers or concerns
  • Expected timeline for next steps

If you're no longer able to sponsor this SEP, please let us know so we can find another maintainer.


This is an automated message from the SEP lifecycle bot.

@localden localden added final SEP finalized. and removed accepted SEP accepted by core maintainers, but still requires final wording and reference implementation. labels Jun 4, 2026
koic added a commit to modelcontextprotocol/ruby-sdk that referenced this pull request Jun 24, 2026
## Motivation and Context

SEP-2106 (modelcontextprotocol/modelcontextprotocol#2106, merged for the 2026-07-28 spec release)
makes tool `inputSchema` and `outputSchema` conform to the full JSON Schema 2020-12 vocabulary:
an input schema keeps `type: "object"` at the root but may use any 2020-12 keyword below it;
an output schema may be ANY valid schema (object, array, primitive, or a root-level composition);
and `CallToolResult.structuredContent` widens from an object to any JSON value. The SEP also adds
resource bounds: `$ref` resolution is restricted (same-document only in the reference implementation) and
composition-heavy documents must be bounded to avoid excessive validation cost.

This follows the TypeScript SDK's reference implementation (typescript-sdk#2249; the Python SDK tracks
the same work in python-sdk#2792):

- `Tool::Schema` now validates against the JSON Schema 2020-12 metaschema rather than the draft-04 metaschema.
  The draft-04 pin was a stopgap from when the SDK used the `json-schema` gem, which did not support 2020-12;
  `json_schemer` does, so `$defs`/`$ref` and the rest of the 2020-12 vocabulary now resolve natively.
  This matches the dialect the SDK already advertises in emitted schemas and the Python SDK's behavior,
   whose `jsonschema.validate` selects the validator from the schema's `$schema`.
- `Tool::Schema` moves root-type defaulting into an overridable `apply_default_root_type!` hook.
  `InputSchema` keeps the historical `type: "object"` default; `OutputSchema` now applies it only
  when no root schema keyword (`type`, `$ref`, `oneOf`, `anyOf`, `allOf`, `not`, `if`, `const`, `enum`) is present.
  The previous unconditional default merged `type: "object"` into root combinators such as `{ oneOf: [...] }`,
  producing a wrong schema, so that case is a bug fix.
- `Tool::Schema` enforces the TypeScript SDK's schema bounds at construction time: only same-document
  `$ref`/`$dynamicRef`s (starting with `#`, so schema handling can never trigger network or file access),
  `MAX_SCHEMA_DEPTH = 64` nesting levels, and `MAX_SUBSCHEMA_COUNT = 10_000` subschema objects,
  all raising `ArgumentError` on violation.
- `Server#call_tool` mirrors non-object `structuredContent` into `content` as serialized JSON text
  when the tool provided no content blocks, so pre-SEP clients that only read `content` still receive the data.
  Object results and explicit content are untouched.

Resolves #377.

## How Has This Been Tested?

- `test/mcp/tool/output_schema_test.rb`: root-level `oneOf`, `$ref`+`$defs`, primitive, and `enum` schemas serialize
  without an injected `type` and validate results correctly; the `properties`-only shorthand still serializes
  with `type: "object"` (wire-format regression); explicit `type: "array"` keeps working.
- `test/mcp/tool/input_schema_test.rb`: an input schema using `$defs`, `$ref`, `oneOf`, `if`/`then`, and `allOf`
  keeps its object root and round-trips all keywords; a draft-04-only boolean `exclusiveMinimum` is rejected
  under the 2020-12 dialect while the numeric form is accepted.
- `test/mcp/tool/schema_test.rb`: depth and subschema-count bound violations raise `ArgumentError`;
  non-same-document `$ref`s (remote URI, sibling file) are rejected while `#/$defs/...` is accepted.
  The previous unbounded-depth caching test is replaced, since the depth bound now rejects such documents by design.
- `test/mcp/server_test.rb`: `tools/call` with array `structuredContent` and no content gains
  the serialized TextContent fallback; explicit content is not overwritten; object `structuredContent` gets no fallback.

`bundle exec rake` (tests, RuboCop, and conformance baseline, including the `json-schema-2020-12` server scenario) passes.

## Breaking Changes

Three narrow behavior changes, all intentional per the SEP:

- Runtime validation now uses the JSON Schema 2020-12 metaschema instead of draft-04.
  Schemas that rely on draft-04-only syntax are rejected at construction time. The practical case is the boolean
  `exclusiveMinimum`/`exclusiveMaximum` form (deprecated since draft-06), which must now be the numeric form;
  the Python SDK rejects it the same way. Other draft-04 spellings (`definitions`, `id`, `dependencies`) still validate,
  since 2020-12 tolerates unknown keywords.
- Schemas that exceed the new resource bounds (nesting deeper than 64, more than 10,000 subschema objects) or
  use a non-same-document `$ref`/`$dynamicRef` now raise `ArgumentError` at construction time.
  Previously such documents were accepted (external references were already never fetched, only ignored).
- An `OutputSchema` whose root declares a schema keyword other than `type` (e.g. `oneOf`) no longer has  `type: "object"`
  merged into it. The old output was an invalid hybrid schema, so no conforming consumer could have relied on it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

final SEP finalized. SEP

Projects

Status: Accepted

Development

Successfully merging this pull request may close these issues.

5 participants