Skip to content

fix(mcp): move tool error problem to _meta - #91

Merged
htcom-code merged 2 commits into
mainfrom
fix/mcp-output-schema-conformance
Oct 30, 2026
Merged

htcom-code merged 2 commits into
mainfrom
fix/mcp-output-schema-conformance

Conversation

@htcom-code

@htcom-code htcom-code commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Fixes #89, Fixes #90

What

#90 — coded tool errors no longer put their problem detail in structuredContent.
A stable-code error (McpToolResult.error(ErrorCode, …)) now carries its RFC 9457 problem in the result
_meta under org.htcom.protean/problem (new constant McpToolResult.PROBLEM_META_KEY), merged with any
_meta the tool attached. structuredContent carries only what the tool itself returns.

#89 — protean.list_runtimes declares runtimes[].scope as ["string","null"].

Why

  • The MCP spec (2025-11-25 and 2026-07-28) says that when an outputSchema is declared, "Servers MUST provide
    structured results that conform to this schema"
    — with no exemption for isError results. The problem
    object never matches a tool's success schema, so every built-in tool that declares an outputSchema was
    emitting non-conforming error results (e.g. scope_list with auto-provision off, patch_module on an
    unknown module).
  • This is not theoretical for a library that cannot choose its clients: @modelcontextprotocol/sdk 1.32.2
    (TypeScript SDK 1.x) validates structuredContent whenever it is present, regardless of isError, and
    replaces the actionable error with Structured content does not match the tool's output schema.
    (@modelcontextprotocol/client 2.x and the Python SDK skip validation on errors.)
  • list_runtimes returns scope: null whenever auto-provision is off (the default), so every default-install
    result violated its schema, and with protean.mcp.strict-schema=true the call failed outright with
    OUTPUT_SCHEMA_VIOLATION.

Compatibility

Wire change for MCP clients: read an error's stable code from _meta["org.htcom.protean/problem"].code
instead of structuredContent.code. The [CODE]-prefixed text content, success results, and JSON-RPC
protocol errors (error.data) are unchanged. The Java API only gains a constant. Recorded under
Changed in CHANGELOG.md / CHANGELOG.ko.md.

Tests

  • New McpOutputSchemaConformanceTest.error_results_never_carry_a_nonconforming_structured_content sweeps
    every tool that declares an outputSchema with error-provoking arguments and asserts no non-conforming
    structuredContent plus a coded problem in _meta. A floor (named tools + ≥10 exercised) keeps it from
    passing vacuously — missing-argument calls are JSON-RPC protocol errors, not tool results, so they
    exercise nothing.
  • New: strict-mode list_runtimes with a null scope; gated scope_list reports STATE_CONFLICT in _meta;
    list_runtimes added to the real-output conformance check; tool _meta + problem merge (tool's object
    not mutated).
  • Nine existing error assertions moved from structuredContent to _meta.
  • Transport level: McpHttpControllerTest.coded_tool_error_over_http_carries_its_problem_in_meta POSTs
    scope_list to /platform/mcp and asserts the wire shape (_meta problem, no structuredContent) — the
    dispatcher-level tests above never touched the HTTP serialization. Fails against main's dispatcher.
  • Red check: reverting only McpDispatcher and ListRuntimesTool makes all six new/changed tests fail; the
    sweep independently catches patch_module, not only scope_list.
  • ./gradlew check passes locally (603 tests).
  • Live, :examples:quickstart:runMcp (in-process, fresh store): gate-fail deploy → _meta problem
    GATE_FAILED/gate=tests, no structuredContent; config.set with an unknown key → INVALID_ARGUMENT with
    outcomes; scope_list → STATE_CONFLICT; list_runtimes → scope: null, no error.

Not in this PR

  • The Go (protego) and Rust (protrus) ports replicate the old shape; handed off to their own repos to follow
    this contract after merge.
  • The java_mcp scenario runbooks name the old location in two expected values; handed off, to apply after
    merge.

Docs

docs/guide/08-mcp-integration.md (+ko): new Tool errors section with the result shape; the outputSchema
validation section notes that a custom tool attaching structuredContent to an error must keep it conformant.

A coded tool error wrote its RFC 9457 problem object into
structuredContent. The spec reserves that field for the payload the
tool's outputSchema describes and requires it to conform, and some
clients (the TypeScript SDK 1.x) validate it on error results too, so
they replaced the actionable error with a schema-violation error. Every
built-in tool that declares an outputSchema was affected. The problem
now travels in the result _meta under org.htcom.protean/problem
(McpToolResult.PROBLEM_META_KEY), next to any _meta the tool attached.

list_runtimes declared runtimes[].scope as a string although it is null
whenever auto-provision is off (the default), so every default-install
result violated its outputSchema and strict-schema mode rejected the
call outright. scope is now nullable.

Tests sweep every schema-declaring tool's error path and run
list_runtimes under strict validation; guides and changelog (en+ko)
document the new error location.

Fixes #89, Fixes #90
Co-Authored-By: htjulia <[email protected]>
The error-shape tests drove McpDispatcher directly, so nothing in CI
checked what POST /platform/mcp actually puts on the wire. A coded
scope_list error must arrive with its problem in _meta and no
structuredContent.

Related: #90
Co-Authored-By: htjulia <[email protected]>
@htcom-code
htcom-code merged commit 7e6abed into main Oct 30, 2026
7 checks passed
@htcom-code
htcom-code deleted the fix/mcp-output-schema-conformance branch October 30, 2026 04:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant