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
- Install
@modelcontextprotocol/sdk@1.29.0 and zod@^4.
- Register a tool with any Zod
outputSchema.
- Call
tools/list.
- 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-18 — mapMiniTarget(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:
tools[].inputSchema.$schema === 'https://json-schema.org/draft/2020-12/schema'
tools[].outputSchema.$schema === 'https://json-schema.org/draft/2020-12/schema' (when registered)
- Tuple-array fields emit
prefixItems (not items: [...])
Related
Environment
@modelcontextprotocol/sdk@1.29.0
zod@^4.4.3 (also reproduces on v3)
- Node 20+
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/outputSchemaemission intools/list. In@modelcontextprotocol/sdk@1.29.0,tools/liststill emits draft-07 becausemcp.jscallstoJsonSchemaCompat(...)without passing atargetoption, andtoJsonSchemaCompat'smapMiniTarget(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'inzod-json-schema-compat.js, but the call sites inmcp.jsdon't pass it.Impact
Every server built on
@modelcontextprotocol/sdksince SEP-1613 closed advertises draft-07 schemas ontools/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 likeprefixItems.To Reproduce
@modelcontextprotocol/sdk@1.29.0andzod@^4.outputSchema.tools/list.$schema: "http://json-schema.org/draft-07/schema#"on every tool'sinputSchemaandoutputSchema(or absent — falls back).Expected behavior
Per SEP-1613,
tools/listadvertises$schema: "https://json-schema.org/draft/2020-12/schema"by default.Code path evidence (
1.29.0)dist/esm/server/mcp.js:76-95— bothinputSchemaandoutputSchemaemission paths calltoJsonSchemaCompatwith NOtargetoption:dist/esm/server/zod-json-schema-compat.js:9-18—mapMiniTarget(undefined)returns'draft-7':So
mcp.js → toJsonSchemaCompat(obj, { strictUnions, pipeStrategy })resolves tomapMiniTarget(undefined) === 'draft-7'.Suggested fix
Pass
target: 'draft-2020-12'in bothmcp.jscall 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), calltools/list, and assert:tools[].inputSchema.$schema === 'https://json-schema.org/draft/2020-12/schema'tools[].outputSchema.$schema === 'https://json-schema.org/draft/2020-12/schema'(when registered)prefixItems(notitems: [...])Related
patch-packagein the interim.Environment
@modelcontextprotocol/sdk@1.29.0zod@^4.4.3(also reproduces on v3)