Skip to content

[SEP-1613 regression] tools/list still emits draft-07 in 1.29.0 — target option missing from mcp.js toJsonSchemaCompat calls #2084

@rsalus

Description

@rsalus

Describe the bug

SEP-1613 (tracked in #1057, closed 2025-11-21) was supposed to make JSON Schema 2020-12 the default dialect for tool inputSchema / outputSchema emission in tools/list. In @modelcontextprotocol/sdk@1.29.0, tools/list still emits draft-07 because mcp.js calls toJsonSchemaCompat(...) without passing a target option, and toJsonSchemaCompat's mapMiniTarget(undefined) falls back to 'draft-7'.

This appears to be a regression / partial implementation of SEP-1613; the SDK has the wiring for target: 'draft-2020-12' in zod-json-schema-compat.js, but the call sites in mcp.js don't pass it.

Impact

Every server built on @modelcontextprotocol/sdk since SEP-1613 closed advertises draft-07 schemas on tools/list, contrary to the MCP 2025-11-25 spec which mandates 2020-12. Strict validating clients reject those servers outright (#745 reported Claude Code returning 400s against Mapbox MCP for exactly this reason). For most simple schemas the break is silent (draft-07 is a structural subset of 2020-12), but tuple/composition-heavy schemas lose 2020-12-only keywords like prefixItems.

To Reproduce

  1. Install @modelcontextprotocol/sdk@1.29.0 and zod@^4.
  2. Register a tool with any Zod outputSchema.
  3. Call tools/list.
  4. Observe $schema: "http://json-schema.org/draft-07/schema#" on every tool's inputSchema and outputSchema (or absent — falls back).

Expected behavior

Per SEP-1613, tools/list advertises $schema: "https://json-schema.org/draft/2020-12/schema" by default.

Code path evidence (1.29.0)

dist/esm/server/mcp.js:76-95 — both inputSchema and outputSchema emission paths call toJsonSchemaCompat with NO target option:

inputSchema: (() => {
    const obj = normalizeObjectSchema(tool.inputSchema);
    return obj
        ? toJsonSchemaCompat(obj, {
            strictUnions: true,
            pipeStrategy: 'input'
        })
        : EMPTY_OBJECT_JSON_SCHEMA;
})(),
...
if (tool.outputSchema) {
    const obj = normalizeObjectSchema(tool.outputSchema);
    if (obj) {
        toolDefinition.outputSchema = toJsonSchemaCompat(obj, {
            strictUnions: true,
            pipeStrategy: 'output'
        });
    }
}

dist/esm/server/zod-json-schema-compat.js:9-18mapMiniTarget(undefined) returns 'draft-7':

function mapMiniTarget(t) {
    if (!t)
        return 'draft-7';
    if (t === 'jsonSchema7' || t === 'draft-7')
        return 'draft-7';
    if (t === 'jsonSchema2019-09' || t === 'draft-2020-12')
        return 'draft-2020-12';
    return 'draft-7'; // fallback
}

So mcp.js → toJsonSchemaCompat(obj, { strictUnions, pipeStrategy }) resolves to mapMiniTarget(undefined) === 'draft-7'.

Suggested fix

Pass target: 'draft-2020-12' in both mcp.js call sites (line 78 for inputSchema, line 91 for outputSchema):

 inputSchema: (() => {
     const obj = normalizeObjectSchema(tool.inputSchema);
     return obj
         ? toJsonSchemaCompat(obj, {
+            target: 'draft-2020-12',
             strictUnions: true,
             pipeStrategy: 'input'
         })
         : EMPTY_OBJECT_JSON_SCHEMA;
 })(),
 if (tool.outputSchema) {
     const obj = normalizeObjectSchema(tool.outputSchema);
     if (obj) {
         toolDefinition.outputSchema = toJsonSchemaCompat(obj, {
+            target: 'draft-2020-12',
             strictUnions: true,
             pipeStrategy: 'output'
         });
     }
 }

Alternatively, change mapMiniTarget(undefined) to default to 'draft-2020-12' per SEP-1613's intent, but explicit passing at the call sites is safer for callers that might rely on the legacy draft-7 default.

Test to verify

Register a tool with a non-trivial Zod v4 schema (including a tuple, for prefixItems-evidence), call tools/list, and assert:

  1. tools[].inputSchema.$schema === 'https://json-schema.org/draft/2020-12/schema'
  2. tools[].outputSchema.$schema === 'https://json-schema.org/draft/2020-12/schema' (when registered)
  3. Tuple-array fields emit prefixItems (not items: [...])

Related

Environment

  • @modelcontextprotocol/sdk@1.29.0
  • zod@^4.4.3 (also reproduces on v3)
  • Node 20+

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions