Repository navigation
[Spec Gap] Tool Execution Errors lack a schema-governed signal for LLM forwarding — structuredContent should be defined for error path #3003
Description
Activity
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 instanceThis 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.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_ARGUMENTBut 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_codeand implementation-specificerror_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 escalateAt 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 noneI'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_mswithout 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_actionmay be safe where:
acquire role finance-admin from group Xwould 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;
waitcan 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.
Reacted by Stève Sfartz- the host can deterministically act on
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
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