Visitar URL original
[Spec Gap] Tool Execution Errors lack a schema-governed signal for LLM forwarding — structuredContent should be defined for error path · Issue #3003 · modelcontextprotocol/modelcontextprotocol · GitHub
Skip to content

[Spec Gap] Tool Execution Errors lack a schema-governed signal for LLM forwarding — structuredContent should be defined for error path #3003

Description

@antara1414

What's broken?

The spec is ambiguous or self-contradictory

Where in the spec or docs?

https://modelcontextprotocol.io/specification/2025-11-25/server/tools

What should happen?

The use of unstructured content in the example for tool execution error without a textural reference of whether its proposed to be always used or it is up to the implementor choice is unclear.

The gap this creates:

Since content[].text is unstructured and unvalidatable, anything can reach the LLM context window via the error path — including stack traces, internal system identifiers, and PII sourced from downstream systems. There is no structural mechanism to prevent this; it relies entirely on developer discipline at the MCP server layer.

structuredContent with outputSchema already provides exactly the right mechanism — schema validation before client consumption — but is undefined for errors.

Specific questions for the spec

1.	Is content[] intentionally prescribed as the LLM-facing error signal, or is this an example that became a de facto standard?
2.	Is structuredContent intentionally excluded from the error path, or is this a gap?
3.	Given that the spec uses SHOULD (not MUST) for content[] when structuredContent is present, would the spec support structuredContent as the primary error signal when outputSchema is declared?
4.	Would the spec consider defining a structured error schema within structuredContent for isError: true responses — analogous to how Protocol Errors have a defined schema in JSON-RPC 2.0?

Proposed direction

Allow structuredContent to carry a defined error object on the error path when outputSchema is declared, enabling MCP clients to schema-validate before LLM forwarding. This would: close the PII leakage vector, provide a deterministic LLM self-correction signal, and align the error path with the structured governance model already established for the success path.

What actually happens?

Observation

The MCP spec 2025-11-25 prescribes content[] as the error signal for Tool Execution Errors (isError: true), shown by example in the tools specification. However the spec explicitly describes content[] as carrying “unstructured” content with no schema validation mechanism.

Simultaneously, the spec defines structuredContent with outputSchema validation for the success path — a schema-governed, client-validatable channel — but does not extend this to the error path.

Anything else?

No response

Activity

  1. kuangmi-bit commented on Jul 3, 2026

    @kuangmi-bit
  2. antara1414 commented on Jul 3, 2026

    @antara1414
    Author
  3. TimeToBuildBob commented on Jul 10, 2026

    @TimeToBuildBob
  4. antara1414 commented on Jul 23, 2026

    @antara1414
    Author
  5. TimeToBuildBob commented on Jul 23, 2026

    @TimeToBuildBob
  6. antara1414 commented on Jul 23, 2026

    @antara1414
    Author
  7. TimeToBuildBob commented on Jul 23, 2026

    @TimeToBuildBob
  8. TimeToBuildBob commented on Jul 23, 2026

    @TimeToBuildBob
  9. ObjectIsAdvantag commented on Jul 27, 2026

    @ObjectIsAdvantag

    hi @antara1414,

    I am not a maintainer, but here are my 2 cents as I am also experiencing the gap with tools execution:

    • how to document errors schema and return structured error responses for tools execution

    Before I dig into a proposal, I'd like to clarify that I think we are interpreting one part of the MCP specification differently.
    My reading is that structuredContent and outputSchema currently describe only the successful result of a tool call,
    Tool-execution errors are described separately using isError: true, and unstructured content are for LLMs to consider and adjust the tool invocation as needed so that the next tool invocation can be successful.

    Because of these, I don't think it would be convenient to extend the existing structuredContent and use it for errors.

    The way out could be to support error schemas natively in an MCP model such as:

    isError
        discriminates between the two paths (same as today, no change)
    
    structuredContent + outputSchema
        represents successful execution (same as today, no change)
     
    structuredError + errorSchema
        represents failed execution (new)
    
    content
        remains the human/LLM-readable and backward-compatible representation (same as today, no change)
    

    This approach would require an MCP specification change, so an SEP which I am happy to contribute to with code samples.

    As a temporary, opt-in convention, implementations could advertise an error schema or error catalogue in the tool’s namespaced _meta field and return the error instance in CallToolResult._meta, while continuing to provide content and isError: true.

    For example:

    Tool._meta["com.example/tool-errors"]
        declares the error schema or known errors
    
    CallToolResult._meta["com.example/tool-error"]
        carries the structured error instance
    

    This would allow experimentation without changing MCP, but generic clients would not understand the convention.
    That's the approach that I'm using in my own projects for the short term.

  10. dgenio commented on Aug 24, 2026

    @dgenio

    One additional modeling distinction may be useful here:

    what failed and what can make the operation succeed are different axes.

    I've run into this downstream while building policy enforcement for agent tool calls. Stable reason/error codes answer the first question well:

    POLICY_DENIED
    RATE_LIMITED
    CONSTRAINT_VIOLATION
    INVALID_ARGUMENT
    

    But agents and hosts then have to infer the second question from free text:

    Should I correct the request?
    Wait?
    Ask the user for approval?
    Reduce the requested scope?
    Choose another capability?
    Give up?
    

    That inference is exactly where retry loops and unsafe recovery behavior start appearing.

    For example, these two failures may have the same broad error class but very different valid recovery paths:

    {
      "code": "POLICY_DENIED",
      "message": "Additional approval is required."
    }

    and:

    {
      "code": "POLICY_DENIED",
      "message": "This operation is not permitted."
    }

    For the first, escalation/user action may make the same operation valid. For the second, repeatedly asking for approval should not.

    So, if MCP introduces a structured tool-error representation, I think it is worth considering a small optional remediation/recovery semantic separate from error_code and implementation-specific error_details.

    Conceptually:

    {
      "isError": true,
      "structuredContent": {
        "error_code": "RATE_LIMITED",
        "error_message": "The operation is temporarily unavailable.",
        "remediation": {
          "kind": "wait",
          "retry_after_ms": 30000
        }
      }
    }

    or:

    {
      "isError": true,
      "structuredContent": {
        "error_code": "POLICY_DENIED",
        "error_message": "The requested operation requires user authorization.",
        "remediation": {
          "kind": "request_user_action"
        }
      }
    }

    The useful property is that the remediation vocabulary can be much smaller and more stable than the error-code space.

    In a downstream implementation I initially used fairly policy-specific kinds such as:

    provide_justification
    acquire_role
    wait
    reduce_scope
    escalate
    

    At MCP level I would probably make this more policy-agnostic, for example something closer to:

    correct_request
    wait
    reduce_scope
    request_user_action
    choose_alternative
    none
    

    I'm not suggesting those exact names yet; the distinction matters more than the taxonomy.

    This also seems to fit the two-consumer distinction already raised in this thread:

    • the host can deterministically act on wait + retry_after_ms without spending another model turn;
    • the model can be told that user action is required without receiving internal IAM/policy details;
    • orchestration code can distinguish a potentially recoverable denial from a terminal one without parsing prose.

    There is an important security constraint here: remediation should describe a permitted recovery class, not expose the policy model.

    For example:

    request_user_action
    

    may be safe where:

    acquire role finance-admin from group X
    

    would create an authorization oracle.

    Likewise, a remediation hint must not mean "the client is authorized to perform this recovery automatically." It only describes what class of state change could make a subsequent request eligible.

    I would therefore keep a few invariants:

    • remediation is optional;
    • absence does not imply retryability;
    • the same deterministic denial should produce the same remediation class;
    • hints MUST NOT disclose more policy information than the caller is allowed to know;
    • clients MUST NOT interpret a remediation hint as authorization;
    • wait can carry a concrete retry-after value, but clients are not required to retry;
    • terminal/non-recoverable errors can explicitly have no remediation.

    This is complementary to error_code: an error code classifies the failure; remediation describes the valid next-action space.

    It may turn out that tool-specific error schemas are sufficient and this should remain an application convention. But if independent MCP clients are expected to make deterministic recovery decisions across servers, a very small interoperable remediation vocabulary could avoid every host reverse-engineering recovery semantics from error prose.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions