Proposals · SEP-2106 · Final

Tools `inputSchema` & `outputSchema` Conform to JSON Schema 2020-12

Standards Track · Created 2026-01-06 · Source

Authorship note: The original proposal was authored by John McBride (@jpmcb) in PR #881, prior to the SEP-1850 PR-based workflow. This file converts that proposal to the current SEP format and is shepherded by Ola Hungerford (@olaservo), who has also revised the Backward Compatibility, Security Implications, and SDK Migration sections in response to review feedback. The original prose and design intent remain John's; substantive changes since the conversion are tracked in this PR's commit history.

Abstract

This SEP proposes loosening the restrictions on inputSchema, outputSchema, and structuredContent to better support JSON Schema 2020-12. Specifically:

This proposal enables MCP servers to leverage the expressiveness of JSON Schema 2020-12 while maintaining backward compatibility with existing implementations.

Motivation

The current MCP specification restricts tool schemas in ways that conflict with full JSON Schema support:

  1. inputSchema restriction: Currently only allows type, properties, and required fields. This prevents use of composition keywords like anyOf, oneOf, and allOf for sophisticated object validation patterns.

  2. outputSchema restriction: Also restricted to type: "object" with only properties and required, despite the specification claiming to support "JSON Schema."

  3. structuredContent restriction: Defined as { [key: string]: unknown } (an object with string keys), which prevents returning arrays—a common API response pattern.

Real-World Impact

Consider a weather API tool that returns hourly forecasts:

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

Currently, this natural array response is impossible because structuredContent must be an object. Developers are forced to wrap arrays in unnecessary container objects:

{
  "forecasts": [
    { "hour": "09:00", "temp": 68, "conditions": "sunny" },
    ...
  ]
}

This artificial constraint:

Schema Composition Use Cases

The current inputSchema restriction prevents legitimate schema patterns. With this SEP, tools can use composition keywords alongside type: "object":

{
  "type": "object",
  "oneOf": [
    { "properties": { "id": { "type": "string" } }, "required": ["id"] },
    { "properties": { "name": { "type": "string" } }, "required": ["name"] }
  ]
}

This pattern allows a tool to accept either an ID-based or name-based lookup—a common API design that is currently unsupported because the schema only allows type, properties, and required fields.

Specification

1. Loosen inputSchema

Current definition:

inputSchema: {
  type: "object";
  properties?: { [key: string]: object };
  required?: string[];
};

Proposed definition:

inputSchema: {
  $schema?: string;
  type: "object";
  [key: string]: unknown;
};

The inputSchema field retains the type: "object" requirement (since tool arguments are always objects), but now accepts any additional JSON Schema properties. This enables:

2. Loosen outputSchema

Current definition:

outputSchema?: {
  type: "object";
  properties?: { [key: string]: object };
  required?: string[];
};

Proposed definition:

outputSchema?: {
  $schema?: string;
  [key: string]: unknown;
};

The outputSchema field accepts any valid JSON Schema 2020-12 object, enabling schemas that validate arrays, primitives, or complex compositions. Unlike inputSchema, there is no type: "object" requirement since tool outputs can be any valid JSON.

3. Loosen structuredContent

Current definition:

structuredContent?: { [key: string]: unknown };

Proposed definition:

structuredContent?: unknown;

The structuredContent field accepts any valid JSON value that conforms to the tool's outputSchema. This includes:

4. Documentation Updates

Update docs/specification/draft/server/tools.mdx:

5. Examples

Tool returning an array of objects:

{
  "name": "list_users",
  "description": "List all users in the system",
  "inputSchema": {
    "type": "object",
    "properties": {
      "limit": { "type": "integer", "minimum": 1, "maximum": 100 }
    }
  },
  "outputSchema": {
    "type": "array",
    "items": {
      "type": "object",
      "properties": {
        "id": { "type": "string" },
        "name": { "type": "string" },
        "email": { "type": "string", "format": "email" }
      },
      "required": ["id", "name"]
    }
  }
}

Response:

{
  "content": [
    {
      "type": "text",
      "text": "Found 2 users: Alice (u1, alice@example.com) and Bob (u2, bob@example.com)."
    }
  ],
  "structuredContent": [
    { "id": "u1", "name": "Alice", "email": "alice@example.com" },
    { "id": "u2", "name": "Bob", "email": "bob@example.com" }
  ]
}

Tool with composition schema:

{
  "name": "find_resource",
  "description": "Find a resource by ID or name",
  "inputSchema": {
    "type": "object",
    "oneOf": [
      {
        "properties": { "id": { "type": "string", "format": "uuid" } },
        "required": ["id"]
      },
      {
        "properties": { "name": { "type": "string", "minLength": 1 } },
        "required": ["name"]
      }
    ]
  }
}

Rationale

Why not just allow arrays?

While we could simply extend structuredContent to allow arrays, this would be an incomplete solution. The root cause is that the schema types are artificially restricted to type: "object". By allowing any valid JSON Schema, we:

  1. Enable the full power of JSON Schema 2020-12
  2. Align with the specification's claim of JSON Schema support
  3. Provide a consistent, principled approach rather than piecemeal fixes

Why not require a wrapper object?

Requiring arrays to be wrapped in objects (e.g., { "items": [...] }) was considered but rejected because:

  1. It adds unnecessary complexity to responses
  2. It conflicts with common API design patterns
  3. It prevents direct schema validation of the actual response structure
  4. JSON Schema already handles array validation elegantly

