Skip to content

feat(mcpx): validate CallTool arguments against inputSchema (-32602), warn on output mismatch (#1483) - #1596

Merged
dezren39 merged 8 commits into
mainfrom
queue/1616-1483-calltool-validation
Oct 28, 2026
Merged

dezren39 merged 8 commits into
mainfrom
queue/1616-1483-calltool-validation

Conversation

@dezren39

@dezren39 dezren39 commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Fixes #1483 (Phase 2 of #1481; queue task 1616). Gate: Phase 1 #1482 merged (PR #1570, main 1da6d84) — verified fresh this shift.

What

  • List time (internal/mcpclient/schemacheck.go, wired into ListTools on every connection): each tool's inputSchema/outputSchema is compiled with internal/schemaval, cached per tool keyed by schema hash. A schema that does not compile (unsupported dialect, unresolvable/external $ref, invalid or oversized) excludes the tool with a Warning carrying the cause — the x-mcp-header precedent, surfaced by the pool as server.warning.
  • Inputs, enforced: CallTool validates the marshalled arguments against the compiled input validator before the transport is touched. Failure returns a typed InvalidParamsError (-32602 via RPCCode()), naming the tool and the path-qualified reason; the upstream receives zero calls.
  • Gateway pass-through: the tools/call invoke path in internal/mcpserver relays an RPCCode() == -32602 error as a protocol error instead of converting it to an isError result (interface check — no import cycle).
  • Outputs, warn-only: a result's structuredContent is validated against the output validator; a violation records a Warning and the result is delivered unchanged.
  • Validation applies once a connection has listed tools (the pool always lists before calling, so the choke point is covered); a never-listed connection behaves exactly as before — no surprise tools/list is issued on a call's behalf. Tools with no schema behave exactly as today.

Tests

New defining tests in schemacheck_test.go: wrong-type / missing-required / additional-property arguments all fail -32602 with upstream call count 0; valid arguments reach the upstream; unsupported-dialect and unresolvable-$ref tools are absent from ListTools with warnings naming the cause; output violation is warn-only with the result unchanged.

Local (laptop): go test ./internal/mcpclient/... ./internal/mcpserver/... ./internal/pool/... — all pass.

Benchmark (triage budget: ≤100µs/op median, ≤1ms worst)

BenchmarkCallToolValidateArgs (nested schema, ~5 levels / ~20 properties): 41.6 µs/op on the laptop (Ryzen 7840U) for a full scripted CallTool round trip including validation — inside budget. Also recorded on #1483.

Conformance

Matrix rows json-schema-support-2020-12, json-schema-validate-per-dialect-and-error-on-unsupported, json-schema-document-dialects, json-schema-reject-unresolved-ref updated from gap #254 to implemented (both occurrences each).


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…output mismatch (#1483)

Phase 2 of #1481, consuming the Phase-1 schemaval core (#1482):

- ListTools compiles each tool input/output schema via internal/schemaval (cached per tool, keyed by schema hash); a tool whose schema does not compile (unsupported dialect, unresolvable $ref, invalid/oversized) is excluded with a Warning, the x-mcp-header precedent. Applies on every connection, not just modern HTTP.
- CallTool validates marshalled arguments against the compiled input validator before the transport is touched; failure is a typed InvalidParamsError carrying -32602 (RPCCode), naming the tool and the path-qualified reason; the upstream receives zero calls.
- The gateway invoke path in internal/mcpserver relays an RPCCode -32602 error as a protocol error instead of an isError tool result (interface check, no import cycle).
- structuredContent is validated against the output validator warn-only; the result is delivered unchanged.
- Validation applies once a connection has listed (the pool always lists before calling); a never-listed connection behaves exactly as before.
- Conformance matrix rows for the four json-schema ids updated from gap #254 to implemented.

Tests: defining tests in internal/mcpclient/schemacheck_test.go (wrong type / missing required / additional property rejected with upstream call count 0; valid args pass; unsupported dialect and unresolvable $ref excluded with warnings; output violation warn-only). Full mcpclient + mcpserver + pool suites pass on the laptop.
Benchmark (laptop, Ryzen 7840U): BenchmarkCallToolValidateArgs 41.6us/op for a full scripted CallTool round trip including validation of a ~5-level/~20-property nested schema — inside the triage budget of <=100us/op median.
@dezren39
dezren39 requested a review from a team as a code owner October 28, 2026 05:11
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 26 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 76dfdb31-0149-4025-ad8a-a019cca5db20
Commits

Reviewing files that changed from the base of the PR and between ea7272c and 6928005.

Files selected for processing: 12
  • pkgs/mcpx/docs/spec/conformance-matrix.md
  • pkgs/mcpx/internal/conformance/coverage_messages.go
  • pkgs/mcpx/internal/conformance/officialclient/main.go
  • pkgs/mcpx/internal/daemon/callerr.go
  • pkgs/mcpx/internal/e2e/callerr_test.go
  • pkgs/mcpx/internal/e2e/ceiling_test.go
  • pkgs/mcpx/internal/mcpclient/client.go
  • pkgs/mcpx/internal/mcpclient/schemacheck.go
  • pkgs/mcpx/internal/mcpclient/schemacheck_test.go
  • pkgs/mcpx/internal/mcpserver/server.go
  • pkgs/mcpx/internal/schemaval/schemaval.go
  • pkgs/mcpx/internal/schemaval/schemaval_test.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-28T14:29:27.472406Z 6928005 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f7f87f5a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +129 to +132
ts, ok := c.validatorsFor(name)
if !ok || ts.input == nil {
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Initialize validators on the connections that execute calls

For session- and call-scoped servers, this silently skips validation on production calls: Pool.RefreshSchemas lists tools on its catalogue connection and then releases that instance (internal/pool/pool.go:827-880), whereas Pool.Call acquires another connection and immediately calls CallTool without listing tools. The new connection therefore has no toolValidators, so invalid arguments reach the upstream and output validation is also skipped. The pool's cached schemas need to initialize the executing connection's validators, or that connection must load them before validation; listing on the separate catalogue connection does not cover this path.

Useful? React with 👍 / 👎.

Comment on lines +1011 to +1013
var coded interface{ RPCCode() int }
if errors.As(err, &coded) && coded.RPCCode() == codeInvalidParams {
return fail(codeInvalidParams, err.Error())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve validation error codes across the daemon boundary

When argument validation fails through the actual MCP gateway, this assertion never sees InvalidParamsError: mcpBackend.Call communicates with the daemon over HTTP (internal/cli/serve.go:122-156), and daemon.callErrorBody preserves RPC codes only for errors with reflected Code and Message fields (internal/daemon/callerr.go:161-203). The new error has neither field, so it becomes an HTTPError without RPCCode() or an upstream error envelope, and the gateway returns an isError tool result instead of the intended -32602 protocol error. Serialize and reconstruct the validation code across that boundary, including the asynchronous ask path, rather than relying only on an in-process interface assertion.

Useful? React with 👍 / 👎.

…cs (#1483)

Phase 2 validation refused schema-moved calls in mcpclient before the
transport, so the daemon explainCall path (which only recognised
upstream rpcErrors) attached no diagnostics: five e2e tests failed in
CI (callerr x4, ceiling x1) and refused calls lost the schema-change
explanation every surface is built around.

- daemon: clientFault() recognises an error carrying RPCCode() -32602
  (interface assertion, the gateway precedent) and explainCall runs
  diagnose.CallError for it, so /v1/call, tasks, scripts and the CLI
  carry the same diagnostics as an upstream refusal.
- mcpclient: InvalidParamsError headline uses the canonical
  "Invalid params" spelling, matching upstream -32602 messages.
- e2e: two assertions that hardcoded the upstream headline
  ("mcp error -32602...") now expect the validator headline; the
  diagnostics assertions are unchanged.
…binator failures (#1483)

schemaval.Validate read DetailedOutput as a flat list: interior units (anyOf/oneOf/allOf/not and other grouping keywords) carry a nil Error, so an argument failure through a combinator nil-pointer panicked the daemon — the mcpx conformance server-all leg crashed mid-suite on PR #1596 (196/51 vs main 272/0) and every later scenario failed to connect. Walk the output tree to its leaves instead. Regression tests: combinator cases in schemaval and a CallTool anyOf case in mcpclient (returns -32602, upstream untouched).
@dezren39

Copy link
Copy Markdown
Member Author

Do not merge b4a06b8 — conformance found a real crash; fixed in 4ff47f7.

The mcpx conformance server-all failure on b4a06b8 was not informational noise: main's server-all is 272 passed / 0 failed, this PR's was 196/51. The run artifact's daemon log shows why — a nil-pointer panic in schemaval.(*Schema).Validate (schemaval.go:259) from mcpclient.validateToolArgs during http-header-validation; the daemon died (ECONNREFUSED / fetch failed afterwards) and every later scenario (tasks-, input-required-) failed to connect.

Root cause: Validate treated DetailedOutput() as a flat list, but it is a tree — interior units (every combinator failure: anyOf/oneOf/allOf/not, and other grouping keywords) carry sub-errors and a nil Error, so formatting the problems dereferenced nil. Simple leaf failures (all of Phase 1's tests) never hit it; Phase 2's call-path enforcement is the first production caller. Reproduced exactly with an anyOf schema (identical stack).

Fix (4ff47f7): walk the output tree to its leaves when flattening problems. Regression tests: combinator cases in schemaval (TestFlattenedCombinatorErrors) and a CallTool anyOf case in mcpclient (returns -32602 InvalidParamsError naming $.mode, upstream call count 0). Laptop: internal/schemaval, internal/mcpclient, internal/mcpserver, internal/pool, internal/daemon all pass; gofmt clean.

Note: PR #1606 (Phase 3 elicitation, #1484) validates through the same schemaval.Validate, so this fix covers that path too once both land.

Awaiting CI on 4ff47f7 — merge only when green incl. conformance.

… a violation (#1483)

CallTool validation now drops null-valued non-required top-level properties from the copy it validates (SEP-2243: a null parameter is not provided); the wire arguments are unchanged and a null for a required property still fails -32602. This restores the client-2026-07-28 conformance checks http-custom-headers/omit-null, whose suite call sends verbose:null against a boolean schema.

officialclient: the no-arguments fallback now synthesizes minimal schema-valid arguments from each advertised tool's inputSchema instead of {}, restoring http-invalid-tool-headers/valid_tool (required region) under CallTool enforcement.
@dezren39

Copy link
Copy Markdown
Member Author

Pushed 550585c2 fixing the one red check, client-2026-07-28 conformance (was 65/70 vs expected 67/68, with exactly 2 non-OAuth failures — both root-caused from the run's artifacts):

  • http-custom-headers · sep-2243-client-omit-null — the suite calls test_custom_headers_null with verbose: null against a boolean property schema; CallTool validation refused it (-32602) before the server ever saw the call. Fix: validation now treats an explicit null for a non-required top-level property as an omission (SEP-2243's own semantics: a null parameter is not provided). Only the validated copy changes — wire arguments are byte-identical — and a null for a required property still fails. New test TestCallToolNullForOptionalPropertyIsAnOmission covers both halves.
  • http-invalid-tool-headers · ClientKeepsValidTool — the suite gives the driver no arguments for valid_tool, and the officialclient fallback called it with {} against a required region. Fix: the fallback now synthesizes minimal schema-valid arguments from each advertised tool's inputSchema (one minimal value per required property) — what a schema-aware host does; suite-supplied and tabled calls are untouched.

Laptop verification: go test ./internal/mcpclient/ ./internal/mcpserver/ ./internal/conformance/ all pass, officialclient builds, vet clean. CI on this head is the deciding run.

— queue worker (repo-worker-code), lane A, task 1616 resume

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 550585c2cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +177 to +179
for key, value := range obj {
if string(bytes.TrimSpace(value)) == "null" && !ts.required[key] {
delete(obj, key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve nullable values required by composed schemas

When an input schema declares a required nullable property through allOf or $ref, requiredProps only reads the root-level required, so this removes a valid explicit null before validation. For example, {"type":"object","allOf":[{"required":["x"],"properties":{"x":{"type":["null","string"]}}}]} accepts {"x":null}, but the client instead validates {} and rejects the call with -32602 without contacting upstream. Preserve schema-valid nulls rather than deciding optionality solely from the root-level keyword.

Useful? React with 👍 / 👎.

…ool-validation

# Conflicts:
#	pkgs/mcpx/docs/spec/conformance-matrix.md
#	pkgs/mcpx/internal/mcpclient/client.go
Sign up for free to 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.

mcpx: Phase 2 of #1481 — validate tool arguments at CallTool (enforce, -32602) + structuredContent against outputSchema (warn-only)

1 participant