Skip to content

[Architecture]: Unify MCP protocol errors, discovery errors, and capability diagnostics into one recoverable taxonomy #1135

Description

@morluto

Priority

P1 architecture/agent-UX defect: the same correctable input mistake can surface through incompatible error channels, and the public MCP boundary often removes the exact constraint an agent needs to repair the call.

Area

MCP adapter, SDK validation, math.find, math.run, capability dispatch, diagnostics, host connectors, recovery actions.

Summary

Jacobian currently exposes several materially different failure contracts:

  1. JSON-RPC/MCP exceptions raised before or during tool dispatch;
  2. CallToolResult(is_error=true) with a JSON string in text content;
  3. a structured math.find response with kind="error";
  4. a syntactically successful math.run result whose CapabilityResult.execution failed and whose diagnostics explain why;
  5. expected no-match/weak-match discovery results that are correctly non-errors.

The distinctions are meaningful internally, but they are not represented through one finite agent-facing taxonomy. Hosts and generated connectors may preserve, flatten, or discard different parts of each channel.

Worse, the public MCP error mapper turns every ValueError into the same generic response:

{
  "error": {
    "code": "INVALID_INPUT",
    "stage": "<tool>",
    "message": "The tool input is not valid for this operation.",
    "hint": "Check the tool input schema or call math.find, then retry."
  }
}

This discards precise safe details already known by the server, such as:

  • unknown top-level argument names;
  • capability_id cannot be combined with search filters;
  • view is exact-inspection-only;
  • artifact_type requires input_kind=TYPED_ARTIFACT;
  • a specific nested JSON pointer is missing or malformed;
  • a field exceeds a published maximum;
  • input and candidate must be siblings inside payload.

The result is fail-closed but not reliably recoverable by an agent.

Audit baseline: current main as inspected on 2026-08-10.

Current evidence

The public boundary erases the original ValueError

src/jacobian/adapters/mcp/context.py classifies any ValueError as generic INVALID_INPUT and does not preserve str(tool_error), a field path, expected shape, or allowed arguments.

src/jacobian/adapters/mcp/server.py constructs a precise error for unknown arguments:

unknown tool arguments: ...

but then passes it through the same generic public mapper.

src/jacobian/adapters/mcp/tools.py raises precise ValueErrors for invalid browse/search/inspect combinations, but those details are similarly lost at the public tool boundary.

Discovery and execution use different error shapes

An unknown exact capability in math.find is converted into a typed discovery error containing nearby IDs and recovery paths.

An unknown capability in math.run is represented as a failed CapabilityResult diagnostic and then bounded separately.

A malformed math.find mode combination can become a tool error with only text content.

A no-match query is correctly a successful discovery result rather than an error.

These are all defensible local choices, but there is no common object that tells a client:

which layer rejected the request
whether mathematical execution started
whether anything was persisted
whether retry is safe
which exact field/constraint failed
which next actions are callable in this host

Model-in-the-loop failures are dominated by repairable shape errors

Domain validation behaved safely in these cases. The missing layer is a uniform machine-actionable recovery contract across the transport and capability boundaries.

Root cause

Jacobian has separate error models for separate implementation layers, but no canonical projection between them.

The public boundary currently treats safe validation detail and potentially sensitive implementation detail as the same category, so it redacts both.

At the same time, MCP/host semantics (is_error, JSON-RPC failure, structured content) are allowed to determine the agent-visible shape rather than projecting one stable Jacobian diagnostic into every supported channel.

This creates two anti-patterns:

precise internal validation
    -> generic public prose

and:

same recoverable mistake
    -> different shape depending on rejection layer

Proposed architecture

1. Define one closed AgentToolDiagnostic

Introduce a bounded typed model shared by MCP errors, math.find errors, and capability diagnostics. A possible shape:

{
  "diagnostic_version": "1",
  "code": "INVALID_ARGUMENT_COMBINATION",
  "layer": "TOOL_ENVELOPE",
  "stage": "math.find",
  "message": "capability_id cannot be combined with query",
  "path": "/query",
  "expected": "omit discovery fields when capability_id is present",
  "actual_type": "string",
  "execution_started": false,
  "state_changed": false,
  "retryability": "SAFE_AFTER_CORRECTION",
  "recovery_actions": []
}

Exact names may differ. The model should distinguish at least:

PROTOCOL
AUTHENTICATION
TOOL_ENVELOPE
CAPABILITY_RESOLUTION
CAPABILITY_INPUT
CAPABILITY_POLICY
PROVIDER
ADAPTER_EXECUTION
TIMEOUT
CANCELLATION
RESOURCE

2. Preserve safe validation facts, never rejected values

Allowlist bounded details that are safe and useful:

  • JSON pointer/path;
  • failed keyword or invariant;
  • expected type/range/required fields;
  • actual JSON type, not the value;
  • count of validation errors;
  • capability/tool name and contract version;
  • whether execution/artifact writes began;
  • one or more bounded recovery actions.

Do not return:

  • full rejected payload values;
  • stack traces;
  • filesystem paths;
  • secrets/tokens;
  • provider stderr;
  • arbitrary exception strings from unreviewed backends.

Use typed validation exceptions for public-safe detail rather than exposing generic ValueError messages indiscriminately.

3. Define the channel mapping explicitly

Recommended boundary:

  • malformed JSON-RPC, unknown top-level tool, and authentication failure remain MCP/transport errors;
  • their error data/message carries the canonical bounded diagnostic;
  • a valid math.find call with invalid mode arguments returns is_error=true plus the same diagnostic in structured and text channels;
  • no-match and weak-match discovery remain successful kind="discovery" results;
  • unknown exact capability uses one explicit capability-resolution diagnostic and bounded nearby matches;
  • math.run validation/provider/adapter outcomes remain CapabilityResult values, but their diagnostics use or embed the same canonical diagnostic model;
  • the client can always determine whether execution began and whether retry is safe.

Do not force every mathematical non-conclusion into is_error=true. The objective is semantic consistency, not flattening all outcomes into one boolean.

4. Make recovery actions typed and host-resolvable

A recovery action should identify:

canonical tool/resource
local callable binding when different
arguments or argument patch
preconditions
whether it is ranked or merely available

#1029's invariant applies: never tell an agent to call a tool absent from its current host surface.

For a payload error, prefer a minimal patch or exact-inspection action over returning the complete catalog.

5. Preserve one diagnostic across channels

Structured content, text projection, connector exception wrappers, telemetry, and logs should carry the same diagnostic ID/code and bounded fields.

The text projection can be concise, but it must not change the code, stage, path, expected constraint, execution-started status, or recovery action.

6. Add a validation-error adapter layer

Translate reviewed exception families explicitly:

  • Pydantic validation errors;
  • JSON Schema validation errors;
  • Jacobian typed domain validation errors;
  • capability-resolution/policy errors;
  • resource not found;
  • timeout/cancellation;
  • authentication/tenant limits.

Unknown exceptions remain generic OPERATION_FAILED with a log correlation digest. Do not guess a repair for unknown backend failures.

7. Version and test error semantics

Error codes and recovery action shapes are public protocol. Version them, document compatibility, and prevent one host adapter from inventing another code/stage vocabulary.

Acceptance criteria

  • Unknown top-level tool arguments identify the rejected field names without echoing values.
  • capability_id + query reports the exact incompatible fields and a safe corrected-call shape.
  • Discovery-mode view identifies /view as exact-inspection-only.
  • artifact_type/input_kind dependency failures expose the exact invariant.
  • A nested verifier wrapper error states that /payload/input and /payload/candidate are siblings, with no candidate contents returned.
  • math.find and math.run use the same diagnostic code/path/recovery vocabulary for equivalent resolution/input failures.
  • Clients can determine execution_started, state_changed, and retryability for every public failure class.
  • No-match/weak-match discovery remains a non-error and cannot be confused with transport failure.
  • Every recovery action is callable from the same in-process, stdio, HTTP, packaged Codex, and installed connector surface.
  • Structured and text channels agree on all decision-relevant fields.
  • Oversized validation sets are deterministically bounded with an explicit omitted-error count.
  • Adversarial tests prove payload values, secrets, paths, stack traces, and backend stderr are never returned.

Suggested regression matrix

Test the same failures through every supported surface:

  1. unknown top-level argument;
  2. mutually exclusive math.find fields;
  3. invalid enum/type;
  4. nested missing required field;
  5. invalid cross-field Pydantic invariant;
  6. unknown capability ID;
  7. policy-denied capability;
  8. provider unavailable;
  9. timeout before execution;
  10. client cancellation during execution;
  11. resource not found;
  12. unexpected backend exception.

Canonicalize and compare:

  • code/layer/stage/path;
  • expected/actual type;
  • execution/state flags;
  • retryability;
  • recovery actions;
  • is_error/transport mapping;
  • absence of sensitive values.

Related work

Non-goals

  • Exposing arbitrary exception text or stack traces.
  • Treating timeouts, no witness, or incomplete search as mathematical falsehood.
  • Making client-side validation authoritative.
  • Returning the full catalog on every error.
  • Prescribing mathematical strategy through recovery actions.
  • Collapsing all failures into one is_error value without preserving semantics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions