[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613) - #2085

Open
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12
Open

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613)#2085
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12

Conversation

@rsalus

@rsalusrsalus commented May 14, 2026

Copy link
Copy Markdown

Fixes#2084.

Summary

McpServer's tools/list handler calls toJsonSchemaCompat() without a target, so mapMiniTarget(undefined) returns 'draft-7' and every inputSchema/outputSchema advertises draft-07 instead of the 2020-12 dialect SEP-1613 requires.

The target: 'draft-2020-12' plumbing was added in #1135, but the mcp.ts call sites weren't updated to use it. This PR does that, plus an extra fix for the Zod v3 path that the 2-liner doesn't reach.

What changed

src/server/mcp.ts — pass target: 'draft-2020-12' at both call sites. That's the 2-line patch from the issue. For Zod v4 it's the whole fix: z4mini.toJSONSchema then emits the right $schema and uses prefixItems for tuples.

src/server/zod-json-schema-compat.ts — the v3 branch needed more. zod-to-json-schema has no draft-2020-12 target (only jsonSchema7 and jsonSchema2019-09), so passing the option through is a silent no-op. The v3 branch now overrides $schema to https://json-schema.org/draft/2020-12/schema when 2020-12 is requested.

A caveat on the v3 override: the keywords zod-to-json-schema actually emits (type, properties, required, additionalProperties, oneOf/anyOf/allOf, enum, const) have identical semantics in draft-07 and 2020-12. The one keyword that diverges is tuples. items: [...] is a positional tuple in draft-07 but means "every item matches one of these schemas" in 2020-12. A Zod v3 tuple stamped as 2020-12 is therefore subtly wrong; an inline comment in the file flags this and points at Zod v4 for native prefixItems. IMO, leaving v3 stuck on draft-07 is worse than the caveat.

test/server/mcp.test.ts: three new tests in the existing zodTestMatrix block so they run on v3 and v4: inputSchema $schema, outputSchema $schema, and a v4-only tuple → prefixItems check.

.changeset/fix-sep-1613-emit-2020-12.md: patch-level changeset.

Tests

  • npm test: 1584 / 1584 pass
  • npm run typecheck: clean
  • npm run lint: clean

On v2.0.0-alpha

main doesn't have this bug. standardSchemaToJsonSchema hardcodes target: 'draft-2020-12' at the conversion site, and v2 dropped Zod v3.

Test plan

  • New tests fail on unpatched v1.x
  • New tests pass with the fix
  • Full npm test (1584 / 1584)
  • npm run typecheck passes
  • npm run lint passes
  • Changeset added

McpServer's tools/list handler called toJsonSchemaCompat() without the
target option, so mapMiniTarget(undefined) fell back to 'draft-7' and
every inputSchema/outputSchema advertised draft-07, contradicting
SEP-1613 / MCP 2025-11-25 spec.
Pass target: 'draft-2020-12' at both call sites. For the Zod v4 branch,
this surfaces native 2020-12 output (including prefixItems for tuples).
For the Zod v3 branch — which uses zod-to-json-schema, a library with
no 2020-12 target — overlay $schema in toJsonSchemaCompat when 2020-12
is requested; the emitted keyword subset (type, properties, required,
additionalProperties, oneOf/anyOf/allOf, enum, const) is unchanged
between draft-07 and 2020-12.
Tests cover inputSchema and outputSchema $schema across both Zod
versions, plus Zod v4 tuples emitting prefixItems.
Fixesmodelcontextprotocol#2084
@rsalus
rsalus requested a review from a team as a code ownerMay 14, 2026 05:35
@changeset-bot

changeset-botBot commented May 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f16599

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

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

pkg-pr-newBot commented May 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@2085

commit: 4f16599

Wrap long line in .changeset/fix-sep-1613-emit-2020-12.md to satisfy
the repo's prettier config. No semantic change.
@anatoly314

Copy link
Copy Markdown

Adding a downstream data point in support of this, since it's been open a while: this is now causing real breakage for end users, not just spec non-conformance.

Two independent users of my server (@ankimcp/anki-mcp-server, tracked in ankimcp/anki-mcp-server#53) hit this within a day of each other. The client-side error is explicit about the dialect:

Error: Tool 'listDecks' 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

Reproduced on Claude Desktop 1.28929.0 (Code tab). Every tool call fails at the same validation step, so the server is effectively unusable in that client.

What makes this hard to work around downstream: on 1.30.0 there is no way to opt in. server/mcp.js calls toJsonSchemaCompat at both the inputSchema and outputSchema sites with a hardcoded options object and never passes target, so mapMiniTarget(undefined) falls through to draft-7. normalizeObjectSchema only accepts Zod schemas or raw shapes, and there's no fromJsonSchema path, so a server author can't supply a pre-built 2020-12 schema either. The capability exists in mapMiniTarget — it just isn't reachable.

That leaves downstream servers patching the emitted schema after the fact, which is why I'd rather see this land. Happy to test a build against the affected client if that's useful.

Sign up for freeto 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.

2 participants

@rsalus@anatoly314
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613) - #2085

Open
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12
Open

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613)#2085
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12

Conversation

@rsalus

@rsalusrsalus commented May 14, 2026

Copy link
Copy Markdown

Fixes#2084.

Summary

McpServer's tools/list handler calls toJsonSchemaCompat() without a target, so mapMiniTarget(undefined) returns 'draft-7' and every inputSchema/outputSchema advertises draft-07 instead of the 2020-12 dialect SEP-1613 requires.

The target: 'draft-2020-12' plumbing was added in #1135, but the mcp.ts call sites weren't updated to use it. This PR does that, plus an extra fix for the Zod v3 path that the 2-liner doesn't reach.

What changed

src/server/mcp.ts — pass target: 'draft-2020-12' at both call sites. That's the 2-line patch from the issue. For Zod v4 it's the whole fix: z4mini.toJSONSchema then emits the right $schema and uses prefixItems for tuples.

src/server/zod-json-schema-compat.ts — the v3 branch needed more. zod-to-json-schema has no draft-2020-12 target (only jsonSchema7 and jsonSchema2019-09), so passing the option through is a silent no-op. The v3 branch now overrides $schema to https://json-schema.org/draft/2020-12/schema when 2020-12 is requested.

A caveat on the v3 override: the keywords zod-to-json-schema actually emits (type, properties, required, additionalProperties, oneOf/anyOf/allOf, enum, const) have identical semantics in draft-07 and 2020-12. The one keyword that diverges is tuples. items: [...] is a positional tuple in draft-07 but means "every item matches one of these schemas" in 2020-12. A Zod v3 tuple stamped as 2020-12 is therefore subtly wrong; an inline comment in the file flags this and points at Zod v4 for native prefixItems. IMO, leaving v3 stuck on draft-07 is worse than the caveat.

test/server/mcp.test.ts: three new tests in the existing zodTestMatrix block so they run on v3 and v4: inputSchema $schema, outputSchema $schema, and a v4-only tuple → prefixItems check.

.changeset/fix-sep-1613-emit-2020-12.md: patch-level changeset.

Tests

  • npm test: 1584 / 1584 pass
  • npm run typecheck: clean
  • npm run lint: clean

On v2.0.0-alpha

main doesn't have this bug. standardSchemaToJsonSchema hardcodes target: 'draft-2020-12' at the conversion site, and v2 dropped Zod v3.

Test plan

  • New tests fail on unpatched v1.x
  • New tests pass with the fix
  • Full npm test (1584 / 1584)
  • npm run typecheck passes
  • npm run lint passes
  • Changeset added

McpServer's tools/list handler called toJsonSchemaCompat() without the
target option, so mapMiniTarget(undefined) fell back to 'draft-7' and
every inputSchema/outputSchema advertised draft-07, contradicting
SEP-1613 / MCP 2025-11-25 spec.
Pass target: 'draft-2020-12' at both call sites. For the Zod v4 branch,
this surfaces native 2020-12 output (including prefixItems for tuples).
For the Zod v3 branch — which uses zod-to-json-schema, a library with
no 2020-12 target — overlay $schema in toJsonSchemaCompat when 2020-12
is requested; the emitted keyword subset (type, properties, required,
additionalProperties, oneOf/anyOf/allOf, enum, const) is unchanged
between draft-07 and 2020-12.
Tests cover inputSchema and outputSchema $schema across both Zod
versions, plus Zod v4 tuples emitting prefixItems.
Fixesmodelcontextprotocol#2084
@rsalus
rsalus requested a review from a team as a code ownerMay 14, 2026 05:35
@changeset-bot

changeset-botBot commented May 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f16599

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

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

pkg-pr-newBot commented May 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@2085

commit: 4f16599

Wrap long line in .changeset/fix-sep-1613-emit-2020-12.md to satisfy
the repo's prettier config. No semantic change.
@anatoly314

Copy link
Copy Markdown

Adding a downstream data point in support of this, since it's been open a while: this is now causing real breakage for end users, not just spec non-conformance.

Two independent users of my server (@ankimcp/anki-mcp-server, tracked in ankimcp/anki-mcp-server#53) hit this within a day of each other. The client-side error is explicit about the dialect:

Error: Tool 'listDecks' 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

Reproduced on Claude Desktop 1.28929.0 (Code tab). Every tool call fails at the same validation step, so the server is effectively unusable in that client.

What makes this hard to work around downstream: on 1.30.0 there is no way to opt in. server/mcp.js calls toJsonSchemaCompat at both the inputSchema and outputSchema sites with a hardcoded options object and never passes target, so mapMiniTarget(undefined) falls through to draft-7. normalizeObjectSchema only accepts Zod schemas or raw shapes, and there's no fromJsonSchema path, so a server author can't supply a pre-built 2020-12 schema either. The capability exists in mapMiniTarget — it just isn't reachable.

That leaves downstream servers patching the emitted schema after the fact, which is why I'd rather see this land. Happy to test a build against the affected client if that's useful.

Sign up for freeto 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.

2 participants

@rsalus@anatoly314
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613) - #2085

Open
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12
Open

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613)#2085
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12

Conversation

@rsalus

@rsalusrsalus commented May 14, 2026

Copy link
Copy Markdown

Fixes#2084.

Summary

McpServer's tools/list handler calls toJsonSchemaCompat() without a target, so mapMiniTarget(undefined) returns 'draft-7' and every inputSchema/outputSchema advertises draft-07 instead of the 2020-12 dialect SEP-1613 requires.

The target: 'draft-2020-12' plumbing was added in #1135, but the mcp.ts call sites weren't updated to use it. This PR does that, plus an extra fix for the Zod v3 path that the 2-liner doesn't reach.

What changed

src/server/mcp.ts — pass target: 'draft-2020-12' at both call sites. That's the 2-line patch from the issue. For Zod v4 it's the whole fix: z4mini.toJSONSchema then emits the right $schema and uses prefixItems for tuples.

src/server/zod-json-schema-compat.ts — the v3 branch needed more. zod-to-json-schema has no draft-2020-12 target (only jsonSchema7 and jsonSchema2019-09), so passing the option through is a silent no-op. The v3 branch now overrides $schema to https://json-schema.org/draft/2020-12/schema when 2020-12 is requested.

A caveat on the v3 override: the keywords zod-to-json-schema actually emits (type, properties, required, additionalProperties, oneOf/anyOf/allOf, enum, const) have identical semantics in draft-07 and 2020-12. The one keyword that diverges is tuples. items: [...] is a positional tuple in draft-07 but means "every item matches one of these schemas" in 2020-12. A Zod v3 tuple stamped as 2020-12 is therefore subtly wrong; an inline comment in the file flags this and points at Zod v4 for native prefixItems. IMO, leaving v3 stuck on draft-07 is worse than the caveat.

test/server/mcp.test.ts: three new tests in the existing zodTestMatrix block so they run on v3 and v4: inputSchema $schema, outputSchema $schema, and a v4-only tuple → prefixItems check.

.changeset/fix-sep-1613-emit-2020-12.md: patch-level changeset.

Tests

  • npm test: 1584 / 1584 pass
  • npm run typecheck: clean
  • npm run lint: clean

On v2.0.0-alpha

main doesn't have this bug. standardSchemaToJsonSchema hardcodes target: 'draft-2020-12' at the conversion site, and v2 dropped Zod v3.

Test plan

  • New tests fail on unpatched v1.x
  • New tests pass with the fix
  • Full npm test (1584 / 1584)
  • npm run typecheck passes
  • npm run lint passes
  • Changeset added

McpServer's tools/list handler called toJsonSchemaCompat() without the
target option, so mapMiniTarget(undefined) fell back to 'draft-7' and
every inputSchema/outputSchema advertised draft-07, contradicting
SEP-1613 / MCP 2025-11-25 spec.
Pass target: 'draft-2020-12' at both call sites. For the Zod v4 branch,
this surfaces native 2020-12 output (including prefixItems for tuples).
For the Zod v3 branch — which uses zod-to-json-schema, a library with
no 2020-12 target — overlay $schema in toJsonSchemaCompat when 2020-12
is requested; the emitted keyword subset (type, properties, required,
additionalProperties, oneOf/anyOf/allOf, enum, const) is unchanged
between draft-07 and 2020-12.
Tests cover inputSchema and outputSchema $schema across both Zod
versions, plus Zod v4 tuples emitting prefixItems.
Fixesmodelcontextprotocol#2084
@rsalus
rsalus requested a review from a team as a code ownerMay 14, 2026 05:35
@changeset-bot

changeset-botBot commented May 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f16599

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

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

pkg-pr-newBot commented May 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@2085

commit: 4f16599

Wrap long line in .changeset/fix-sep-1613-emit-2020-12.md to satisfy
the repo's prettier config. No semantic change.
@anatoly314

Copy link
Copy Markdown

Adding a downstream data point in support of this, since it's been open a while: this is now causing real breakage for end users, not just spec non-conformance.

Two independent users of my server (@ankimcp/anki-mcp-server, tracked in ankimcp/anki-mcp-server#53) hit this within a day of each other. The client-side error is explicit about the dialect:

Error: Tool 'listDecks' 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

Reproduced on Claude Desktop 1.28929.0 (Code tab). Every tool call fails at the same validation step, so the server is effectively unusable in that client.

What makes this hard to work around downstream: on 1.30.0 there is no way to opt in. server/mcp.js calls toJsonSchemaCompat at both the inputSchema and outputSchema sites with a hardcoded options object and never passes target, so mapMiniTarget(undefined) falls through to draft-7. normalizeObjectSchema only accepts Zod schemas or raw shapes, and there's no fromJsonSchema path, so a server author can't supply a pre-built 2020-12 schema either. The capability exists in mapMiniTarget — it just isn't reachable.

That leaves downstream servers patching the emitted schema after the fact, which is why I'd rather see this land. Happy to test a build against the affected client if that's useful.

Sign up for freeto 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.

2 participants

@rsalus@anatoly314
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613) - #2085

Open
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12
Open

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613)#2085
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12

Conversation

@rsalus

@rsalusrsalus commented May 14, 2026

Copy link
Copy Markdown

Fixes#2084.

Summary

McpServer's tools/list handler calls toJsonSchemaCompat() without a target, so mapMiniTarget(undefined) returns 'draft-7' and every inputSchema/outputSchema advertises draft-07 instead of the 2020-12 dialect SEP-1613 requires.

The target: 'draft-2020-12' plumbing was added in #1135, but the mcp.ts call sites weren't updated to use it. This PR does that, plus an extra fix for the Zod v3 path that the 2-liner doesn't reach.

What changed

src/server/mcp.ts — pass target: 'draft-2020-12' at both call sites. That's the 2-line patch from the issue. For Zod v4 it's the whole fix: z4mini.toJSONSchema then emits the right $schema and uses prefixItems for tuples.

src/server/zod-json-schema-compat.ts — the v3 branch needed more. zod-to-json-schema has no draft-2020-12 target (only jsonSchema7 and jsonSchema2019-09), so passing the option through is a silent no-op. The v3 branch now overrides $schema to https://json-schema.org/draft/2020-12/schema when 2020-12 is requested.

A caveat on the v3 override: the keywords zod-to-json-schema actually emits (type, properties, required, additionalProperties, oneOf/anyOf/allOf, enum, const) have identical semantics in draft-07 and 2020-12. The one keyword that diverges is tuples. items: [...] is a positional tuple in draft-07 but means "every item matches one of these schemas" in 2020-12. A Zod v3 tuple stamped as 2020-12 is therefore subtly wrong; an inline comment in the file flags this and points at Zod v4 for native prefixItems. IMO, leaving v3 stuck on draft-07 is worse than the caveat.

test/server/mcp.test.ts: three new tests in the existing zodTestMatrix block so they run on v3 and v4: inputSchema $schema, outputSchema $schema, and a v4-only tuple → prefixItems check.

.changeset/fix-sep-1613-emit-2020-12.md: patch-level changeset.

Tests

  • npm test: 1584 / 1584 pass
  • npm run typecheck: clean
  • npm run lint: clean

On v2.0.0-alpha

main doesn't have this bug. standardSchemaToJsonSchema hardcodes target: 'draft-2020-12' at the conversion site, and v2 dropped Zod v3.

Test plan

  • New tests fail on unpatched v1.x
  • New tests pass with the fix
  • Full npm test (1584 / 1584)
  • npm run typecheck passes
  • npm run lint passes
  • Changeset added

McpServer's tools/list handler called toJsonSchemaCompat() without the
target option, so mapMiniTarget(undefined) fell back to 'draft-7' and
every inputSchema/outputSchema advertised draft-07, contradicting
SEP-1613 / MCP 2025-11-25 spec.
Pass target: 'draft-2020-12' at both call sites. For the Zod v4 branch,
this surfaces native 2020-12 output (including prefixItems for tuples).
For the Zod v3 branch — which uses zod-to-json-schema, a library with
no 2020-12 target — overlay $schema in toJsonSchemaCompat when 2020-12
is requested; the emitted keyword subset (type, properties, required,
additionalProperties, oneOf/anyOf/allOf, enum, const) is unchanged
between draft-07 and 2020-12.
Tests cover inputSchema and outputSchema $schema across both Zod
versions, plus Zod v4 tuples emitting prefixItems.
Fixesmodelcontextprotocol#2084
@rsalus
rsalus requested a review from a team as a code ownerMay 14, 2026 05:35
@changeset-bot

changeset-botBot commented May 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f16599

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

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

pkg-pr-newBot commented May 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@2085

commit: 4f16599

Wrap long line in .changeset/fix-sep-1613-emit-2020-12.md to satisfy
the repo's prettier config. No semantic change.
@anatoly314

Copy link
Copy Markdown

Adding a downstream data point in support of this, since it's been open a while: this is now causing real breakage for end users, not just spec non-conformance.

Two independent users of my server (@ankimcp/anki-mcp-server, tracked in ankimcp/anki-mcp-server#53) hit this within a day of each other. The client-side error is explicit about the dialect:

Error: Tool 'listDecks' 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

Reproduced on Claude Desktop 1.28929.0 (Code tab). Every tool call fails at the same validation step, so the server is effectively unusable in that client.

What makes this hard to work around downstream: on 1.30.0 there is no way to opt in. server/mcp.js calls toJsonSchemaCompat at both the inputSchema and outputSchema sites with a hardcoded options object and never passes target, so mapMiniTarget(undefined) falls through to draft-7. normalizeObjectSchema only accepts Zod schemas or raw shapes, and there's no fromJsonSchema path, so a server author can't supply a pre-built 2020-12 schema either. The capability exists in mapMiniTarget — it just isn't reachable.

That leaves downstream servers patching the emitted schema after the fact, which is why I'd rather see this land. Happy to test a build against the affected client if that's useful.

Sign up for freeto 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.

2 participants

@rsalus@anatoly314
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613) - #2085

Open
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12
Open

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613)#2085
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12

Conversation

@rsalus

@rsalusrsalus commented May 14, 2026

Copy link
Copy Markdown

Fixes#2084.

Summary

McpServer's tools/list handler calls toJsonSchemaCompat() without a target, so mapMiniTarget(undefined) returns 'draft-7' and every inputSchema/outputSchema advertises draft-07 instead of the 2020-12 dialect SEP-1613 requires.

The target: 'draft-2020-12' plumbing was added in #1135, but the mcp.ts call sites weren't updated to use it. This PR does that, plus an extra fix for the Zod v3 path that the 2-liner doesn't reach.

What changed

src/server/mcp.ts — pass target: 'draft-2020-12' at both call sites. That's the 2-line patch from the issue. For Zod v4 it's the whole fix: z4mini.toJSONSchema then emits the right $schema and uses prefixItems for tuples.

src/server/zod-json-schema-compat.ts — the v3 branch needed more. zod-to-json-schema has no draft-2020-12 target (only jsonSchema7 and jsonSchema2019-09), so passing the option through is a silent no-op. The v3 branch now overrides $schema to https://json-schema.org/draft/2020-12/schema when 2020-12 is requested.

A caveat on the v3 override: the keywords zod-to-json-schema actually emits (type, properties, required, additionalProperties, oneOf/anyOf/allOf, enum, const) have identical semantics in draft-07 and 2020-12. The one keyword that diverges is tuples. items: [...] is a positional tuple in draft-07 but means "every item matches one of these schemas" in 2020-12. A Zod v3 tuple stamped as 2020-12 is therefore subtly wrong; an inline comment in the file flags this and points at Zod v4 for native prefixItems. IMO, leaving v3 stuck on draft-07 is worse than the caveat.

test/server/mcp.test.ts: three new tests in the existing zodTestMatrix block so they run on v3 and v4: inputSchema $schema, outputSchema $schema, and a v4-only tuple → prefixItems check.

.changeset/fix-sep-1613-emit-2020-12.md: patch-level changeset.

Tests

  • npm test: 1584 / 1584 pass
  • npm run typecheck: clean
  • npm run lint: clean

On v2.0.0-alpha

main doesn't have this bug. standardSchemaToJsonSchema hardcodes target: 'draft-2020-12' at the conversion site, and v2 dropped Zod v3.

Test plan

  • New tests fail on unpatched v1.x
  • New tests pass with the fix
  • Full npm test (1584 / 1584)
  • npm run typecheck passes
  • npm run lint passes
  • Changeset added

McpServer's tools/list handler called toJsonSchemaCompat() without the
target option, so mapMiniTarget(undefined) fell back to 'draft-7' and
every inputSchema/outputSchema advertised draft-07, contradicting
SEP-1613 / MCP 2025-11-25 spec.
Pass target: 'draft-2020-12' at both call sites. For the Zod v4 branch,
this surfaces native 2020-12 output (including prefixItems for tuples).
For the Zod v3 branch — which uses zod-to-json-schema, a library with
no 2020-12 target — overlay $schema in toJsonSchemaCompat when 2020-12
is requested; the emitted keyword subset (type, properties, required,
additionalProperties, oneOf/anyOf/allOf, enum, const) is unchanged
between draft-07 and 2020-12.
Tests cover inputSchema and outputSchema $schema across both Zod
versions, plus Zod v4 tuples emitting prefixItems.
Fixesmodelcontextprotocol#2084
@rsalus
rsalus requested a review from a team as a code ownerMay 14, 2026 05:35
@changeset-bot

changeset-botBot commented May 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f16599

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

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

pkg-pr-newBot commented May 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@2085

commit: 4f16599

Wrap long line in .changeset/fix-sep-1613-emit-2020-12.md to satisfy
the repo's prettier config. No semantic change.
@anatoly314

Copy link
Copy Markdown

Adding a downstream data point in support of this, since it's been open a while: this is now causing real breakage for end users, not just spec non-conformance.

Two independent users of my server (@ankimcp/anki-mcp-server, tracked in ankimcp/anki-mcp-server#53) hit this within a day of each other. The client-side error is explicit about the dialect:

Error: Tool 'listDecks' 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

Reproduced on Claude Desktop 1.28929.0 (Code tab). Every tool call fails at the same validation step, so the server is effectively unusable in that client.

What makes this hard to work around downstream: on 1.30.0 there is no way to opt in. server/mcp.js calls toJsonSchemaCompat at both the inputSchema and outputSchema sites with a hardcoded options object and never passes target, so mapMiniTarget(undefined) falls through to draft-7. normalizeObjectSchema only accepts Zod schemas or raw shapes, and there's no fromJsonSchema path, so a server author can't supply a pre-built 2020-12 schema either. The capability exists in mapMiniTarget — it just isn't reachable.

That leaves downstream servers patching the emitted schema after the fact, which is why I'd rather see this land. Happy to test a build against the affected client if that's useful.

Sign up for freeto 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.

2 participants

@rsalus@anatoly314
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613) - #2085

Open
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12
Open

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613)#2085
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12

Conversation

@rsalus

@rsalusrsalus commented May 14, 2026

Copy link
Copy Markdown

Fixes#2084.

Summary

McpServer's tools/list handler calls toJsonSchemaCompat() without a target, so mapMiniTarget(undefined) returns 'draft-7' and every inputSchema/outputSchema advertises draft-07 instead of the 2020-12 dialect SEP-1613 requires.

The target: 'draft-2020-12' plumbing was added in #1135, but the mcp.ts call sites weren't updated to use it. This PR does that, plus an extra fix for the Zod v3 path that the 2-liner doesn't reach.

What changed

src/server/mcp.ts — pass target: 'draft-2020-12' at both call sites. That's the 2-line patch from the issue. For Zod v4 it's the whole fix: z4mini.toJSONSchema then emits the right $schema and uses prefixItems for tuples.

src/server/zod-json-schema-compat.ts — the v3 branch needed more. zod-to-json-schema has no draft-2020-12 target (only jsonSchema7 and jsonSchema2019-09), so passing the option through is a silent no-op. The v3 branch now overrides $schema to https://json-schema.org/draft/2020-12/schema when 2020-12 is requested.

A caveat on the v3 override: the keywords zod-to-json-schema actually emits (type, properties, required, additionalProperties, oneOf/anyOf/allOf, enum, const) have identical semantics in draft-07 and 2020-12. The one keyword that diverges is tuples. items: [...] is a positional tuple in draft-07 but means "every item matches one of these schemas" in 2020-12. A Zod v3 tuple stamped as 2020-12 is therefore subtly wrong; an inline comment in the file flags this and points at Zod v4 for native prefixItems. IMO, leaving v3 stuck on draft-07 is worse than the caveat.

test/server/mcp.test.ts: three new tests in the existing zodTestMatrix block so they run on v3 and v4: inputSchema $schema, outputSchema $schema, and a v4-only tuple → prefixItems check.

.changeset/fix-sep-1613-emit-2020-12.md: patch-level changeset.

Tests

  • npm test: 1584 / 1584 pass
  • npm run typecheck: clean
  • npm run lint: clean

On v2.0.0-alpha

main doesn't have this bug. standardSchemaToJsonSchema hardcodes target: 'draft-2020-12' at the conversion site, and v2 dropped Zod v3.

Test plan

  • New tests fail on unpatched v1.x
  • New tests pass with the fix
  • Full npm test (1584 / 1584)
  • npm run typecheck passes
  • npm run lint passes
  • Changeset added

McpServer's tools/list handler called toJsonSchemaCompat() without the
target option, so mapMiniTarget(undefined) fell back to 'draft-7' and
every inputSchema/outputSchema advertised draft-07, contradicting
SEP-1613 / MCP 2025-11-25 spec.
Pass target: 'draft-2020-12' at both call sites. For the Zod v4 branch,
this surfaces native 2020-12 output (including prefixItems for tuples).
For the Zod v3 branch — which uses zod-to-json-schema, a library with
no 2020-12 target — overlay $schema in toJsonSchemaCompat when 2020-12
is requested; the emitted keyword subset (type, properties, required,
additionalProperties, oneOf/anyOf/allOf, enum, const) is unchanged
between draft-07 and 2020-12.
Tests cover inputSchema and outputSchema $schema across both Zod
versions, plus Zod v4 tuples emitting prefixItems.
Fixesmodelcontextprotocol#2084
@rsalus
rsalus requested a review from a team as a code ownerMay 14, 2026 05:35
@changeset-bot

changeset-botBot commented May 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f16599

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

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

pkg-pr-newBot commented May 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@2085

commit: 4f16599

Wrap long line in .changeset/fix-sep-1613-emit-2020-12.md to satisfy
the repo's prettier config. No semantic change.
@anatoly314

Copy link
Copy Markdown

Adding a downstream data point in support of this, since it's been open a while: this is now causing real breakage for end users, not just spec non-conformance.

Two independent users of my server (@ankimcp/anki-mcp-server, tracked in ankimcp/anki-mcp-server#53) hit this within a day of each other. The client-side error is explicit about the dialect:

Error: Tool 'listDecks' 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

Reproduced on Claude Desktop 1.28929.0 (Code tab). Every tool call fails at the same validation step, so the server is effectively unusable in that client.

What makes this hard to work around downstream: on 1.30.0 there is no way to opt in. server/mcp.js calls toJsonSchemaCompat at both the inputSchema and outputSchema sites with a hardcoded options object and never passes target, so mapMiniTarget(undefined) falls through to draft-7. normalizeObjectSchema only accepts Zod schemas or raw shapes, and there's no fromJsonSchema path, so a server author can't supply a pre-built 2020-12 schema either. The capability exists in mapMiniTarget — it just isn't reachable.

That leaves downstream servers patching the emitted schema after the fact, which is why I'd rather see this land. Happy to test a build against the affected client if that's useful.

Sign up for freeto 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.

2 participants

@rsalus@anatoly314
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613) - #2085

Open
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12
Open

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613)#2085
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12

Conversation

@rsalus

@rsalusrsalus commented May 14, 2026

Copy link
Copy Markdown

Fixes#2084.

Summary

McpServer's tools/list handler calls toJsonSchemaCompat() without a target, so mapMiniTarget(undefined) returns 'draft-7' and every inputSchema/outputSchema advertises draft-07 instead of the 2020-12 dialect SEP-1613 requires.

The target: 'draft-2020-12' plumbing was added in #1135, but the mcp.ts call sites weren't updated to use it. This PR does that, plus an extra fix for the Zod v3 path that the 2-liner doesn't reach.

What changed

src/server/mcp.ts — pass target: 'draft-2020-12' at both call sites. That's the 2-line patch from the issue. For Zod v4 it's the whole fix: z4mini.toJSONSchema then emits the right $schema and uses prefixItems for tuples.

src/server/zod-json-schema-compat.ts — the v3 branch needed more. zod-to-json-schema has no draft-2020-12 target (only jsonSchema7 and jsonSchema2019-09), so passing the option through is a silent no-op. The v3 branch now overrides $schema to https://json-schema.org/draft/2020-12/schema when 2020-12 is requested.

A caveat on the v3 override: the keywords zod-to-json-schema actually emits (type, properties, required, additionalProperties, oneOf/anyOf/allOf, enum, const) have identical semantics in draft-07 and 2020-12. The one keyword that diverges is tuples. items: [...] is a positional tuple in draft-07 but means "every item matches one of these schemas" in 2020-12. A Zod v3 tuple stamped as 2020-12 is therefore subtly wrong; an inline comment in the file flags this and points at Zod v4 for native prefixItems. IMO, leaving v3 stuck on draft-07 is worse than the caveat.

test/server/mcp.test.ts: three new tests in the existing zodTestMatrix block so they run on v3 and v4: inputSchema $schema, outputSchema $schema, and a v4-only tuple → prefixItems check.

.changeset/fix-sep-1613-emit-2020-12.md: patch-level changeset.

Tests

  • npm test: 1584 / 1584 pass
  • npm run typecheck: clean
  • npm run lint: clean

On v2.0.0-alpha

main doesn't have this bug. standardSchemaToJsonSchema hardcodes target: 'draft-2020-12' at the conversion site, and v2 dropped Zod v3.

Test plan

  • New tests fail on unpatched v1.x
  • New tests pass with the fix
  • Full npm test (1584 / 1584)
  • npm run typecheck passes
  • npm run lint passes
  • Changeset added

McpServer's tools/list handler called toJsonSchemaCompat() without the
target option, so mapMiniTarget(undefined) fell back to 'draft-7' and
every inputSchema/outputSchema advertised draft-07, contradicting
SEP-1613 / MCP 2025-11-25 spec.
Pass target: 'draft-2020-12' at both call sites. For the Zod v4 branch,
this surfaces native 2020-12 output (including prefixItems for tuples).
For the Zod v3 branch — which uses zod-to-json-schema, a library with
no 2020-12 target — overlay $schema in toJsonSchemaCompat when 2020-12
is requested; the emitted keyword subset (type, properties, required,
additionalProperties, oneOf/anyOf/allOf, enum, const) is unchanged
between draft-07 and 2020-12.
Tests cover inputSchema and outputSchema $schema across both Zod
versions, plus Zod v4 tuples emitting prefixItems.
Fixesmodelcontextprotocol#2084
@rsalus
rsalus requested a review from a team as a code ownerMay 14, 2026 05:35
@changeset-bot

changeset-botBot commented May 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f16599

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

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

pkg-pr-newBot commented May 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@2085

commit: 4f16599

Wrap long line in .changeset/fix-sep-1613-emit-2020-12.md to satisfy
the repo's prettier config. No semantic change.
@anatoly314

Copy link
Copy Markdown

Adding a downstream data point in support of this, since it's been open a while: this is now causing real breakage for end users, not just spec non-conformance.

Two independent users of my server (@ankimcp/anki-mcp-server, tracked in ankimcp/anki-mcp-server#53) hit this within a day of each other. The client-side error is explicit about the dialect:

Error: Tool 'listDecks' 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

Reproduced on Claude Desktop 1.28929.0 (Code tab). Every tool call fails at the same validation step, so the server is effectively unusable in that client.

What makes this hard to work around downstream: on 1.30.0 there is no way to opt in. server/mcp.js calls toJsonSchemaCompat at both the inputSchema and outputSchema sites with a hardcoded options object and never passes target, so mapMiniTarget(undefined) falls through to draft-7. normalizeObjectSchema only accepts Zod schemas or raw shapes, and there's no fromJsonSchema path, so a server author can't supply a pre-built 2020-12 schema either. The capability exists in mapMiniTarget — it just isn't reachable.

That leaves downstream servers patching the emitted schema after the fact, which is why I'd rather see this land. Happy to test a build against the affected client if that's useful.

Sign up for freeto 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.

2 participants

@rsalus@anatoly314
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613) - #2085

Open
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12
Open

