Skip to content

design(mcpx): JSON Schema validation of tool input and output — one validator at the client choke point #1481

Closed
3 of 3 issues completed

Description

@dezren39

Design doc: files/designs/code-mcpx-json-schema-validation.md in the keep-the-four-repos-healthy goal workspace. Register: central potential-projects list, same workspace (files/potential-projects.md).

Validator decision (recorded here per nix#254's acceptance, before implementation): adopt github.com/santhosh-tekuri/jsonschema/v6 behind a thin internal/schemaval wrapper — a new dependency with cause (full JSON Schema 2020-12 is a MUST row in mcpx's own conformance matrix, and the library passes the JSON-Schema-Test-Suite and covers drafts 4/6/7/2019-09/2020-12). Do not extend internal/mcpspec: it is a deliberately test-only subset validator for the vendored protocol schemas (TestNotImportedByBinary keeps it out of the binary), and growing it into a general 2020-12 engine is writing a JSON Schema implementation as a side quest. No user/review: the direction is already set by the roadmap row at proposed and nix#254's acceptance criteria.

Summary

mcpx forwards tool schemas it never enforces. The conformance matrix (pkgs/mcpx/docs/spec/conformance-matrix.md, 2025-11-25 lines 1380–1382; 2026-07-28 lines 2068–2075) carries the gap under nix#254, and internal/conformance/coverage_messages.go registers it:

  • json-schema-support-2020-12 (MUST) — "mcpx never validates arguments against a tool's inputSchema; schemas pass through"
  • json-schema-validate-per-dialect-and-error-on-unsupported (MUST) — "no validator in the binary, so an unsupported $schema is forwarded, not refused" / "is accepted"
  • json-schema-document-dialects (SHOULD) — "no document states which dialects mcpx supports"
  • json-schema-reject-unresolved-ref (SHOULD) — "an unresolvable $ref is rendered permissively (codegen) rather than rejected"

Roadmap: planning/roadmaps/mcpx.md → Next → "JSON Schema validation of tool input and output" (status proposed, nix#254). Origin gap: dezren39/darwin-nix#254 (OPEN) — mcpx's canonical home is this repo, so the design and implementation live here and cite it; no fresh nix issue for the same change.

Proposal — one validator, compiled once per tool at list time, enforced at the client choke point:

  • internal/schemaval (name indicative) wraps the library as the only import site. Dialect policy: no $schema → 2020-12 (MCP default); declared dialect must be one of 2020-12 / 2019-09 / draft-07 / draft-06 / draft-04, else a distinct UnsupportedDialect error. No network, ever: external $ref is an UnresolvedRef compile error, never a fetch (preserves the tested json-schema-no-network-ref MUST NOT). Formats are annotations, not assertions, in v1. Compilation is size-bounded. Errors carry instance paths (e.g. $.count: expected integer, got string).
  • List time: Client.ListTools compiles each tool's InputSchema/OutputSchema beside the existing checkToolHeaders pass (internal/mcpclient/modern.go:495) and caches compiled validators per tool keyed by schema hash. A tool whose schema cannot compile is excluded with a Warning — the established x-mcp-header precedent ("a definition mcpx cannot honour is not forwarded"); the pool already wires OnWarning to server.warning lifecycle events.
  • Call time, inputs enforced: Client.CallTool (internal/mcpclient/client.go:1164) validates arguments before send. Failure = -32602 Invalid Params (tool + path-qualified reason), surfaced by gateway paths as the protocol error it is, not an isError tool result; the upstream sees zero calls. The client is the choke point: Pool.Call (internal/pool/pool.go:1028), daemon Registry.Call, scripts, mcpx_call and pass-through all funnel through it.
  • Call time, outputs warn-only: structuredContent validated against outputSchema when both present; failure = Warning + lifecycle event, result delivered unchanged. The caller did nothing wrong; flipping outputs to enforced is an open question answered with this warning data.
  • Elicitation answers enforced: in Client.answer (internal/mcpclient/modern.go:253), after applyDefaults (which already parses requestedSchema), an accept answer's content is validated against requestedSchema (form mode); failure = -32602 to the requesting server, answer not relayed. The modern input-required flow inherits this via answer(). Only the mechanism slice of the elicitation rows — fix(id): coalesce LogSync ingest batches (#241) #255's storage/redaction/UI items are out of scope.

Grounding: mcpclient.Tool already carries both schemas raw (client.go:163-164); internal/mcpspec/validate.go header documents the subset validator's test-only scope and keyword list; codegen (internal/codegen/ts.go) stays a permissive type renderer — rejection belongs at list time, after which an excluded tool never reaches codegen through the catalog. Alternatives (extend mcpspec; xeipuuv/qri-io libraries — neither reaches 2020-12; validate in daemon/gateway instead of client; enforce outputs day one; do nothing) and risks (sloppy real-world schemas — mitigated structurally, no speculative per-server opt-out in v1; performance — compile cached, benchmark in Phase 2 acceptance; >2^53 number precision — library-required decoding + a Phase 1 test; dependency weight — one pure-Go module behind one package) are argued in the design doc.

Phased breakdown

  • Phase 1 — validator core and dialect policy. internal/schemaval + dependency + dialect document in pkgs/mcpx/docs/; no call-path changes, nothing enforces yet. Defining tests: 2020-12 keyword corpus, unsupported dialect refused, unresolvable $ref refused, remote $ref triggers zero requests against a counting test server, >2^53 integer validates as integer. Filed as a sub-issue of this issue.
  • Phase 2 — tool input enforcement + output warning at the client choke point. List-time compile/exclude, CallTool argument validation (-32602, upstream call count 0 on failure), structuredContent warn-only validation, argument-validation benchmark. Requires Phase 1 merged. Filed as a sub-issue of this issue.
  • Phase 3 — elicitation form-answer validation. requestedSchema check in Client.answer after defaults; wrong-typed answer → upstream receives -32602, not the answer. Requires Phase 1 merged; independent of Phase 2. Filed as a sub-issue of this issue.
  • Open questions, NOT filed. Output enforcement flip (on Phase 2 warning data); format assertion for a named subset; per-server validation mode knob (only on fleet evidence); prompts/sampling reuse of schemaval (fix(id): show world chat only to participants who may chat or read history #256 is separate); codegen hard-fail for schemas from non-catalog sources.

agent-note:v1 — filed by the repo design architect (dedicated code job, pass 3, 2026-10-27). Design doc + register entry as linked above. Phases 1–3 are filed as sub-issues with queue/queued + task/triage packets enqueued to repo-health-code-pilot. Cross-repo note: dezren39/darwin-nix#254 is the origin gap record; this issue is the mcpx-canonical design that satisfies its acceptance — do not file a duplicate in nix.

Acceptance criteria

Activity

  1. added
    priority/normalOrdinary priority (the default)
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    epicMeta-issue grouping multiple items (org Project Type=Epic)
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    on Oct 27, 2026
  2. developing-today commented on Oct 27, 2026

    @developing-today
    Contributor
  3. added
    kind/designA design proposal (design-architect): multi-step, phased, decision-bearing
    on Oct 27, 2026
  4. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    Program tending — Phase 1 landed (2026-10-28)

    Recording on the parent: sub-issue #1482 (Phase 1 — schemaval core: JSON Schema 2020-12 validator + dialect policy, no call-path changes) closed 2026-10-28T04:51Z. This epic has 3 sub-issues; the other two remain open, so the design is in progress, not complete — the remaining phases wire the validator into the call path per this issue's breakdown.

    — program tender (issue #1352 run, 2026-10-28)

  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Transition: picked up
    Worker: repo-worker-code (openchamber-scheduled-lane), task 2592
    Next: Revalidate parent issue and existing phased work before routing.

  6. 5 remaining items

  7. added
    queue/blockedQueue or user escalation state
    status/blockedQueue or user escalation state
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    status/blockedQueue or user escalation state
    queue/blockedQueue or user escalation state
    on Nov 1, 2026
  8. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1

    Transition: picked up
    Worker: repo-worker-code (lane code-e, direct)
    Task: task 2592
    Scope: fresh re-triage of design epic #1481 against current main — check phase sub-issues, merged PRs, and origin dezren39/darwin-nix#254, then disposition or enqueue a correct follow-up.

  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1

    Transition: triage complete — closing as completed (task 2592)
    Worker: repo-worker-code (lane code-e, direct)

    Triage verdict — task 2592, fresh check 2026-11-03T09:41-09:47Z

    All acceptance criteria of this design epic are met:

    No follow-up packet is needed: no work remains. Closing #1481 as completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)effort/lLarge: multi-day or open-ended; expect staged check-insepicMeta-issue grouping multiple items (org Project Type=Epic)kind/designA design proposal (design-architect): multi-step, phased, decision-bearingpriority/normalOrdinary priority (the default)source/design-architectFiled by the repo design architect job

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions