Describe the bug
tools/list advertises tool schemas with "$schema": "http://json-schema.org/draft-07/schema#", even when the tool is defined with Zod v4 — whose native toJSONSchema() emits 2020-12 by default.
Per SEP-1613, JSON Schema 2020-12 is the default dialect for embedded schemas in MCP messages. Because the SDK emits an explicit older dialect rather than omitting $schema, strict clients reject the tool definition outright — every tool on the server becomes unusable, and the failure happens before any tool call is dispatched.
This is distinct from #745 (closed): that issue concerned the Zod v3 path via zod-to-json-schema. The bug reported here is on the Zod v4 path, which is routed through zod/v4-mini's toJSONSchema but is explicitly downgraded to draft-7 by the SDK's own default.
To Reproduce
package.json:
json
{
"type": "module",
"dependencies": {
"@modelcontextprotocol/sdk": "1.30.0",
"zod": "^4.4.3"
}
}
server.mjs:
js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const server = new McpServer({ name: 'repro', version: '1.0.0' });
server.registerTool('echo', {
description: 'Echo a message',
inputSchema: { message: z.string() },
outputSchema: { echoed: z.string() },
}, async ({ message }) => ({
content: [{ type: 'text', text: message }],
structuredContent: { echoed: message },
}));
await server.connect(new StdioServerTransport());
probe.mjs — sends initialize, then tools/list, and prints the advertised dialect:
js
import { spawn } from 'node:child_process';
const p = spawn('node', ['server.mjs'], { stdio: ['pipe', 'pipe', 'ignore'] });
let buf = '';
p.stdout.on('data', d => {
buf += d.toString();
for (const l of buf.split('\n')) {
if (!l.trim().startsWith('{')) continue;
let m; try { m = JSON.parse(l); } catch { continue; }
if (m.id === 1) p.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list' }) + '\n');
if (m.id === 2) {
const t = m.result.tools[0];
console.log('inputSchema.$schema =', t.inputSchema?.$schema);
console.log('outputSchema.$schema =', t.outputSchema?.$schema);
process.exit(0);
}
}
});
p.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize',
params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'p', version: '1' } } }) + '\n');
Run node probe.mjs.
Actual behavior
inputSchema.$schema = http://json-schema.org/draft-07/schema#
outputSchema.$schema = http://json-schema.org/draft-07/schema#
For comparison, the same Zod v4 schema converted directly:
js
> toJSONSchema(z.object({ echoed: z.string() })).$schema
'https://json-schema.org/draft/2020-12/schema'
So the capability is already there — the SDK opts out of it.
Expected behavior
Either https://json-schema.org/draft/2020-12/schema, or no $schema key at all (which SEP-1613 makes equivalent to 2020-12).
Root cause
src/server/mcp.ts calls the conversion helper without a target:
ts
inputSchema: (() => {
const obj = normalizeObjectSchema(tool.inputSchema);
return obj
? toJsonSchemaCompat(obj, { strictUnions: true, pipeStrategy: 'input' })
: EMPTY_OBJECT_JSON_SCHEMA;
})(),
// ...
toolDefinition.outputSchema = toJsonSchemaCompat(obj, {
strictUnions: true,
pipeStrategy: 'output',
});
and src/server/zod-json-schema-compat.ts maps "no target" to the legacy dialect:
ts
function mapMiniTarget(t) {
if (!t) return 'draft-7'; // ← here
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 // ← and here
}
Impact
Any server built on SDK 1.30.0 with Zod v4 is rejected wholesale by clients that enforce SEP-1613. Observed in the wild on obsidian-mcp-server 3.2.9 and 3.2.12, where all 12 tools failed with:
Tool 'obsidian_get_note' has an invalid outputSchema: JSON Schema declares an
unsupported dialect ("$schema": "http://json-schema.org/draft-07/schema#").
The default validator supports JSON Schema 2020-12 only.
There is no user-side workaround: upgrading the server package does not help (the SDK is a transitive dependency), and 1.30.0 is the latest published SDK. The only fix available today is patching dist/{esm,cjs}/server/zod-json-schema-compat.js in place, which is erased by any reinstall.
Suggested fix
Change the default in mapMiniTarget from 'draft-7' to 'draft-2020-12' for the Zod v4 branch, so the SDK matches SEP-1613 unless a caller explicitly asks for draft-7.
Patching only that default (both the if (!t) branch and the trailing fallback) is sufficient — verified: all 12 tools of the affected server then advertise 2020-12 and the client accepts them.
A more conservative variant would be to pass target: 'draft-2020-12' explicitly at the two toJsonSchemaCompat call sites in mcp.ts, leaving mapMiniTarget's default untouched for other callers. Either resolves the reported failure.
Environment
|
-- | --
@modelcontextprotocol/sdk | 1.30.0 (latest published)
zod | 4.4.3
zod-to-json-schema | 3.25.2 (transitive)
Node.js | 24.14.0
OS | macOS
Protocol version negotiated | 2025-06-18
Related
Describe the bug
tools/list advertises tool schemas with "$schema": "http://json-schema.org/draft-07/schema#", even when the tool is defined with Zod v4 — whose native toJSONSchema() emits 2020-12 by default.
Per SEP-1613, JSON Schema 2020-12 is the default dialect for embedded schemas in MCP messages. Because the SDK emits an explicit older dialect rather than omitting $schema, strict clients reject the tool definition outright — every tool on the server becomes unusable, and the failure happens before any tool call is dispatched.
This is distinct from #745 (closed): that issue concerned the Zod v3 path via zod-to-json-schema. The bug reported here is on the Zod v4 path, which is routed through zod/v4-mini's toJSONSchema but is explicitly downgraded to draft-7 by the SDK's own default.
To Reproduce
package.json:
json
{
"type": "module",
"dependencies": {
"@modelcontextprotocol/sdk": "1.30.0",
"zod": "^4.4.3"
}
}
server.mjs:
js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const server = new McpServer({ name: 'repro', version: '1.0.0' });
server.registerTool('echo', {
description: 'Echo a message',
inputSchema: { message: z.string() },
outputSchema: { echoed: z.string() },
}, async ({ message }) => ({
content: [{ type: 'text', text: message }],
structuredContent: { echoed: message },
}));
await server.connect(new StdioServerTransport());
probe.mjs — sends initialize, then tools/list, and prints the advertised dialect:
js
import { spawn } from 'node:child_process';
const p = spawn('node', ['server.mjs'], { stdio: ['pipe', 'pipe', 'ignore'] });
let buf = '';
p.stdout.on('data', d => {
buf += d.toString();
for (const l of buf.split('\n')) {
if (!l.trim().startsWith('{')) continue;
let m; try { m = JSON.parse(l); } catch { continue; }
if (m.id === 1) p.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list' }) + '\n');
if (m.id === 2) {
const t = m.result.tools[0];
console.log('inputSchema.$schema =', t.inputSchema?.$schema);
console.log('outputSchema.$schema =', t.outputSchema?.$schema);
process.exit(0);
}
}
});
p.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize',
params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'p', version: '1' } } }) + '\n');
Run node probe.mjs.
Actual behavior
inputSchema.$schema = http://json-schema.org/draft-07/schema#
outputSchema.$schema = http://json-schema.org/draft-07/schema#
For comparison, the same Zod v4 schema converted directly:
js
toJSONSchema(z.object({ echoed: z.string() })).$schema
'https://json-schema.org/draft/2020-12/schema'
So the capability is already there — the SDK opts out of it.
Expected behavior
Either https://json-schema.org/draft/2020-12/schema, or no $schema key at all (which SEP-1613 makes equivalent to 2020-12).
Root cause
src/server/mcp.ts calls the conversion helper without a target:
ts
inputSchema: (() => {
const obj = normalizeObjectSchema(tool.inputSchema);
return obj
? toJsonSchemaCompat(obj, { strictUnions: true, pipeStrategy: 'input' })
: EMPTY_OBJECT_JSON_SCHEMA;
})(),
// ...
toolDefinition.outputSchema = toJsonSchemaCompat(obj, {
strictUnions: true,
pipeStrategy: 'output',
});
and src/server/zod-json-schema-compat.ts maps "no target" to the legacy dialect:
ts
function mapMiniTarget(t) {
if (!t) return 'draft-7'; // ← here
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 // ← and here
}
Impact
Any server built on SDK 1.30.0 with Zod v4 is rejected wholesale by clients that enforce SEP-1613. Observed in the wild on obsidian-mcp-server 3.2.9 and 3.2.12, where all 12 tools failed with:
Tool 'obsidian_get_note' has an invalid outputSchema: JSON Schema declares an
unsupported dialect ("$schema": "http://json-schema.org/draft-07/schema#").
The default validator supports JSON Schema 2020-12 only.
There is no user-side workaround: upgrading the server package does not help (the SDK is a transitive dependency), and 1.30.0 is the latest published SDK. The only fix available today is patching dist/{esm,cjs}/server/zod-json-schema-compat.js in place, which is erased by any reinstall.
Suggested fix
Change the default in mapMiniTarget from 'draft-7' to 'draft-2020-12' for the Zod v4 branch, so the SDK matches SEP-1613 unless a caller explicitly asks for draft-7.
Patching only that default (both the if (!t) branch and the trailing fallback) is sufficient — verified: all 12 tools of the affected server then advertise 2020-12 and the client accepts them.
A more conservative variant would be to pass target: 'draft-2020-12' explicitly at the two toJsonSchemaCompat call sites in mcp.ts, leaving mapMiniTarget's default untouched for other callers. Either resolves the reported failure.
Environment
@modelcontextprotocol/sdk 1.30.0 (latest published)
zod 4.4.3
zod-to-json-schema 3.25.2 (transitive)
Node.js 24.14.0
OS macOS
Protocol version negotiated 2025-06-18
Related
#745 — same symptom on the Zod v3 path, closed; this report covers the v4 path.
SEP-1613 — establishes 2020-12 as the default dialect for embedded schemas.
Describe the bug
tools/listadvertises tool schemas with"$schema": "http://json-schema.org/draft-07/schema#", even when the tool is defined with Zod v4 — whose nativetoJSONSchema()emits2020-12by default.Per SEP-1613, JSON Schema 2020-12 is the default dialect for embedded schemas in MCP messages. Because the SDK emits an explicit older dialect rather than omitting
$schema, strict clients reject the tool definition outright — every tool on the server becomes unusable, and the failure happens before any tool call is dispatched.This is distinct from #745 (closed): that issue concerned the Zod v3 path via
zod-to-json-schema. The bug reported here is on the Zod v4 path, which is routed throughzod/v4-mini'stoJSONSchemabut is explicitly downgraded to draft-7 by the SDK's own default.To Reproduce
package.json:server.mjs:probe.mjs— sendsinitialize, thentools/list, and prints the advertised dialect:Run
node probe.mjs.Actual behavior
For comparison, the same Zod v4 schema converted directly:
So the capability is already there — the SDK opts out of it.
Expected behavior
Either
https://json-schema.org/draft/2020-12/schema, or no$schemakey at all (which SEP-1613 makes equivalent to 2020-12).Root cause
src/server/mcp.tscalls the conversion helper without atarget:and
src/server/zod-json-schema-compat.tsmaps "no target" to the legacy dialect:Impact
Any server built on SDK 1.30.0 with Zod v4 is rejected wholesale by clients that enforce SEP-1613. Observed in the wild on
obsidian-mcp-server3.2.9 and 3.2.12, where all 12 tools failed with:There is no user-side workaround: upgrading the server package does not help (the SDK is a transitive dependency), and 1.30.0 is the latest published SDK. The only fix available today is patching
dist/{esm,cjs}/server/zod-json-schema-compat.jsin place, which is erased by any reinstall.Suggested fix
Change the default in
mapMiniTargetfrom'draft-7'to'draft-2020-12'for the Zod v4 branch, so the SDK matches SEP-1613 unless a caller explicitly asks for draft-7.Patching only that default (both the
if (!t)branch and the trailing fallback) is sufficient — verified: all 12 tools of the affected server then advertise2020-12and the client accepts them.A more conservative variant would be to pass
target: 'draft-2020-12'explicitly at the twotoJsonSchemaCompatcall sites inmcp.ts, leavingmapMiniTarget's default untouched for other callers. Either resolves the reported failure.Environment
Related
- MCP TypeScript SDK generates JSON Schema draft-07, breaking compatibility with modern MCP clients requiring draft-2020-12 #745 — same symptom on the Zod v3 path, closed; this report covers the v4 path.
- SEP-1613 — establishes 2020-12 as the default dialect for embedded schemas.
Describe the bugtools/list advertises tool schemas with "$schema": "http://json-schema.org/draft-07/schema#", even when the tool is defined with Zod v4 — whose native toJSONSchema() emits 2020-12 by default.
Per SEP-1613, JSON Schema 2020-12 is the default dialect for embedded schemas in MCP messages. Because the SDK emits an explicit older dialect rather than omitting $schema, strict clients reject the tool definition outright — every tool on the server becomes unusable, and the failure happens before any tool call is dispatched.
This is distinct from #745 (closed): that issue concerned the Zod v3 path via zod-to-json-schema. The bug reported here is on the Zod v4 path, which is routed through zod/v4-mini's toJSONSchema but is explicitly downgraded to draft-7 by the SDK's own default.
To Reproduce
package.json:
json
{
"type": "module",
"dependencies": {
"@modelcontextprotocol/sdk": "1.30.0",
"zod": "^4.4.3"
}
}
server.mjs:
js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const server = new McpServer({ name: 'repro', version: '1.0.0' });
server.registerTool('echo', {
description: 'Echo a message',
inputSchema: { message: z.string() },
outputSchema: { echoed: z.string() },
}, async ({ message }) => ({
content: [{ type: 'text', text: message }],
structuredContent: { echoed: message },
}));
await server.connect(new StdioServerTransport());
probe.mjs — sends initialize, then tools/list, and prints the advertised dialect:
js
import { spawn } from 'node:child_process';
const p = spawn('node', ['server.mjs'], { stdio: ['pipe', 'pipe', 'ignore'] });
let buf = '';
p.stdout.on('data', d => {
buf += d.toString();
for (const l of buf.split('\n')) {
if (!l.trim().startsWith('{')) continue;
let m; try { m = JSON.parse(l); } catch { continue; }
if (m.id === 1) p.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list' }) + '\n');
if (m.id === 2) {
const t = m.result.tools[0];
console.log('inputSchema.$schema =', t.inputSchema?.$schema);
console.log('outputSchema.$schema =', t.outputSchema?.$schema);
process.exit(0);
}
}
});
p.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize',
params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'p', version: '1' } } }) + '\n');
Run node probe.mjs.
Actual behavior
inputSchema.$schema = http://json-schema.org/draft-07/schema#
outputSchema.$schema = http://json-schema.org/draft-07/schema#
For comparison, the same Zod v4 schema converted directly:
js
So the capability is already there — the SDK opts out of it.
Expected behavior
Either https://json-schema.org/draft/2020-12/schema, or no $schema key at all (which SEP-1613 makes equivalent to 2020-12).
Root cause
src/server/mcp.ts calls the conversion helper without a target:
ts
inputSchema: (() => {
const obj = normalizeObjectSchema(tool.inputSchema);
return obj
? toJsonSchemaCompat(obj, { strictUnions: true, pipeStrategy: 'input' })
: EMPTY_OBJECT_JSON_SCHEMA;
})(),
// ...
toolDefinition.outputSchema = toJsonSchemaCompat(obj, {
strictUnions: true,
pipeStrategy: 'output',
});
and src/server/zod-json-schema-compat.ts maps "no target" to the legacy dialect:
ts
function mapMiniTarget(t) {
if (!t) return 'draft-7'; // ← here
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 // ← and here
}
Impact
Any server built on SDK 1.30.0 with Zod v4 is rejected wholesale by clients that enforce SEP-1613. Observed in the wild on obsidian-mcp-server 3.2.9 and 3.2.12, where all 12 tools failed with:
Tool 'obsidian_get_note' has an invalid outputSchema: JSON Schema declares an
unsupported dialect ("$schema": "http://json-schema.org/draft-07/schema#").
The default validator supports JSON Schema 2020-12 only.
There is no user-side workaround: upgrading the server package does not help (the SDK is a transitive dependency), and 1.30.0 is the latest published SDK. The only fix available today is patching dist/{esm,cjs}/server/zod-json-schema-compat.js in place, which is erased by any reinstall.
Suggested fix
Change the default in mapMiniTarget from 'draft-7' to 'draft-2020-12' for the Zod v4 branch, so the SDK matches SEP-1613 unless a caller explicitly asks for draft-7.
Patching only that default (both the if (!t) branch and the trailing fallback) is sufficient — verified: all 12 tools of the affected server then advertise 2020-12 and the client accepts them.
A more conservative variant would be to pass target: 'draft-2020-12' explicitly at the two toJsonSchemaCompat call sites in mcp.ts, leaving mapMiniTarget's default untouched for other callers. Either resolves the reported failure.
Environment
@modelcontextprotocol/sdk 1.30.0 (latest published)
zod 4.4.3
zod-to-json-schema 3.25.2 (transitive)
Node.js 24.14.0
OS macOS
Protocol version negotiated 2025-06-18
Related
#745 — same symptom on the Zod v3 path, closed; this report covers the v4 path.
SEP-1613 — establishes 2020-12 as the default dialect for embedded schemas.