[v1.x] fix(server): emit JSON Schema 2020-12 in tools/list (SEP-1613)#2085
rsalus wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
rsalus:fix/sep-1613-emit-2020-12

Conversation

@rsalus

@rsalusrsalus commented May 14, 2026

Copy link
Copy Markdown

Fixes#2084.

Summary

McpServer's tools/list handler calls toJsonSchemaCompat() without a target, so mapMiniTarget(undefined) returns 'draft-7' and every inputSchema/outputSchema advertises draft-07 instead of the 2020-12 dialect SEP-1613 requires.

The target: 'draft-2020-12' plumbing was added in #1135, but the mcp.ts call sites weren't updated to use it. This PR does that, plus an extra fix for the Zod v3 path that the 2-liner doesn't reach.

What changed

src/server/mcp.ts — pass target: 'draft-2020-12' at both call sites. That's the 2-line patch from the issue. For Zod v4 it's the whole fix: z4mini.toJSONSchema then emits the right $schema and uses prefixItems for tuples.

src/server/zod-json-schema-compat.ts — the v3 branch needed more. zod-to-json-schema has no draft-2020-12 target (only jsonSchema7 and jsonSchema2019-09), so passing the option through is a silent no-op. The v3 branch now overrides $schema to https://json-schema.org/draft/2020-12/schema when 2020-12 is requested.

A caveat on the v3 override: the keywords zod-to-json-schema actually emits (type, properties, required, additionalProperties, oneOf/anyOf/allOf, enum, const) have identical semantics in draft-07 and 2020-12. The one keyword that diverges is tuples. items: [...] is a positional tuple in draft-07 but means "every item matches one of these schemas" in 2020-12. A Zod v3 tuple stamped as 2020-12 is therefore subtly wrong; an inline comment in the file flags this and points at Zod v4 for native prefixItems. IMO, leaving v3 stuck on draft-07 is worse than the caveat.

test/server/mcp.test.ts: three new tests in the existing zodTestMatrix block so they run on v3 and v4: inputSchema $schema, outputSchema $schema, and a v4-only tuple → prefixItems check.

.changeset/fix-sep-1613-emit-2020-12.md: patch-level changeset.

Tests

  • npm test: 1584 / 1584 pass
  • npm run typecheck: clean
  • npm run lint: clean

On v2.0.0-alpha

main doesn't have this bug. standardSchemaToJsonSchema hardcodes target: 'draft-2020-12' at the conversion site, and v2 dropped Zod v3.

Test plan

  • New tests fail on unpatched v1.x
  • New tests pass with the fix
  • Full npm test (1584 / 1584)
  • npm run typecheck passes
  • npm run lint passes
  • Changeset added

McpServer's tools/list handler called toJsonSchemaCompat() without the
target option, so mapMiniTarget(undefined) fell back to 'draft-7' and
every inputSchema/outputSchema advertised draft-07, contradicting
SEP-1613 / MCP 2025-11-25 spec.
Pass target: 'draft-2020-12' at both call sites. For the Zod v4 branch,
this surfaces native 2020-12 output (including prefixItems for tuples).
For the Zod v3 branch — which uses zod-to-json-schema, a library with
no 2020-12 target — overlay $schema in toJsonSchemaCompat when 2020-12
is requested; the emitted keyword subset (type, properties, required,
additionalProperties, oneOf/anyOf/allOf, enum, const) is unchanged
between draft-07 and 2020-12.
Tests cover inputSchema and outputSchema $schema across both Zod
versions, plus Zod v4 tuples emitting prefixItems.
Fixesmodelcontextprotocol#2084
@rsalus
rsalus requested a review from a team as a code ownerMay 14, 2026 05:35
@changeset-bot

changeset-botBot commented May 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f16599

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

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

pkg-pr-newBot commented May 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@2085

commit: 4f16599

Wrap long line in .changeset/fix-sep-1613-emit-2020-12.md to satisfy
the repo's prettier config. No semantic change.
@anatoly314

Copy link
Copy Markdown

Adding a downstream data point in support of this, since it's been open a while: this is now causing real breakage for end users, not just spec non-conformance.

Two independent users of my server (@ankimcp/anki-mcp-server, tracked in ankimcp/anki-mcp-server#53) hit this within a day of each other. The client-side error is explicit about the dialect:

Error: Tool 'listDecks' 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

Reproduced on Claude Desktop 1.28929.0 (Code tab). Every tool call fails at the same validation step, so the server is effectively unusable in that client.

What makes this hard to work around downstream: on 1.30.0 there is no way to opt in. server/mcp.js calls toJsonSchemaCompat at both the inputSchema and outputSchema sites with a hardcoded options object and never passes target, so mapMiniTarget(undefined) falls through to draft-7. normalizeObjectSchema only accepts Zod schemas or raw shapes, and there's no fromJsonSchema path, so a server author can't supply a pre-built 2020-12 schema either. The capability exists in mapMiniTarget — it just isn't reachable.

That leaves downstream servers patching the emitted schema after the fact, which is why I'd rather see this land. Happy to test a build against the affected client if that's useful.

Sign up for freeto 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.

2 participants

@rsalus@anatoly314