Skip to content

inputSchema and requestedSchema hard-code JSON Schema keywords into dialect-agnostic slots #3238

Description

@clemensv

What's broken?

The spec is ambiguous or self-contradictory

Where in the spec or docs?

https://modelcontextprotocol.io/specification/2026-07-28/basic/index#schema-dialect

What should happen?

The constraint MCP wants for its schema-governed data shapes is on the instance. It is currently written as a constraint on schema syntax. Those are different layers.

inputSchema should mandate no keyword:

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

with the rule stated in prose instead:

The tool's arguments are always a JSON object. inputSchema MUST constrain the
argument object accordingly; in JSON Schema dialects this is normally expressed as
"type": "object" at the root.

That keeps the guarantee and keeps the guidance for the broader developer population for a common case.

requestedSchema needs the opposite treatment. Its restricted shape does real work
for form rendering. It is pinned to a particular dialect of the spec's choosing and $schema cannot affect how
that shape is read and should thus be dropped.

outputSchema already shows the pattern working. Same interface, same $schema
mechanism, no mandated keyword. It has no root type to pin because structuredContent
may be "any JSON value (object, array, string, number, boolean, or null)", but it
demonstrates that a schema field here can carry $schema, be validated against, and
impose its constraints entirely at the instance level.

What actually happens?

The wire types hard-code keywords and values drawn from one dialect family into the
slot that $schema is supposed to govern.

Schema Dialect:

  1. Default dialect: When a schema does not include a $schema field, it defaults to JSON Schema 2020-12
  2. Explicit dialect: Schemas MAY include a $schema field to specify a different dialect
  3. Supported dialects: Implementations MUST support at least 2020-12 and SHOULD document which additional dialects they support
  4. Recommendation: Implementors are RECOMMENDED to use JSON Schema 2020-12.

Implementation Requirements:

Clients and servers MUST validate schemas according to their declared or default
dialect. They MUST handle unsupported dialects gracefully by returning an appropriate
error indicating the dialect is not supported.

And from schema/2026-07-28/schema.ts:

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

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

Tool arguments are always JSON objects, so type: "object" is required at the root.
Beyond that, any JSON Schema 2020-12 keyword may appear alongside type — including
composition keywords (oneOf, anyOf, allOf, not), conditional keywords
(if/then/else), reference keywords ($ref, $defs, $anchor), and any other
standard validation or annotation keywords.

The pin is deliberate, so this is a request to reconsider a decision rather than a report
of an oversight. Three things follow from it.

1. The pin restates a guarantee the request type already provides

The doc comment justifies it as "Tool arguments are always JSON objects, so
type: "object" is required at the root." But that guarantee is already structural:

export interface CallToolRequestParams extends InputResponseRequestParams {
  name: string;
  arguments?: { [key: string]: unknown };
}

arguments is a JSON object by construction, whatever the schema says. The pin does
catch one authoring mistake: a server advertising {"type": "string"} would reject every
well-formed call, and the pin makes that unrepresentable. But a schema that rejects every
valid instance is a broken schema, and catching it is a job for validating the tool
definition.

2. It rejects ordinary 2020-12 schemas, the mandated dialect's own

"Alongside" is the whole limitation. A schema that factors its argument shape into
$defs and refers to it at the root:

{
  "$ref": "#/$defs/ToolArgs",
  "$defs": { "ToolArgs": { "type": "object", "properties": {} } }
}

is ordinary 2020-12, describes the JSON object the tool expects, and cannot be placed in
inputSchema. Neither can a root allOf composing a shared base with a tool-specific
extension. These are everyday authoring patterns that satisfy "tool arguments are always
JSON objects" and are rejected on the spelling of the root keyword.

{"type": "null"} is rejected too — null is one of the six primitive types of the JSON
Schema instance data model

— even though it is the simplest way to express absence of arguments.

3. requestedSchema offers a dialect choice that does nothing

requestedSchema: {
  $schema?: string;
  type: "object";
  properties: { [key: string]: PrimitiveSchemaDefinition };
  required?: string[];
};

with PrimitiveSchemaDefinition = StringSchema | NumberSchema | BooleanSchema | EnumSchema.

The restriction itself is documented and reasonable for a slot whose contents drive a
rendered form: the doc comment calls it "a restricted subset of JSON Schema" allowing
"only top-level properties … without nesting". The problem is $schema sitting on top of
it. The wire type fixes the structure completely, so declaring a dialect cannot change
how any of it is read. The restriction also reaches well inside 2020-12 rather than
stopping at its boundary: a nested object, a $ref, or any composition keyword is
ordinary 2020-12 and does not fit either.

4. The pin fixes a value set that JSON Schema leaves to the dialect

2020-12 does not treat the values of type as universal:

Note that JSON Schema vocabularies are free to define their own extended type system.
This should not be confused with the core data model types defined here. As an example,
"integer" is a reasonable type for a vocabulary to define as a value for a keyword, but
the data model makes no distinction between integers and other numbers.

2020-12 Core §4.2.1

The legal values of type belong to the dialect. Pinning one of them in the envelope
fixes something JSON Schema deliberately leaves open, for every dialect $schema can
name — and "type": "object" stops coinciding with "the instance is a JSON object" as
soon as a dialect defines its own types.

JSON Structure's choice type
is a discriminated union whose variants are both JSON objects on the wire. A tagged union
serialises as a single-property object keyed by the selector:

{ "int32": 42 }

and an inline union as an object of the selected type carrying the selector as a
property. A tool whose arguments are a discriminated union — "a URL, or a file path, or
inline content" — is an ordinary signature. A choice-rooted schema describes it
precisely and produces the JSON object MCP requires; the pin rejects it because the root
says "type": "choice". The same goes for map. JSON Structure has at least three
object-shaped roots; the pin admits one.

A JSON Schema author writes that union as oneOf beside type: "object" and passes the
check incidentally. What the pin enforces is not "the arguments are an object" but "the
root is spelled the way the Validation vocabulary spells it".

Dialects with no type keyword are excluded outright.
RFC 8927 (JSON Type Definition) expresses
objecthood through the presence of properties:

{ "properties": { "name": { "type": "string" } } }

That is a valid JTD schema describing a JSON object, and no $schema value gets it into
inputSchema.

Anything else?

If "dialect" was meant narrowly, the four points still stand. The section is titled
"JSON Schema Usage", opens with "MCP uses JSON Schema for validation throughout the
protocol", rule 4 recommends 2020-12 outright, and the only worked example of an explicit
dialect is draft-07, so the intended scope may have been "JSON Schema dialects only".
Points 1 to 3 do not depend on how "dialect" is read, and point 4 turns on §4.2.1, which
is JSON Schema's own text about JSON Schema dialects. "JSON Schema" is in any case a
series of mutually incompatible drafts, so "every draft spells it type: "object""
describes the drafts published so far rather than a property the envelope can rely on.

On scope. Relaxing inputSchema widens what a server may send. If that counts as a
compatibility-affecting change rather than a schema defect, I am glad to bring it as a
SEP instead. The documentation-only fix is not breaking, and removing $schema from
requestedSchema affects a field that currently has no effect.

Disclosure. Found while writing a binding that carries
JSON Structure schemas in MCP's schema slots, so I have an
interest in the broad reading. The raised points stand independent of that interest.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions