Skip to content

fix: ensure tool inputSchema includes type: object for discriminated unions#1675

Open
MayCXC wants to merge 2 commits intomodelcontextprotocol:mainfrom
MayCXC:fix/discriminated-union-input-schema
Open

fix: ensure tool inputSchema includes type: object for discriminated unions#1675
MayCXC wants to merge 2 commits intomodelcontextprotocol:mainfrom
MayCXC:fix/discriminated-union-input-schema

Conversation

@MayCXC
Copy link

@MayCXC MayCXC commented Mar 12, 2026

Fixes #1643

Problem

z.toJSONSchema() on a z.discriminatedUnion() produces { oneOf: [...] } without a top-level type field. The MCP protocol requires inputSchema.type === "object" (spec.types.ts), so clients that validate the tools/list response reject these schemas. The as Tool['inputSchema'] cast hides the mismatch at compile time.

Fix

// Before
inputSchema: tool.inputSchema
    ? (schemaToJson(tool.inputSchema, { io: 'input' }) as Tool['inputSchema'])
    : EMPTY_OBJECT_JSON_SCHEMA,

// After
inputSchema: tool.inputSchema
    ? ({ type: 'object' as const, ...schemaToJson(tool.inputSchema, { io: 'input' }) } as Tool['inputSchema'])
    : EMPTY_OBJECT_JSON_SCHEMA,

Spreading type: 'object' before the schema output is a no-op for z.object() (already has type) and adds the required field for unions, intersections, etc. This is semantically correct because discriminated unions are always unions of object types (each variant shares a discriminator key).

The result is valid JSON Schema that satisfies the protocol:

{
  "type": "object",
  "oneOf": [
    { "type": "object", "properties": { "type": { "const": "page" }, ... }, "required": ["type", "search"] },
    { "type": "object", "properties": { "type": { "const": "quiz" }, ... }, "required": ["type", "search"] }
  ]
}

Why not other approaches

  • Auto-flatten unions into a merged object: complex, lossy (loses "exactly one of these shapes" semantics)
  • Throw an error: doesn't solve the problem
  • Change schemaToJson: too MCP-specific for a general utility
  • Protocol spec change: unnecessary since type: "object" + oneOf is already valid JSON Schema

…od schemas

z.toJSONSchema() on z.discriminatedUnion() produces { oneOf: [...] }
without a top-level type field. The MCP protocol requires
inputSchema.type === "object" (spec.types.ts), so clients that
validate the tools/list response reject discriminated union schemas.

Since discriminated unions are always unions of object types (each
variant shares a discriminator key), type: "object" is semantically
correct at the top level. Spreading it before the schema output
is a no-op for z.object() (already has type) and adds the required
field for unions, intersections, etc.

Fixes: modelcontextprotocol#1643
@MayCXC MayCXC requested a review from a team as a code owner March 12, 2026 22:19
@changeset-bot
Copy link

changeset-bot bot commented Mar 12, 2026

🦋 Changeset detected

Latest commit: c624fc7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@modelcontextprotocol/server Patch
@modelcontextprotocol/express Patch
@modelcontextprotocol/hono Patch
@modelcontextprotocol/node Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 12, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@1675

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@1675

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@1675

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@1675

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@1675

commit: 2ff1317

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

registerTool() silently drops inputSchema for z.discriminatedUnion() — normalizeObjectSchema only passes z.object()

1 participant