Following up on #10, which was closed as "resolved in v2.0". The error still occurs in the current release (@dokploy/mcp@0.0.3, published 2026-04-16). The OpenAPI-to-Zod rewrite doesn't address it, because the bug is in how the MCP SDK converts Zod schemas to JSON Schema — not in the schemas themselves.
Why this belongs at Dokploy, not upstream
This is a real SDK bug (modelcontextprotocol/typescript-sdk#745), but upstream isn't going to fix it any time soon:
- #1432 (minimal target fix) — closed unmerged
- #1689 (Standard Schema support) — merged 2026-03-23. Only released as
@modelcontextprotocol/core@2.0.0-alpha.1 (alpha, breaking changes). Not in @modelcontextprotocol/sdk@1.29.0 stable.
So until Dokploy either adopts the alpha or works around the bug in its own code, every user hits 400 invalid_request_error in Claude Code.
Error
API Error: 400 {"type":"invalid_request_error",
"message":"tools.31.custom.input_schema: JSON schema is invalid.
It must match JSON Schema draft 2020-12 (https://json-schema.org/draft/2020-12)"}
Environment: @dokploy/mcp@0.0.3, @modelcontextprotocol/sdk@1.29.0 (latest), Claude Code, DOKPLOY_ENABLED_TAGS=server,postgres,application,project,domain,deployment (87 tools loaded).
Root cause — full chain
-
The SDK responds to tools/list by calling its own converter in mcp.js:
toJsonSchemaCompat(obj, { strictUnions: true, pipeStrategy: 'input' })
No target is passed.
-
Inside toJsonSchemaCompat, Dokploy's Zod schemas are detected as Zod v4 (because Zod 3.25+ ships the v4 internals used for the detection) and enter the v4 branch:
if (isZ4Schema(schema)) {
return z4mini.toJSONSchema(schema, {
target: mapMiniTarget(opts?.target),
io: opts?.pipeStrategy ?? 'input'
});
}
-
opts.target is undefined (step 1), so mapMiniTarget(undefined) falls through to its default:
function mapMiniTarget(t) {
if (!t) return 'draft-7';
...
}
-
Every emitted tool schema therefore carries "$schema": "http://json-schema.org/draft-07/schema#".
-
Claude's API rejects the entire tools/list response with a 400.
The Zod schemas Dokploy generates are perfectly valid. The bug is the draft declaration the SDK attaches at serialization time.
Reproduction
Raw tools/list against unmodified @dokploy/mcp@0.0.3:
Tools loaded: 87
Top-level $schema on every tool: http://json-schema.org/draft-07/schema#
Additional nested draft-07 references: (found in several sub-schemas)
draft/2020-12 references: 0
Result in Claude Code: immediate 400 on the first tool call (tool index varies by which is first in the list).
Verified fix (local patch)
I patched toJsonSchemaCompat in node_modules/@modelcontextprotocol/sdk/.../zod-json-schema-compat.js to:
- Force
target: 'draft-2020-12' in the v4 branch.
- Recursively strip nested
$schema keys from the result.
- Set the top-level
$schema to https://json-schema.org/draft/2020-12/schema.
After patching, all 87 tools validate and Claude Code works end-to-end.
Suggested fix in @dokploy/mcp
Option A — post-process schemas before returning them (ships today).
Register a custom tools/list handler that pre-converts Zod schemas with zod-to-json-schema (targeting jsonSchema2019-09), strips nested $schema keys, and sets the top-level $schema to https://json-schema.org/draft/2020-12/schema. Bypasses the SDK's buggy converter entirely. Zero breaking changes.
Option B — adopt @modelcontextprotocol/core@2.0.0-alpha.1 (long-term).
The alpha supports Standard Schema inputs directly (PR #1689), which would let Dokploy register pre-built JSON Schemas without any conversion in the SDK path. Requires accepting alpha breaking changes (including registerTool API shape, experimental.tasks changes, removed exports) — probably not worth shipping yet.
Option A is the pragmatic path. Happy to open a PR.
Following up on #10, which was closed as "resolved in v2.0". The error still occurs in the current release (
@dokploy/mcp@0.0.3, published 2026-04-16). The OpenAPI-to-Zod rewrite doesn't address it, because the bug is in how the MCP SDK converts Zod schemas to JSON Schema — not in the schemas themselves.Why this belongs at Dokploy, not upstream
This is a real SDK bug (
modelcontextprotocol/typescript-sdk#745), but upstream isn't going to fix it any time soon:@modelcontextprotocol/core@2.0.0-alpha.1(alpha, breaking changes). Not in@modelcontextprotocol/sdk@1.29.0stable.So until Dokploy either adopts the alpha or works around the bug in its own code, every user hits
400 invalid_request_errorin Claude Code.Error
Environment:
@dokploy/mcp@0.0.3,@modelcontextprotocol/sdk@1.29.0(latest), Claude Code,DOKPLOY_ENABLED_TAGS=server,postgres,application,project,domain,deployment(87 tools loaded).Root cause — full chain
The SDK responds to
tools/listby calling its own converter inmcp.js:No
targetis passed.Inside
toJsonSchemaCompat, Dokploy's Zod schemas are detected as Zod v4 (because Zod3.25+ships the v4 internals used for the detection) and enter the v4 branch:opts.targetisundefined(step 1), somapMiniTarget(undefined)falls through to its default:Every emitted tool schema therefore carries
"$schema": "http://json-schema.org/draft-07/schema#".Claude's API rejects the entire
tools/listresponse with a 400.The Zod schemas Dokploy generates are perfectly valid. The bug is the draft declaration the SDK attaches at serialization time.
Reproduction
Raw
tools/listagainst unmodified@dokploy/mcp@0.0.3:Result in Claude Code: immediate 400 on the first tool call (tool index varies by which is first in the list).
Verified fix (local patch)
I patched
toJsonSchemaCompatinnode_modules/@modelcontextprotocol/sdk/.../zod-json-schema-compat.jsto:target: 'draft-2020-12'in the v4 branch.$schemakeys from the result.$schematohttps://json-schema.org/draft/2020-12/schema.After patching, all 87 tools validate and Claude Code works end-to-end.
Suggested fix in @dokploy/mcp
Option A — post-process schemas before returning them (ships today).
Register a custom
tools/listhandler that pre-converts Zod schemas withzod-to-json-schema(targetingjsonSchema2019-09), strips nested$schemakeys, and sets the top-level$schematohttps://json-schema.org/draft/2020-12/schema. Bypasses the SDK's buggy converter entirely. Zero breaking changes.Option B — adopt
@modelcontextprotocol/core@2.0.0-alpha.1(long-term).The alpha supports Standard Schema inputs directly (PR #1689), which would let Dokploy register pre-built JSON Schemas without any conversion in the SDK path. Requires accepting alpha breaking changes (including
registerToolAPI shape,experimental.taskschanges, removed exports) — probably not worth shipping yet.Option A is the pragmatic path. Happy to open a PR.