Skip to content

v2.0 does not fix draft 2020-12 schema error (reopen #10) #32

@kk-michael

Description

@kk-michael

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

  1. The SDK responds to tools/list by calling its own converter in mcp.js:

    toJsonSchemaCompat(obj, { strictUnions: true, pipeStrategy: 'input' })

    No target is passed.

  2. 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'
        });
    }
  3. opts.target is undefined (step 1), so mapMiniTarget(undefined) falls through to its default:

    function mapMiniTarget(t) {
        if (!t) return 'draft-7';
        ...
    }
  4. Every emitted tool schema therefore carries "$schema": "http://json-schema.org/draft-07/schema#".

  5. 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:

  1. Force target: 'draft-2020-12' in the v4 branch.
  2. Recursively strip nested $schema keys from the result.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions