Proposals · SEP-3313 · Draft

Structured Tool-Failure Classification (`ToolFailure`)

Standards Track · Created 2026-08-27 · Source

Imported from upstream. Proposed as modelcontextprotocol#3313 by @johnyzaguirre-glean on 2026-08-27, where it was still open on 2026-09-25. Accepted into omcp on its technical merit. The text is the author's; only the status, the sponsor field, and documentation links were changed on import.

Reviewed 2026-09-25 — not yet accepted. Held as a draft because no prototype a reviewer can run is cited. Acceptance needs a prototype a reviewer can run, with its setup and the data to run it; Final also needs conformance tests and the reference documentation chapter.

Abstract

This SEP proposes a small, additive extension to CallToolResult that lets servers attach structured, machine-readable classification to a tool execution failure. Today, a failing tool call communicates only isError: true plus free-text content; clients, agents, and human operators must all parse prose to decide how to react or how to troubleshoot. This SEP adds an optional io.modelcontextprotocol/toolFailure _meta key carrying a broad behavioral class (capability / reliability / governance), an extensible code, retry hints, a mandatory operator correlationId, and a short agentGuidance string aimed at the calling model. The extension is fully additive: servers and clients that ignore it see no change in behavior.

Motivation

Discussion #2930 observed that MCP tool failures fall into a small number of recurring shapes — missing auth, permission denial, unsupported capability, transient failure, policy refusal, and "user must act" — but the spec gives servers no structured way to say which shape a given failure is. In practice this means:

The discussion converged on three ideas that this SEP formalizes:

  1. Split a broad behavioral class from a granular code, so clients can dispatch on class while servers keep the freedom to define new codes (@HarperZ9).
  2. Make the operator correlation ID mandatory within the classification object — an optional field doesn't get implemented consistently, and the entire point is reliable cross-referencing (this proposal's author, agreed by @YoadElkayam).
  3. Keep a short, separate agent-facing guidance string apart from verbose diagnostics, to avoid wasting model context (@YoadElkayam, mcp-fuse).

Specification

Placement: CallToolResult, not a protocol-level error

Per the existing specification (schema/draft/schema.ts, see the isError doc comment on CallToolResult), errors that originate from the tool itself must stay inside the result object so the calling model can see them and self-correct — they must not be reported as an MCP protocol-level (JSON-RPC) error. This SEP follows that rule: the new types extend CallToolResult, and no new JSON-RPC error code is allocated.

Extension point: _meta

The classification is carried under the existing, general-purpose _meta extension mechanism, using the same namespaced-key pattern already used for io.modelcontextprotocol/subscriptionId (SubscriptionsListenResultMetaObject). This keeps the change additive: no new required field appears on CallToolResult itself.

/**
 * Broad behavioral class of a tool failure, controlling how a client should
 * react independent of the specific ToolFailure.code:
 *
 * - `capability`: the operation itself cannot succeed as requested (e.g. an
 *   unsupported feature, missing permission). Stop and surface to the user
 *   or agent for a different approach — retrying as-is will not help.
 * - `reliability`: a transient condition. Safe to retry, honoring
 *   `retryable`/`retryNotBefore` and the tool's declared `idempotentHint`.
 * - `governance`: refused by policy (auth, approval, audit). Requires
 *   out-of-band action (e.g. a human approval), not a retry.
 */
export type ToolFailureClass = "capability" | "reliability" | "governance";

export interface ToolFailure {
  /** The broad behavioral class of this failure. */
  class: ToolFailureClass;

  /**
   * A specific, machine-readable failure code (e.g. "auth_required").
   * An open string, not a closed enum, so servers can define new codes
   * without a spec change. A baseline vocabulary is RECOMMENDED
   * (see below).
   */
  code: string;

  /**
   * Whether a client MAY retry this exact call. Only meaningful for
   * class: "reliability"; MUST be absent or false otherwise.
   *
   * Auto-retry eligibility also depends on the tool's declared
   * `idempotentHint` — a client MUST NOT auto-retry a non-idempotent tool
   * even when `retryable` is true.
   */
  retryable?: boolean;

  /**
   * Earliest time (Unix epoch milliseconds) at which a retry is likely to
   * succeed. A "not before" hint, not a guarantee. Only meaningful when
   * `retryable` is true.
   */
  retryNotBefore?: number;

  /**
   * A server-generated identifier correlating this failure with the
   * server's own logs, for operator troubleshooting. REQUIRED so operators
   * can reliably match a user-reported failure to server-side diagnostics;
   * intermediaries relaying this result MUST preserve it unchanged.
   */
  correlationId: string;

  /**
   * A short instruction for the calling model describing how to proceed
   * (e.g. "ask the user to re-authenticate"), kept separate from verbose
   * diagnostic detail in `content` so it doesn't waste context.
   */
  agentGuidance?: string;
}

export interface CallToolErrorMetaObject extends ResultMetaObject {
  /**
   * Structured classification of the failure. Present only on error
   * results (isError: true) from servers that implement this extension.
   */
  "io.modelcontextprotocol/toolFailure"?: ToolFailure;
}

CallToolResult._meta is typed as CallToolErrorMetaObject (widening its inherited ResultMetaObject type).

Baseline code vocabulary

code is intentionally open, but the following baseline values are RECOMMENDED so common cases interoperate out of the box, grouped by class:

class code
capability capability_unsupported, resource_not_found, invalid_input
reliability retryable_transient_error
governance auth_required, permission_denied, policy_blocked, user_action_required

Servers MAY define additional codes consistent with one of the three classes. Clients MUST dispatch primary behavior on class and treat code as informational/logging detail when they don't recognize it.

Client behavior

Rationale

Why _meta instead of a new top-level field on CallToolResult? A new required or optional top-level field is a larger surface to reason about for every existing CallToolResult consumer. _meta is the specification's existing, designed-for-this extension point, and there is precedent for exactly this shape of addition (SubscriptionsListenResultMetaObject). Piggybacking on it keeps this SEP fully additive and low-risk to adopt incrementally.

Why a class enum plus an open code string, rather than one closed enum? An early proposal in the discussion used a single closed set of codes. @HarperZ9 pointed out that the proposed codes actually require different client behavior (stop-and-prompt vs. retry vs. refuse-pending-approval), and that behavior dispatch should not depend on recognizing every possible code a server might emit. Splitting a small, closed class (behavior contract) from an open code (specific diagnosis) lets clients written today handle servers' future codes correctly by falling back to class.

Why is correlationId mandatory, not optional? The primary enterprise/operator pain point raised in the discussion was the inability to match a user's report of "the tool failed" to the corresponding server log line. An optional field is inconsistently implemented; making it required within the ToolFailure object (which itself remains optional) guarantees that any server adopting this extension gets the operator benefit, without forcing adoption on servers that don't implement the extension at all.

Alternatives considered:

Backward Compatibility

Fully backward compatible. io.modelcontextprotocol/toolFailure is an optional key on an already-optional, already-extensible _meta object. Existing servers that never set it are unaffected. Existing clients that don't read it see no change: they continue to interpret isError and content exactly as before. There are no changes to required fields, no removed fields, and no new JSON-RPC error codes.

Security Implications

Reference Implementation

A working prototype schema change (types, doc comments, and a passing example) is implemented in PR #3312, which adds ToolFailureClass, ToolFailure, and CallToolErrorMetaObject to schema/draft/schema.ts, wires _meta?: CallToolErrorMetaObject onto CallToolResult, and includes schema/draft/examples/ToolFailure/policy-blocked.json. All schema checks pass: npm run check:schema:ts (tsc/eslint/prettier) and npm run check:schema (259/259 example validations, including the new example).

A client-side reference implementation demonstrating retry dispatch on class (a small proxy or SDK patch) is not yet linked; this is required before the SEP can move past Draft/In-Review, per the prototype requirements.

Open Questions

Acknowledgments

Thanks to @Mohataseem89, @HarperZ9, and @YoadElkayam for the ideas developed in discussion #2930 that this SEP formalizes.