Real-World API Patterns

Many production APIs return arrays directly:

Forcing wrapper objects creates friction for developers integrating existing APIs with MCP. Generic JSON Schema validation libraries should work without MCP-specific customization.

Alignment with JSON Schema 2020-12

JSON Schema 2020-12 provides powerful features for schema composition and validation. By removing artificial restrictions, MCP aligns with industry standards (OpenAPI 3.1 uses JSON Schema 2020-12) and enables developers to leverage existing JSON Schema knowledge and tooling.

SDK Ecosystem Evidence

The friction caused by current restrictions is not theoretical. FastMCP, one of the most popular Python SDKs for MCP, has implemented extensive workarounds:

  1. Explicit error messages acknowledge the limitation:

    raise ValueError(
        f"Output schemas must represent object types due to MCP spec limitations."
    )
    
  2. Auto-wrapping infrastructure adds complexity:

    • A _WrappedResult dataclass wraps non-object returns
    • A custom x-fastmcp-wrap-result extension enables client-side unwrapping
    • Both SDK and client need matching wrap/unwrap logic
  3. Real bugs have resulted from these workarounds:

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

This demonstrates that the current restrictions create genuine ecosystem friction that SEP-2106 would eliminate.

OpenAPI Precedent

The OpenAPI specification went through a similar evolution. OpenAPI 3.0 used an "extended subset" of JSON Schema with custom restrictions (like requiring nullable: true instead of allowing "null" as a type).

OpenAPI 3.1 made the strategic decision to fully align with JSON Schema 2020-12, accepting breaking changes to eliminate the friction. The result: better tooling compatibility and less ecosystem confusion.

OpenAPI's Problem MCP's Parallel
type must be string, not array inputSchema only allows specific fields
Couldn't use standard null handling Can't use oneOf/anyOf in schemas
Custom nullable keyword Object-only structuredContent
Caused tooling confusion Causes SDK workarounds

MCP can learn from OpenAPI's experience rather than repeating the same evolution over several years.

Backward Compatibility

This change is wire-format backward compatible but has nuances depending on the direction of the version mismatch.

Compatibility Matrix

New client (post-SEP) Old client (pre-SEP)
New server (post-SEP) Fully compatible. Compatible only when the server returns object-typed structuredContent. Arrays/primitives in structuredContent may break.
Old server (pre-SEP) Fully compatible. Existing object-only schemas remain valid. Unchanged.

The asymmetry: a new server that takes advantage of array or primitive structuredContent (or composition keywords in inputSchema) cannot assume an old client will accept the response. Old clients written against the previous wire format may reject structuredContent that is not a JSON object, or fail to validate inputSchema containing keywords beyond type/properties/required.

To remain interoperable with older clients, servers using array or primitive structuredContent MUST also emit a TextContent block containing the serialized JSON (as already recommended in the tools specification). Clients that do not understand non-object structuredContent can fall back to the text content.

TypeScript / SDK Migration

Widening the structuredContent field type from { [key: string]: unknown } to unknown is a source-breaking change for typed consumers, even though the wire format is unchanged. Code such as:

const result = await client.callTool({ name: "get_weather", arguments: { ... } });
const temp = result.structuredContent?.temperature;        // previously compiled (type: unknown)
const city = result.structuredContent?.["city"] as string; // previously compiled

will no longer type-check after the change, because TypeScript forbids property access on unknown without a narrowing guard:

const sc = result.structuredContent;
if (sc && typeof sc === "object" && !Array.isArray(sc)) {
  const temp = (sc as Record<string, unknown>).temperature;
}

This break is intentional — the previous type was a lie whenever a tool returned a non-object — but SDK maintainers SHOULD:

Migration Path

Security Implications

JSON Schema validation already handles type checking, value constraints, and required field validation, and implementations MUST continue to validate all inputs and outputs against declared schemas. Allowing the full JSON Schema 2020-12 vocabulary surfaces two areas that warrant explicit guidance.

$ref Dereferencing (SSRF and Fetch-DoS)

JSON Schema 2020-12 permits $ref to point at an absolute URI, not just a JSON Pointer into the same document. A naive implementation that resolves every $ref it encounters by issuing an HTTP request gives an attacker a server-side request forgery / fetch amplification primitive: a malicious tool definition can cause the host to fetch arbitrary URLs, including internal metadata endpoints or large payloads designed to exhaust resources.

To mitigate this:

Composition-Keyword Resource Use

Composition keywords (anyOf, oneOf, allOf, if/then/else) and $defs enable expressive schemas, but pathological combinations can be expensive to validate. Implementations SHOULD apply reasonable bounds — for example, a maximum schema depth, a cap on the total number of subschemas, or a per-validation time budget — to prevent a malicious tool definition from acting as a CPU DoS vector against the validator.

Reference Implementation

TypeScript SDK

A reference implementation demonstrating the loosened type restrictions:

Everything Server Demo Tools

Three demo tools added to the everything server demonstrating SEP-2106 capabilities:

Implementation Guidance

SDK implementations will need to:

  1. Update inputSchema types to retain type: "object" but allow any additional JSON Schema properties
  2. Update outputSchema types to allow any valid JSON Schema (remove type: "object" constraint)
  3. Update structuredContent types to accept any valid JSON value
  4. Update JSON Schema definitions accordingly

Acknowledgments

This proposal builds on discussions in GitHub issue #834 and incorporates feedback from the MCP community.