Proposals

omcp
Proposals

All 74 proposals in numeric order · Built from 40123d1 · 2026-09-29

SEP-414 Document OpenTelemetry Trace Context Propagation Conventions

Final · Standards Track · Created 2025-04-25

Abstract

This SEP documents conventions for OpenTelemetry (OTel) trace context propagation in MCP.

OTel semantic conventions for MCP specify using _meta as the carrier for W3C Trace Context keys. This is already in practice in the C# SDK and other implementations.

This specification documents an exception to the DNS prefixing convention for keys in _meta. This enables interoperability across existing and new implementations and serves as a foundation for related SEPs (such as SEP-2028).

Specification

This SEP adds documentation to the MCP specification, noting:

  1. When OTel trace context is propagated via _meta, the keys traceparent, tracestate, and baggage follow W3C Trace Context and W3C Baggage value formats.

  2. A non-normative example showing trace context in _meta.

  3. A note clarifying why this an exception to DNS prefixing keys in _meta: to remain compatible with existing implementations and the OpenTelemetry semantic conventions.

See agentclientprotocol/agent-client-protocol#297 for equivalent documentation changes in ACP.

Non-normative example

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "location": "New York"
    },
    "_meta": {
      "traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"
    }
  }
}

Rationale

Why document this?

This is currently documented elsewhere, but not as an MCP specification. Doing so ensures that SEPs depending on this pattern can complete, as well as other SDKs in and outside the MCP org can as well, such as Logfire and ToolHive.

If we don't document this shared concern, differing interpretations could materialize, such as namespacing traceparent like io.modelcontextprotocol.traceparent, which will break traces and log correlation.

  • SEP-1788 - reserved keys in _meta; should be updated with traceparent, tracestate, and baggage when this SEP is implemented
  • SEP-2028 - builds on this SEP for forwarding _meta values to HTTP headers

Backward Compatibility

This SEP documents existing conventions and is backward compatible.

Security Implications

Trace context in _meta may include correlation IDs. Implementations should follow existing data-handling guidance appropriate to their environment.

Reference Implementation

Existing implementations using this pattern:

SEP-932 Model Context Protocol Governance

Final · Process · Created 2025-07-08

  • Status: Final
  • Type: Process
  • Created: 2025-07-08
  • Author(s): David Soria Parra
  • PR: #931
  • Issue: #932

Abstract

This SEP establishes the formal governance model for the Model Context Protocol (MCP) project. It defines the organizational structure, decision-making processes, and contribution guidelines necessary for transparent and effective project stewardship. The proposal introduces a hierarchical governance structure with clear roles and responsibilities, along with the Specification Enhancement Proposal (SEP) process for managing protocol changes.

Motivation

As the Model Context Protocol grows in adoption and complexity, the need for formal governance becomes critical. The current informal decision-making process lacks:

  1. Transparency: Community members have no clear visibility into how decisions are made
  2. Participation Pathways: Contributors lack defined ways to influence project direction
  3. Accountability: No formal structure exists for resolving disputes or contentious issues
  4. Scalability: Ad-hoc processes cannot scale with growing community and technical complexity

Without formal governance, the project risks:

  • Fragmentation of the ecosystem
  • Unclear or inconsistent technical decisions
  • Reduced community trust and participation
  • Inability to effectively manage contributions at scale

Rationale

The proposed governance model draws inspiration from successful open source projects like Python, PyTorch, and Rust. Key design decisions include:

Hierarchical Structure

We chose a hierarchical model (Contributors → Maintainers → Core Maintainers → Lead Maintainers) that is effectively how the project decisions are made today. From there we will continue to evolve governance in the best interest of the project.

Individual vs Corporate Membership

Membership is explicitly tied to individuals rather than companies to:

  • Ensure decisions prioritize protocol integrity over corporate interests
  • Prevent capture by any single organization
  • Maintain continuity when individuals change employers

SEP Process

The Specification Enhancement Proposal process ensures:

  • All protocol changes undergo thorough review
  • Community input is systematically collected
  • Design decisions are documented for posterity
  • Implementation precedes finalization

Specification

Governance Structure

Contributors
  • Any individual who files issues, submits pull requests, or participates in discussions
  • No formal membership or approval required
Maintainers
  • Responsible for specific components (SDKs, documentation, etc.)
  • Appointed by Core Maintainers
  • Have write/admin access to their repositories
  • May establish component-specific processes
Core Maintainers
  • Deep understanding of MCP specification required
  • Responsible for protocol evolution and project direction
  • Meet bi-weekly for decisions
  • Can veto maintainer decisions by majority vote
  • Current members listed in governance documentation
Lead Maintainers
  • Justin Spahr-Summers and David Soria Parra
  • Can veto any decision
  • Appoint/remove Core Maintainers
  • Admin access to all infrastructure

Backwards Compatibility

N/A

Reference Implementation

See #931

  1. Documentation Files:
    • /docs/community/governance.mdx - Full governance documentation
    • /docs/community/sep-guidelines.mdx - SEP process guidelines

Security Implications

N/A

SEP-973 Expose additional metadata for Implementations, Resources, Tools and Prompts

Final · Standards Track · Created 2025-07-15

  • Status: Final
  • Type: Standards Track
  • Created: 2025-07-15
  • Author(s): @jesselumarie
  • Issue: #973

Abstract

This SEP proposes adding two optional fields—icons and websiteUrl. The icons and websiteUrl would be added to the Implementation schema so that clients can visually identify third-party implementations and link directly to their documentation. The icons parameter will also be added to the Tool, Resource and Prompt schemas. While this can be used by both servers and clients for all implementations, we expect it to be used initially for server-provided implementations.

Motivation

Current State

Current implementations only expose namespaced metadata, forcing clients to display generic labels with no visual cues.

Image

Proposed State

The proposed implementation would allow us to add visual affordances and links to documentation, making it easier to visually identify which servers/clients are providing an implementation e.g. a tool in a slash command interface:

Image
  • Visual Affordance: Icons make it immediately clear to users which tool or resource source is in use.
  • Discoverability: A link to documentation (websiteUrl) allows clients to direct users to more information with a single click.

Rationale

This design builds on prior work in web manifests (MDN) and consolidates community feedback:

  • Consolidation of PRs: Merges the changes from PR #417 and PR #862 into a single, cohesive enhancement.
  • Flexible Icon Sizes: Supports multiple icon sizes (e.g., 48x48, 96x96, or any for vector formats) to accommodate different client UI needs.
  • Optional Fields: By making both fields optional, existing implementations remain fully compatible.

Specification

Extend the Implementation object as follows:

/**
 * A url pointing to an icon URL or a base64-encoded data URI
 *
 * Clients that support rendering icons MUST support at least the following MIME types:
 * - image/png - PNG images (safe, universal compatibility)
 * - image/jpeg (and image/jpg) - JPEG images (safe, universal compatibility)
 *
 * Clients that support rendering icons SHOULD also support:
 * - image/svg+xml - SVG images (scalable but requires security precautions)
 * - image/webp - WebP images (modern, efficient format)
 */
export interface Icon {
  /**
   * A standard URI pointing to an icon resource.
   *
   * Consumers MUST takes steps to ensure URLs serving icons are from the
   * same domain as the client/server or a trusted domain.
   *
   * Consumers MUST take appropriate precautions when consuming SVGs as they can contain
   * executable JavaScript
   *
   * @format uri
   */
  src: string;
  /** Optional override if the server’s MIME type is missing or generic. */
  mimeType?: string;
  /** e.g. "48x48", "any" (for SVG), or "48x48 96x96" */
  sizes?: string;
}

/**
 * Describes the MCP implementation
 */
export interface Implementation extends BaseMetadata {
  version: string;
  /**
   * An optional list of icons for this implementation.
   * This can be used by clients to display the implementation in a user interface.
   * Each icon should have a `kind` property that specifies whether it is a data representation or a URL source, a `src` property that points to the icon file or data representation, and may also include a `mimeType` and `sizes` property.
   * The `mimeType` property should be a valid MIME type for the icon file, such as "image/png" or "image/svg+xml".
   * The `sizes` property should be a string that specifies one or more sizes at which the icon file can be used, such as "48x48" or "any" for scalable formats like SVG.
   * The `sizes` property is optional, and if not provided, the client should assume that the icon can be used at any size.
   */
  icons?: Icon[];
  /**
   * An optional URL of the website for this implementation.
   *
   * Consumers MUST takes steps to ensure URLs serving icons are from the
   * same domain as the client/server or a trusted domain.
   *
   * Consumers MUST take appropriate precautions when consuming SVGs as they can contain
   * executable JavaScript
   *
   * @format: uri
   */
  websiteUrl?: string;
}

Extend the Tool, Resource and Prompt interfaces with the following type:

  /**
   * An optional list of icons for a resource.
   * This can be used by clients to display the resource's icon in a user interface.
   * Each icon should have a `kind` property that specifies whether it is a data representation or a URL source, a `src` property that points to the icon file or data representation, and may also include a `mimeType` and `sizes` property.
   * The `mimeType` property should be a valid MIME type for the icon file, such as "image/png" or "image/svg+xml".
   * The `sizes` property should be a string that specifies one or more sizes at which the icon file can be used, such as "48x48" or "any" for scalable formats like SVG.
   * The `sizes` property is optional, and if not provided, the client should assume that the icon can be used at any size.
   */
  icons?: Icon[];

Backwards Compatibility

Both icons and websiteUrl are optional fields; clients that ignore them will fall back to existing behavior.

Security Implications

This shouldn't introduce any new security implications.

SEP-985 Align OAuth 2.0 Protected Resource Metadata with RFC 9728

Final · Standards Track · Created 2025-07-16

  • Status: Final
  • Type: Standards Track
  • Created: 2025-07-16
  • Author(s): sunishsheth2009
  • Issue: #985

Abstract

This proposal brings the MCP spec's handling of OAuth 2.0 Protected Resource Metadata in line with RFC 9728.

Currently, the MCP spec requires the use of the HTTP WWW-Authenticate header when returning a 401 Unauthorized to indicate the location of the protected resource metadata. However, RFC 9728, Section 5 states:

“A protected resource MAY use the WWW-Authenticate HTTP response header field, as discussed in RFC 9110, to return a URL to its protected resource metadata to the client.”

This suggests that the MCP spec could be made more flexible while still maintaining RFC compliance.

Rationale

Many large-scale, dynamic, multi-tenant environments rely on a centralized authentication service separate from the backend resource servers. In such deployments, injecting WWW-Authenticate headers from backend services is non-trivial due to separation of concerns and infrastructure complexity.

In these scenarios, having the option to discover metadata via a well-known URL provides a practical path forward for easier MCP adoption. Requiring only the header would impose significant communication overhead between components, especially when hundreds or thousands of MCP instances are created and destroyed dynamically. Also if there are specific managed MCP servers, adopting headers across centralized system would add significant overhead.

While this increases complexity for clients—who must now implement logic to probe metadata endpoints—it reduces friction for server deployments and may encourage broader adoption. There are tradeoffs:

Pros for Server Developers: Avoid complex header injection; simplifies integration in distributed environments.

Cons for Client Developers: Clients must fall back to metadata discovery logic when the header is absent, increasing client complexity.

Proposed State

Update the MCP spec to:

Clients MUST interpret the WWW-Authenticate header, and fallback to probing for metadata if not present.
Servers SHOULD return the WWW-Authenticate header

The reason for deviating a bit on the RFC: Go with SHOULD over MAY for WWW-Authenticate is that it makes supporting other features, such as incremental authorization easier (e.g. you make a request for a tool, but need additional scopes, and receive a WWW-Authenticate challenge indicating the scopes).

Based on the above, following the updated flow:

  • Attempt the MCP request without a token.
  • If a 401 Unauthorized response is received: Check for a WWW-Authenticate header. If present and includes the resource_metadata parameter, use it to locate the resource metadata.
  • If the header is absent or does not include resource_metadata, fallback to requesting /.well-known/oauth-protected-resource.

This change allows more flexible deployment models without removing existing capabilities.

Authorization ServerMCP Server (Resource Server)ClientAuthorization ServerMCP Server (Resource Server)ClientAttempt unauthenticated MCP requestExtract resource_metadata URL from headerFallback to metadata probingAbort or use pre-configured valuesalt[Metadata found][Metadata not found]alt[Header includes resource_metadata][No resource_metadata in header]Validate RS metadata,build AS metadata URLOAuth 2.1 authorization flow happens hereMCP communication continues with valid tokenMCP request without tokenHTTP 401 Unauthorized (may include WWW-Authenticate header)GET resource_metadata URIResource metadata with authorization server URLGET /.well-known/oauth-protected-resourceResource metadata with authorization server URLGET /.well-known/oauth-authorization-serverAuthorization server metadataToken requestAccess tokenMCP request with access tokenMCP response

Backward Compatibility

This proposal is fully backward-compatible.

It retains support for the WWW-Authenticate header (already in the spec) and introduces a fallback mechanism using the .well-known metadata path, which is already defined in MCP as a MUST-support location.

Clients that already support metadata probing benefit from improved interoperability. Servers are not required to emit the WWW-Authenticate header if it is infeasible, but doing so is still encouraged to reduce client complexity and enable future extensibility.

SEP-986 Specify Format for Tool Names

Final · Standards Track · Created 2025-07-16

  • Status: Final
  • Type: Standards Track
  • Created: 2025-07-16
  • Author(s): kentcdodds
  • Issue: #986

Abstract

The Model Context Protocol (MCP) currently lacks a standardized format for tool names, resulting in inconsistencies and confusion for both implementers and users. This SEP proposes a clear, flexible standard for tool names: tool names should be 1–64 characters, case-sensitive, and may include alphanumeric characters, underscores (_), dashes (-), dots (.), and forward slashes (/). This aims to maximize compatibility, clarity, and interoperability across MCP implementations while accommodating a wide range of naming conventions.

Motivation

Without a prescribed format for tool names, MCP implementations have adopted a variety of naming conventions, including different separators, casing, and character sets. This inconsistency can lead to confusion, errors in tool invocation, and difficulties in documentation and automation. Standardizing the allowed characters and length will:

  • Make tool names predictable and interoperable across clients.
  • Allow for hierarchical and namespaced tool names (e.g., using / and .).
  • Support both human-readable and machine-generated names.
  • Avoid unnecessary restrictions that could block valid use cases.

Rationale

Community discussion highlighted the need for flexibility in tool naming. While some conventions (like lower-kebab-case) are common, many tools and clients use uppercase, underscores, dots, and slashes for namespacing or clarity. The proposed pattern—allowing a-z, A-Z, 0-9, _, -, ., and /—is based on patterns used in major clients (e.g., VS Code, Claude) and aligns with common conventions in programming and APIs. Restricting spaces and commas avoids parsing issues and ambiguity. The length limit (1–64) is generous enough for most use cases but prevents abuse.

Specification

  • Tool names SHOULD be between 1 and 64 characters in length (inclusive).
  • Tool names are case-sensitive.
  • Allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), dash (-), dot (.), and forward slash (/).
  • Tool names SHOULD NOT contain spaces, commas, or other special characters.
  • Tool names SHOULD be unique within their namespace.
  • Example valid tool names:
    • getUser
    • user-profile/update
    • DATA_EXPORT_v2
    • admin.tools.list

Backwards Compatibility

This change is not backwards compatible for existing tools that use disallowed characters or exceed the new length limits. To minimize disruption:

  • Existing non-conforming tool names SHOULD be supported as aliases for at least one major version, with a deprecation warning.
  • Tool authors SHOULD update their documentation and code to use the new format.
  • A migration guide SHOULD be provided to assist implementers in updating their tool names.

Reference Implementation

A reference implementation can be provided by updating the MCP core library to enforce the new tool name validation rules at registration time. Existing tools can be updated to provide aliases for their new conforming names, with warnings for deprecated formats. Example code and migration scripts can be included in the MCP repository.

Security Implications

None. Standardizing tool name format does not introduce new security risks.

SEP-990 Enable enterprise IdP policy controls during MCP OAuth flows

Final · Standards Track · Created 2025-06-04

  • Status: Final
  • Type: Standards Track
  • Created: 2025-06-04
  • Author(s): Aaron Parecki (@aaronpk)
  • PR: #646
  • Issue: #990

Abstract

This extension is designed to facilitate secure and interoperable authorization of MCP clients within corporate environments, leveraging existing enterprise identity infrastructure.

  • For end users, this removes the need to manually connect and authorize the MCP Client to individual services within the organization.
  • For enterprise admins, this enables visibility and control over which MCP Servers are able to be used within the organization.

How Has This Been Tested?

We have an end to end implementation of this here, and in-progress MCP implementations with some partners.

Breaking Changes

This is designed to augment the existing OAuth profile by providing an alternative when used under an enterprise IdP. MCP clients can opt in to this profile when necessary.

Additional Context

For more background on this problem, you can refer to my blog post about this here:

Enterprise-Ready MCP

I also presented this at the MCP Dev Summit in May.

A high level overview of the flow is below:

Identity ProviderMCP Resource ServerMCP Authorization ServerMCP ClientBrowserIdentity ProviderMCP Resource ServerMCP Authorization ServerMCP ClientBrowserUser Logs InUser is loggedin to MCP Client.Client stores ID Token.Evaluate PolicyValidate ID-JAGloop​Redirect to IdPRedirect to IdPIdP Authorization CodeIdP Authorization CodeToken Request with IdP Authorization CodeID TokenExchange ID Token for ID-JAGResponds with ID-JAGToken Request with ID-JAGMCP Access TokenCall MCP API with Access TokenMCP Response with Data

[!IMPORTANT] State: Ready to Review

SEP-991 Enable URL-based Client Registration using OAuth Client ID Metadata Documents

Final · Standards Track · Created 2025-07-07

  • Status: Final
  • Type: Standards Track
  • Created: 2025-07-07
  • Author(s): Paul Carleton (@pcarleton) Aaron Parecki (@aaronpk)
  • Issue: #991

SEP: OAuth Client ID Metadata Documents for MCP

Abstract

This SEP proposes adopting OAuth Client ID Metadata Documents as specified in draft-parecki-oauth-client-id-metadata-document-03 as an additional client registration mechanism for the Model Context Protocol (MCP). This approach allows OAuth clients to use HTTPS URLs as client identifiers, where the URL points to a JSON document containing client metadata. This specifically addresses the common MCP scenario where servers and clients have no pre-existing relationship, enabling servers to trust clients without pre-coordination while maintaining full control over access policies.

Motivation

The Model Context Protocol currently supports two client registration approaches:

  1. Pre-registration: Requires either client developers or users to manually register clients with each server
  2. Dynamic Client Registration (DCR): Allows just-in-time registration by sending client metadata to a register endpoint on the Authorization server.

Both approaches have significant limitations for MCP's use case where clients frequently need to connect to servers they've never encountered before:

  • Pre-registration by developers is impractical as servers may not exist when clients ship
  • Pre-registration by users creates poor UX requiring manual credential management
  • DCR requires servers to manage unbounded databases, handle expiration, and trust self-asserted metadata

The Target Use Case: No Pre-existing Relationship

This proposal specifically targets the common MCP scenario where:

  • A user wants to connect a client to a server they've discovered
  • The client developer has never heard of this server
  • The server operator has never heard of this client
  • Both parties need to establish trust without prior coordination

For scenarios with pre-existing relationships, pre-registration remains the optimal solution. However, MCP's value comes from its ability to connect arbitrary clients and servers, making the "no pre-existing relationship" case critical to address.

Relatedly, there are many more MCP servers than there are clients (similar to how there are many more web browsers than API's). A common scenario is an MCP server developer wanting to restrict usage to a set of clients they trust.

Key Innovation: Server-Controlled Trust Without Pre-Coordination

Client ID Metadata Documents enable a unique trust model where:

  1. Servers can trust clients they've never seen before based on:

    • The HTTPS domain hosting the metadata
    • The metadata content itself
    • Domain reputation and security policies
  2. Servers maintain full control through flexible policies:

    • Open Servers: Can accept any HTTPS client_id, enabling maximum interoperability
    • Protected Servers: Can restrict to trusted domains or specific clients
  3. No client pre-coordination required:

    • Clients don't need to know about servers in advance
    • Clients just need to host their metadata document
    • Trust flows from the client's domain, not prior registration

Specification Changes

The change to the specification will be adding Client ID Metadata documents as a SHOULD, and changing DCR to a MAY, as we think that Client ID Metadata documents are a better default option for this scenario.

We will primarily rely on the text in the linked RFC, aiming not to repeat most of it. Below is a short version of what we'll need to specify.

MCP ServerMetadata Endpoint(Client's HTTPS URL)Authorization ServerMCP ClientUserMCP ServerMetadata Endpoint(Client's HTTPS URL)Authorization ServerMCP ClientUserClient hosts metadata athttps://app.example.com/oauth/metadata.jsonAuthenticates userDetects URL-formatted client_idValidates:1. client_id matches URL2. redirect_uri in allowed list3. Document structure valid4. Domain allowed via trust policyalt[Validation Success][Validation Failure]Cache metadata for future requests(respecting HTTP cache headers)Initiates connection to MCP ServerAuthorization Requestclient_id=https://app.example.com/oauth/metadata.jsonredirect_uri=http://localhost:3000/callbackGET https://app.example.com/oauth/metadata.jsonJSON Metadata Document{client_id, client_name, redirect_uris, ...}Display consent page with client_nameApproves accessAuthorization code via redirect_uriExchange code for tokenclient_id=https://app.example.com/oauth/metadata.jsonAccess tokenMCP requests with access tokenMCP responsesError responseerror=invalid_client or invalid_request

Client Requirements

  • Clients MUST host their metadata document at an HTTPS URL following RFC requirements
  • The client_id URL MUST use "https" scheme and contain a path component
  • Metadata documents MUST be valid JSON and include at minimum:
    • client_id: matching the document URL exactly
    • client_name: human-readable name for authorization prompts
    • redirect_uris: array of allowed redirect URIs
    • token_endpoint_auth_method: "none" for public clients

Note a client can use private_key_jwt for a token_endpoint_auth_method given the client metadata can provide public key information.

Server Requirements

  • Servers SHOULD fetch metadata documents when encountering URL-formatted client_ids
  • Servers MUST validate the fetched document contains matching client_id
  • Servers SHOULD cache metadata respecting HTTP headers (max 24 hours recommended)
  • Servers MUST validate redirect URIs match those in metadata document

Discovery

  • Servers advertise support via OAuth metadata: client_id_metadata_document_supported: true
  • Clients detect support and can fallback to DCR or pre-registration if unavailable

Example metadata document:

{
  "client_id": "https://app.example.com/oauth/client-metadata.json",
  "client_name": "Example MCP Client",
  "client_uri": "https://app.example.com",
  "logo_uri": "https://app.example.com/logo.png",
  "redirect_uris": [
    "http://127.0.0.1:3000/callback",
    "http://localhost:3000/callback"
  ],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

Integration with Existing MCP Auth

This proposal adds Client ID Metadata Documents as a third registration option alongside pre-registration and DCR. Servers MAY support any combination of these approaches:

  • Pre-registration remains unchanged
  • DCR remains unchanged
  • Client ID Metadata Documents are detected by URL-formatted client_ids, and server support is advertised in OAuth metadata.

Rationale

Why This Solves the "No Pre-existing Relationship" Problem

Unlike pre-registration which requires coordination, or DCR which requires servers to manage a registration database, Client ID Metadata Documents provide:

  1. Verifiable Identity: The HTTPS URL serves as both identifier and trust anchor
  2. No Coordination Needed: Clients publish metadata, servers consume it
  3. Flexible Trust Policies: Servers decide their own trust criteria without requiring client changes
  4. Stable Identifiers: Unlike DCR's ephemeral IDs, URLs are stable and auditable

Redirect URI Attestation

A key benefit of Client ID Metadata Documents is attestation of redirect URIs:

  1. The metadata document cryptographically binds redirect URIs to the client identity via HTTPS
  2. Servers can trust that redirect URIs in the metadata are controlled by the client - not attacker-supplied
  3. This prevents redirect URI manipulation attacks common with self-asserted registration

Risks of this approach

Risk: Localhost URL Impersonation

A limitation of Client ID Metadata Documents is that they cannot prevent localhost URL impersonation by itself. An attacker can claim to be any client by:

  1. Providing the legitimate client's metadata URL as their client_id
  2. Binding to the same localhost port the legitimate client uses
  3. Intercepting the authorization code when the user approves

This attack is concerning because the server sees the correct metadata document and the user sees the correct client name, making detection difficult.

Platform-specific attestations (iOS DeviceCheck, Android Play Integrity) could address this, but they're not universally available. This would work by a developer running a backend service that consumes the DeviceCheck / Play Integrity signatures and returns a JWT usable as the private_key_jwt authentication for the token_endpoint_auth_method.

A similar approach without requiring platform-specific attestations that still raises the cost of the attack is possible using JWKS and short-lived JWTs signed by a server-side component hosted by the client developer. This component could use attestation mechanisms other than platform-specific ones to attest to the clients identity, such as the client's standard login flow. Using short lived JWTs reduces the risk of credential compromise and replay, but does not eliminate it entirely - an attacker could still proxy requests to the legitimate client's signing endpoint.

Fully mitigating this risk is outside the scope of this proposal. This proposal has the same risks as DCR does in a localhost redirect scenario.

Servers SHOULD display additional warnings for localhost-only clients.

Risk: Server Side Request Forgery (SSRF)

The authorization server takes a URL as input from an unknown client, and then fetches that URL. A malicious client could use this to send non-metadata requests on behalf of the authorization server. An example would be sending a URL corresponding to a private administration endpoint that the authorization server has access to.

This can be prevented by validating the URL's and the IP's those URL's resolve to prior to initiating a fetch request.

Risk: Distributed Denial of Service (DDoS)

Similarly, an attacker could try to leverage a pool of authorization servers to perform a denial of service attack on a non-MCP server.

There is not any additional amplification for the fetch request (i.e. the bandwidth from the client to make the request roughly equals the bandwidth of the request sent to the target server), and each authorization server can aggressively cache the result of these metadata fetches, so it is unlikely to be an attractive DDoS vector.

Risk: Maturity of referenced specification

The RFC for Client ID Metadata documents is still a draft. It has been implemented by the platform Bluesky, but has not been ratified or very widely adopted outside of that, and may evolve over time. Our intention is to evolve and align with subsequent drafts and any final standard, while minimizing disruption and breakage with existing implementations.

This approach has the risk that there are implementation challenges or flaws in the protocol that have not surfaced yet. However, even though DCR has been ratified, and it also has a number of implementation challenges that developers are facing when trying to use it in an open ecosystem context like MCP. Those challenges are the motivation behind this proposal.

Risk: Client implementation burden, especially local clients

This specification requires an additional piece of infrastructure for clients, since they need to host a metadata file behind an HTTPS url. Without this specification, a client could be strictly a desktop application for example.

The burden of hosting this endpoint is expected to be low as hosting a static JSON file is fairly straightforward and most known clients have a webpage advertising their client or providing download links.

Risk: Fragmentation of authorization approaches

Authorization for MCP is already challenging to fully implement for clients and servers. Questions about how to do it correctly and best practices are some of the most common in the community. Adding another branch to the authorization flow means this could be even more complicated and fractured, meaning fewer developers succeed in following the specification, and the promise of compatibility and an open ecosystem suffers as a result.

This proposal intends to simplify the story for authorization server and resource server developers by providing a clearer mechanism to trust redirect URIs and less operational overhead. This proposal depends on that simplicity being clearly the better option for most folks, which will drive more adoption and end up being the most supported option. If we do not believe that it is clearly the better option, then we should not adopt this proposal.

This proposal also provides a unified mechanism for both open servers and servers that want to restrict which clients can be used. Alternatives to this proposal require that clients and servers implement different mechanisms for the open and protected use cases.

Alternatives Considered

  1. Enhanced DCR with Software Statements: More complex, requires JWKS hosting and JWT signing
  2. Mandatory Pre-registration: Poor developer and user experience for MCP's distributed ecosystem
  3. Mutual TLS: Requires trusting a client certificate authority, impractical in an open ecosystem
  4. Status Quo: Continues current pain points for server implementers

Client ID Metadata document is a strict improvement over DCR for the most common open-ecosystem use case. It can be further extended in the future to better support things like OS-level attestations and jwks_uri's.

Backward Compatibility

This proposal is fully backward compatible:

  • Existing pre-registered clients continue working unchanged
  • Existing DCR implementations continue working unchanged
  • Servers can adopt Client ID Metadata Documents incrementally
  • Clients can detect support and fall back to other methods

Prototype Implementation

A prototype implementation is available here demonstrating:

  1. Client-side metadata document hosting
  2. Server-side metadata fetching and validation
  3. Integration with existing MCP OAuth flows
  4. Proper error handling and fallback behavior

Security Implications

  1. Phishing Prevention: Display client hostname prominently
  2. SSRF Protection: Validate URLs, limit response size, timeout requests, rate limit outbound requests

Best Practices

  • Only fetch client metadata after authenticating the user
  • Implement rate limiting on outbound metadata fetches
  • Consider additional warnings for new/unknown/localhost domains
  • Log metadata fetch failures for monitoring

References

SEP-994 Shared Communication Practices/Guidelines

Final · Process · Created 2025-07-17

  • Status: Final
  • Type: Process
  • Created: 2025-07-17
  • Author(s): @localden
  • Issue: #994
  • PR: #1002

Abstract

This SEP establishes the communication strategy and framework for the Model Context Protocol community. It defines the official channels for contributor communication, guidelines for their use, and processes for decision documentation.

Motivation

As the MCP community grows, clear communication guidelines are essential for:

  • Consistency: Ensuring all contributors know where and how to communicate
  • Transparency: Making project decisions visible and accessible
  • Efficiency: Directing discussions to the most appropriate channels
  • Security: Establishing proper processes for handling sensitive issues

Specification

Communication Channels

The MCP project uses three primary communication channels:

  1. Discord: For real-time or ad-hoc discussions among contributors
  2. GitHub Discussions: For structured, longer-form discussions
  3. GitHub Issues: For actionable tasks, bug reports, and feature requests

Security-sensitive issues follow a separate process defined in SECURITY.md.

Discord Guidelines

The Discord server is designed for MCP contributors and is not intended for general MCP support.

Public Channels (Default)
  • Open community engagement and collaborative development
  • SDK and tooling development discussions
  • Working and Interest Group discussions
  • Community onboarding and contribution guidance
  • Office hours and maintainer availability
Private Channels (Exceptions)

Private channels are reserved for:

  • Security incidents (CVEs, protocol vulnerabilities)
  • People matters (maintainer discussions, code of conduct)
  • Coordination requiring immediate focused response

All technical and governance decisions must be documented publicly in GitHub.

GitHub Discussions

Used for structured, long-form discussion:

  • Project roadmap planning
  • Announcements and release communications
  • Community polls and consensus-building
  • Feature requests with context and rationale

GitHub Issues

Used for actionable items:

  • Bug reports with reproducible steps
  • Documentation improvements
  • CI/CD and infrastructure issues
  • Release tasks and milestone tracking

Decision Records

All MCP decisions are documented publicly:

  • Technical decisions: GitHub Issues and SEPs
  • Specification changes: Changelog on the MCP website
  • Process changes: Community documentation
  • Governance decisions: GitHub Issues and SEPs

Decision documentation includes:

  • Decision makers
  • Background context and motivation
  • Options considered
  • Rationale for chosen approach
  • Implementation steps

Rationale

This framework balances openness with practicality:

  • Public by default: Maximizes transparency and community participation
  • Private when necessary: Protects security and personal matters
  • Channel separation: Keeps discussions organized and searchable
  • Documentation requirements: Ensures decisions are preserved and discoverable

Backward Compatibility

This SEP establishes new processes and does not affect existing protocol functionality.

Reference Implementation

The communication guidelines are published at: https://omcp.tech/community/communication

SEP-1024 MCP Client Security Requirements for Local Server Installation

Final · Standards Track · Created 2025-07-22

  • Status: Final
  • Type: Standards Track
  • Created: 2025-07-22
  • Author(s): Den Delimarsky
  • Issue: #1024

Abstract

This SEP addresses critical security vulnerabilities in MCP client implementations that support one-click installation of local MCP servers. The current MCP specification lacks explicit security requirements for client-side installation flows, allowing malicious actors to execute arbitrary commands on user systems through crafted MCP server configurations distributed via links or social engineering.

This proposal establishes a best practice for MCP clients, requiring explicit user consent before executing any local server installation commands and complete command transparency.

Motivation

The existing MCP specification does not address client-side security concerns related to streamlined ("one-click") local server configuration. Current MCP clients that implement these configuration experiences create significant attack vectors:

  1. Silent Command Execution: MCP clients can automatically execute embedded commands without user review or consent when installing local servers via one-click flows.

  2. Lack of Visibility: Users have no insight into what commands are being executed on their systems, creating opportunities for data exfiltration, system compromise, and privilege escalation.

  3. Social Engineering Vulnerabilities: Users become comfortable executing commands labeled as "MCP servers" without proper scrutiny, making them susceptible to malicious configurations.

  4. Arbitrary Code Execution: Attackers can embed harmful commands in MCP server configurations and distribute them through legitimate channels (repositories, documentation, social media).

Visual Studio Code addressed this by implementing consent dialogs. Similarly, Cursor also supports a consent dialog for one-click local MCP server installation.

Without explicit security requirements in the specification, MCP client implementers may unknowingly create vulnerable installation flows, putting end users at risk of system compromise.

Specification

Client Security Requirements

MCP clients that support one-click local MCP server configuration MUST implement the following security controls:

Before executing any command to install or configure a local MCP server, the MCP client MUST:

  1. Display a clear consent dialog that shows:

    • The exact command that will be executed, without truncation
    • All arguments and parameters
    • A clear warning that this operation may be potentially dangerous
  2. Require explicit user approval through an affirmative action (button click, checkbox, etc.)

  3. Provide an option for users to cancel the installation

  4. Not proceed with installation if consent is denied or not provided

Rationale

Design Decisions

Mandatory Consent Dialogs: The requirement for explicit consent dialogs balances security with usability. While this adds friction to the MCP server configuration process, it prevents potential breaches from silent command execution.

Backward Compatibility

This SEP introduces new requirements for MCP client implementations but does not change the core MCP protocol or wire format.

Impact Assessment:

  • Low Impact: Existing MCP servers and the core protocol remain unchanged
  • Client Implementation Required: MCP clients must update their local server installation flows to comply with new security requirements
  • User Experience Changes: Users will see consent dialogs where none existed before

Migration Path:

  1. MCP clients can implement these changes in new versions without breaking existing functionality
  2. Existing installed MCP servers continue to work normally
  3. Only new installation flows require the consent mechanisms

No protocol-level backward compatibility issues exist, as this SEP addresses client behavior rather than the MCP wire protocol.

Reference Implementation

N/A

Security Implications

Security Benefits

This SEP directly addresses:

  • Arbitrary Code Execution: Prevents silent execution of malicious commands
  • Social Engineering: Forces users to consciously review commands before execution
  • Supply Chain Attacks: Creates visibility into MCP server installation commands
  • Privilege Escalation: Users can identify and reject commands requesting elevated privileges

Residual Risks

Even with these controls, risks remain:

  • User Override: Users may approve malicious commands despite warnings
  • Sophisticated Obfuscation: Advanced attackers may craft commands that appear legitimate
  • Implementation Gaps: Clients may implement controls incorrectly

Risk Mitigation

These residual risks are addressed through:

  • Clear warning language in consent dialogs
  • Recommendation for additional security layers (sandboxing, signatures)
  • Ongoing security research and community awareness

SEP-1034 Support default values for all primitive types in elicitation schemas

Final · Standards Track · Created 2025-07-22

  • Status: Final
  • Type: Standards Track
  • Created: 2025-07-22
  • Author(s): Tapan Chugh (chugh.tapan@gmail.com)
  • Issue: #1034

Abstract

This SEP recommends adding support for default values to all primitive types in the MCP elicitation schema (StringSchema, NumberSchema, and EnumSchema), extending the existing support that only covers BooleanSchema.

Motivation

Elicitations in MCP offer a way to mitigate complex API designs: tools can request information on-demand rather than resorting to convoluted parameter handling. The challenge however is that users must manually enter obvious information that could be pre-populated for more natural interactions. Currently, only BooleanSchema supports default values in elicitation requests. This limitation prevents servers from providing sensible defaults for text inputs, numbers, and enum selections leading to more user overhead.

Real-World Example

Consider implementing an email reply function. Without elicitation, the tool becomes unwieldy:

def reply_to_email_thread(
    thread_id: str,
    content: str,
    recipient_list: List[str] = [],
    cc_list: List[str] = []
) -> None:
    # Ambiguity: Does empty list mean "no recipients" or "use defaults"?
    # Complex logic needed to handle different combinations

With elicitation, the tool signature itself can be much simpler

def reply_to_email_thread(
    thread_id: str,
    content: Optional[str] = ""
) -> None:
    # Code can lookup the participants from the original thread
    # and prepare an elicitation request with the defaults setup
const response = await client.request("elicitation/create", {
  message: "Configure email reply",
  requestedSchema: {
    type: "object",
    properties: {
      recipients: {
        type: "string",
        title: "Recipients",
        default: "alice@company.com, bob@company.com"  // Pre-filled
      },
      cc: {
        type: "string",
        title: "CC",
        default: "john@company.com"  // Pre-filled
      },
      content: {
        type: "string",
        title: "Message"
        default: "" // If provided in the tool above
      }
    }
  }
});

Implementation

A working implementation demonstrating clients require minimal changes to display defaults (~10 lines of code):

Specification

Schema Changes

Extend the elicitation primitive schemas to include optional default values:

export interface StringSchema {
  type: "string";
  title?: string;
  description?: string;
  minLength?: number;
  maxLength?: number;
  format?: "email" | "uri" | "date" | "date-time";
  default?: string; // NEW
}

export interface NumberSchema {
  type: "number" | "integer";
  title?: string;
  description?: string;
  minimum?: number;
  maximum?: number;
  default?: number; // NEW
}

export interface EnumSchema {
  type: "string";
  title?: string;
  description?: string;
  enum: string[];
  enumNames?: string[];
  default?: string; // NEW - must be one of enum values
}

// BooleanSchema already has default?: boolean

Behavior

  1. The default field is optional, maintaining full backward compatibility
  2. Default values must match the schema type
  3. For EnumSchema, the default must be one of the valid enum values
  4. Clients that support defaults SHOULD pre-populate form fields. Clients that don't support defaults MAY ignore the field entirely.

Rationale

  1. The high-level rationale is to follow the precedent set by BooleanSchema rather than creating new mechanisms.
  2. Making defaults optional ensures backward compatibility.
  3. This maintains the high-level intuition of keeping the client implementation simple.

Alternatives Considered

  1. Server-side Templates: Servers could maintain templates separately, but this adds complexity
  2. New Request Type: A separate request type for forms with defaults would fragment the API
  3. Required Defaults: Making defaults required would break existing implementations

Backwards Compatibility

This change is fully backward compatible with no breaking changes. Clients that don't understand defaults will ignore them, and existing elicitation requests continue to work unchanged. Clients can adopt default support at their own pace

Security Implications

No new security concerns:

  1. No Sensitive Data: The existing guidance against requesting sensitive information still applies
  2. Client Control: Clients retain full control over what data is sent to servers
  3. User Visibility: Default values are visible to users who can modify them before submission

SEP-1036 URL Mode Elicitation for secure out-of-band interactions

Final · Standards Track · Created 2025-07-22

  • Status: Final
  • Type: Standards Track
  • Created: 2025-07-22
  • Author(s): Nate Barbettini (@nbarbettini) and Wils Dawson (@wdawson)
  • Issue: #1036

Abstract

This SEP introduces a new url mode for the existing elicitation client capability, enabling secure out-of-band interactions that bypass the MCP client. URL mode elicitation addresses sensitive use cases that form mode elicitation cannot, such as gathering sensitive credentials, performing OAuth flows for external (3rd-party) authorization, and handling payments, without exposing sensitive data to the MCP client. By directing users to trusted URLs in their browser, this mode maintains security boundaries while enabling rich integrations with third-party services.

Motivation

The current MCP specification (2025-06-18) provides an elicitation mechanism for gathering non-sensitive information from users through structured, in-band requests (most commonly imagined as the MCP client rendering a form to collect data from the end-user). However, several critical use cases require interactions that must not pass through the MCP client:

  1. Sensitive data collection: API keys, passwords, and other credentials must never transit through intermediary systems.
  2. External authorization: MCP servers often need to access third-party APIs on behalf of users. The MCP authorization specification only covers client-to-server authorization, not server-to-third-party authorization. The Security Best Practices document explicitly forbids token passthrough, requiring a secure mechanism for external (3rd-party) OAuth flows. This was a particularly important motivating factor emerging from discussions in #234 and #284.
  3. Payment and Subscription Flows: Financial transactions require PCI compliance and secure payment processing that cannot be achieved through in-band data collection.

Without a standardized mechanism for these interactions, MCP servers must resort to non-standard workarounds or insecure practices like requesting API keys through in-band, form-style elicitation. This SEP addresses these gaps by introducing a URL elicitation mode that leverages established web security patterns to handle sensitive interactions securely.

URL elicitation is fundamentally different from MCP authorization. URL elicitation is not for authorizing the MCP client's access to the MCP server (that's handled directly by MCP authorization). Instead, it's used when the MCP server needs to obtain sensitive information or third-party authorization on behalf of the user. The MCP client's bearer token remains unchanged, and the client's only responsibility is to provide the user with context about the elicitation URL the server wants them to open.

Specification

Overview

Elicitation is updated to support two modes:

  • Form mode (in-band): Servers can request structured data from users with optional JSON schemas to validate responses (no change here, other than adding a name to the existing capability)
  • URL mode (out-of-band): Servers can direct users to external URLs for sensitive interactions that must not pass through the MCP client

Capabilities

Clients that support elicitation MUST declare the elicitation capability during initialization:

{
  "capabilities": {
    "elicitation": {
      "form": {},
      "url": {}
    }
  }
}

For backwards compatibility, an empty capabilities object is equivalent to declaring support for form mode only:

{
  "capabilities": {
    "elicitation": {},
  },
}

Clients declaring the elicitation capability MUST support at least one mode (form or url).

Form Elicitation Requests

The only change from the existing specification is the addition of a mode field in the elicitation/create request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "elicitation/create",
  "params": {
    "mode": "form", // New field
    "message": "Please provide your GitHub username",
    "requestedSchema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        }
      },
      "required": ["name"]
    }
  }
}

URL Elicitation Requests

URL elicitation requests MUST specify mode: "url" and include these parameters:

Name Type Description
url string The URL that the user should navigate to.
elicitationId string A unique identifier for the elicitation.
message string A human-readable message explaining why the interaction is needed.
Example: OAuth Authorization Flow
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "elicitation/create",
  "params": {
    "mode": "url",
    "elicitationId": "550e8400-e29b-41d4-a716-446655440000",
    "url": "https://github.com/login/oauth/authorize?client_id=abc123&state=xyz789&scope=repo",
    "message": "Please authorize access to your GitHub repositories to continue."
  }
}
Response Actions

URL elicitation responses use the same three-action model as form elicitation:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "action": "accept" // or "decline" or "cancel"
  }
}

The response with action: "accept" indicates that the user has consented to the interaction. The interaction occurs out of band and the client is not aware of the outcome unless the server sends a completion notification.

Completion Notifications

Servers SHOULD send a notifications/elicitation/complete notification when an out-of-band interaction started by URL mode elicitation is completed. This allows clients to react programmatically if appropriate.

  • The notification MUST only be sent to the client that initiated the elicitation request.
  • The notification MUST include the elicitationId established in the original elicitation/create request.
  • Clients MUST ignore notifications referencing unknown or already-completed IDs.
  • If a completion notification never arrives, clients SHOULD provide a manual way for the user to continue the interaction.

Clients MAY use the notification to automatically retry requests that received a URL elicitation required error, update the user interface, or otherwise continue an interaction. However, because delivery of the notification is not guaranteed, clients must not wait indefinitely for a notification from the server.

{
  "jsonrpc": "2.0",
  "method": "notifications/elicitation/complete",
  "params": {
    "elicitationId": "550e8400-e29b-41d4-a716-446655440000"
  }
}
URL Elicitation Required Error

When a request cannot be processed until an elicitation is completed, the server MAY return a URLElicitationRequiredError (code -32042) to indicate that a URL mode elicitation is required. The server MUST NOT return this error except when URL mode elicitation is required by the user interaction.

{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32042,
    "message": "This request requires more information.",
    "data": {
      "elicitations": [
        {
          "mode": "url",
          "elicitationId": "550e8400-e29b-41d4-a716-446655440000",
          "url": "https://oauth.example.com/authorize?client_id=abc123&response_type=code&...",
          "message": "Authorization is required to access your Example Co files."
        }
      ]
    }
  }
}

Any elicitations returned in the error MUST be URL mode elicitations and include an elicitationId.

Returning a URLElicitationRequiredError is equivalent to sending an elicitation/create request. The server may return an error (instead of sending a separate elicitation/create request) as an affordance to the client to make it clear that a particular elicitation is directly related to a failed client request.

The client must treat URLElicitationRequiredError responses as equivalent to elicitation/create requests. Clients may automatically retry the failed request after the elicitation is completed successfully, for example after receiving a completion notification.

Rationale

Design Decisions

Why extend elicitation instead of creating a new mechanism?

Initially, we considered creating a separate mechanism for out-of-band interactions (discussed in #475). However, after discussions with the MCP maintainers, we decided to extend the existing elicitation specification because:

  1. Both mechanisms serve the same fundamental purpose: gathering information from users
  2. Having two similar-but-separate mechanisms for the same purpose is confusing and error-prone
  3. The mode parameter cleanly separates the two interaction patterns

Why can't the client perform the interaction itself?

It is tempting to suggest that the MCP client should perform the interaction itself, e.g. act as an OAuth client to a third-party authorization server. However, there are several reasons why this is not a good idea:

  • If the MCP client obtains user tokens from a third-party authorization server, the MCP server becomes a token passthrough server, which is explicitly forbidden.
  • Similarly, for payment-type flows, the MCP client would need to perform PCI-compliant payment processing, which is not a desired requirement for MCP clients.

Why doesn't the server block (wait) on the elicitation to complete?

URL mode elicitation requests are asynchronous or "disconnected" flows by design, because the kinds of interactions they enable are inherently asynchronous. Payment flows, external authorization, etc. can take minutes or more to complete, and in some cases never complete at all (if abandoned by the end-user).

Why disallow URLs in form mode?

Being very explicit about when URLs can (and cannot) be sent in an elicitation request improves the client's security posture. By clearly stating in the spec that URLs are only allowed in the url field of a URL mode elicitation request, client implementers can implement UX patterns that are consistent with the security model. For example, a client could refuse to render a URL as a clickable hyperlink in a form mode elicitation request, reducing the likelihood of a user clicking on a malicious URL sent by a malicious server.

Alternative Approaches Considered

  1. Token Passthrough: Simply passing the MCP client's token to external services was rejected due to security concerns documented in the Security Best Practices. Having the MCP client obtain additional tokens and passing those to the MCP server was rejected for the same reason.

  2. OAuth-specific Capability: Creating a capability specific to external (3rd-party) authorization with OAuth was considered, but rejected in favor of the more general URL mode elicitation approach that supports multiple use cases.

Community Feedback

This proposal incorporates extensive community feedback from discussions in #475, #234, and #284, as well as the #auth-wg working group on Discord. The community identified the need for:

  • Secure credential collection without client exposure
  • External authorization patterns separate from MCP authorization
  • Payment and subscription flow support
  • Clear security boundaries and trust models

Backward Compatibility

This SEP introduces the following breaking changes:

  1. Capability Declaration: Clients must now specify which elicitation modes they support:

    {
      "capabilities": {
        "elicitation": {
          "form": {},
          "url": {}
        }
      }
    }
    

    Previously, clients only declared "elicitation": {} without mode specification.

  2. Mode Parameter: All elicitation/create requests must now include a mode parameter ("form" or "url").

Migration Path

To ease migration:

  • Servers SHOULD check client capabilities before sending mode-specific requests
  • Clients MAY initially support only form mode to maintain compatibility
  • Existing form elicitation implementations continue to work with the addition of the mode parameter

Reference Implementation

Client/server implementation in TypeScript: feat/url-elicitation

Explainer video: https://drive.google.com/file/d/1llCFS9wmkK_RUgi5B-zHfUUgy-CNb0n0/view?usp=sharing

Security Implications

This SEP introduces several security considerations:

URL Security Requirements

  1. SSRF Prevention: Clients must validate URLs to prevent Server-Side Request Forgery attacks
  2. Protocol Restrictions: Only HTTPS URLs are allowed for URL elicitation
  3. Domain Validation: Clients must clearly display target domains to users

Trust Boundaries

URL elicitation explicitly creates clear trust boundaries:

  • The MCP client never sees sensitive data obtained by the MCP server via URL elicitation
  • The MCP server must independently verify user identity
  • Third-party services interact directly with users through secure browser contexts

Identity Verification

Servers must verify that the user completing a URL elicitation is the same user who initiated the request. Verifying the identity of the user must not rely on untrusted input (e.g. user input) from the client.

Implementation Requirements

  1. Clients must:

    • Use secure browser contexts that prevent inspection of user inputs
    • Validate URLs for SSRF protection
    • Obtain explicit user consent before opening URLs
    • Clearly display target domains
  2. Servers must:

    • Bind elicitation state to authenticated user sessions
    • Verify user identity at the beginning and end of a URL elicitation flow
    • Implement appropriate rate limiting
  3. Both parties should:

    • Log security events for audit purposes
    • Implement timeout mechanisms for elicitation requests
    • Provide clear error messages for security failures

Relationship to Existing Security Measures

This proposal builds upon and complements existing MCP security measures:

  • Works within the existing MCP authorization framework (MCP authorization is not affected by this proposal)
  • Follows Security Best Practices regarding token handling
  • Maintains separation of concerns between client-server and server-third-party authorization

SEP-1046 Support OAuth client credentials flow in authorization

Final · Standards Track · Created 2025-07-23

  • Status: Final
  • Type: Standards Track
  • Created: 2025-07-23
  • Author(s): Darin McAdams (@D-McAdams )
  • Issue: #1046

Preamble

Title: Support OAuth client credentials flow in authorization Author: Darin McAdams (@D-McAdams ) Status: Proposal Type: Standards Track Created: 2025-07-23

Abstract

Recommends adding the OAuth client credentials flow to the authorization spec to enable machine-to-machine scenarios.

Motivation

The original authorization spec mentioned the client credentials flow, but it was dropped in subsequent revisions. Therefore, the spec is currently silent on how to solve machine-to-machine scenarios where an end-user is unavailable for interactive authorization.

Specification

The authorization spec would be amended to list the OAuth client credentials flow as being allowed. Adhering to the patterns established by OAuth 2.1, the specification would RECOMMEND the use of asymmetric methods defined in RFC 753 (JWT Assertions), but also allow client secrets.

As guidance to implementors, the spec overview would also be updated to describe the different flows and when each is applicable. In addition, to address a common question, the spec would be updated to indicate that implementors may implement other authorization scenarios beyond what's defined; emphasizing that the specification defines the baseline requirements.

Rationale

To maximize interoperability (and minimize SDK complexity), this change would intentionally constrain the client credentials flow to two options:

  1. JWT Assertions as per RFC 7523 (RECOMMENDED)
  2. Client Secrets via HTTP Basic authentication (Allowed for maximum compatibility with existing systems)

Other options, such as mTLS, are not included.

While the spec encourages the use of RFC 7523 (JWT Assertions), it does not yet specify how to populate the JWT contents nor how to discover the client's JWKS URI to validate the JWT. In future iterations of the spec, it will be beneficial to do so. However, this was currently left unspecified pending maturity of other RFCs that can define these profiles. The other RFCs include WIMSE Headless JWT Authentication (for specifying JWT contents) and Client ID Metadata (for specifying the JWKS URI). This revision intentionally leaves extensibility for these future profiles. As a practical matter, this means implementers needing to ship solutions ASAP will most likely use client secrets which are widely supported today, whereas the JWT Assertion pattern represents the longer-term direction.

Backward Compatibility

This change is fully backward compatible. It introduces a new authorization flow, but does not alter the existing flows.

Security Implications

The specification refers to the existing OAuth security guidance.

SEP-1302 Formalize Working Groups and Interest Groups in MCP Governance

Final · Standards Track · Created 2025-08-05

  • Status: Final
  • Type: Standards Track
  • Created: 2025-08-05
  • Author(s): tadasant
  • Issue: #1302

PR: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1350

Abstract

A short (~200 word) description of the technical issue being addressed.

In SEP-994, we introduced a notion of “Working Groups” and “Interest Groups” that facilitate MCP sub-communities for discussion and collaboration. This SEP aims to formally define those two terms: what they are meant to achieve, how groups can be created, how they are governed, and how they can be retired.

Interest Groups work to define problems that MCP should solve by facilitating discussions, while Working Groups push forward specific solutions by collaboratively producing deliverables (in the form of SEPs or community-owned implementations of the specification). Interest Group input is a welcome (but not required) justification for creation of a Working Group. Interest Group or Working Group input is collectively a welcome (but not required) input into a SEP.

Motivation

The motivation should clearly explain why the existing protocol specification is inadequate to address the problem that the SEP solves.

The community has already been self-organizing into several disparate systems for these collaborative groups:

  • The Steering group has had a long-standing practice of managing a handful of collaborative groups through Discord channels (e.g. security, auth, agents). See bottom of MAINTAINERS.md.
  • The “CWG Discord” has had a semi-formal process for pushing equivalent grassroots initiatives, mostly in pursuit of creating artifacts for SEP consideration (e.g. hosting, UI, tool-interfaces, search-tools)

With SEP-994 resulting in the merging of the Discord communities, we have a need to:

  • Merge the existing initiatives into one unified approach, so when we reference “working group” or “interest group”, everyone knows what that means and what kind of weight the reference might carry
  • Standardize a process around the creation (and eventual retirement) of such groups
  • Properly distinguish between “working” and “interest” groups; the CWG experience has shown two very different motivations for starting a group worth treating with different expectations and lifecycle. Put succinctly, “interest” groups are about brainstorming possible problems, and “working” groups are about pushing forward specific solutions.

These groups exist to:

  • Facilitate high signal spaces for discussion such that those opting into notifications and meetings feel most content is relevant to them and they can meaningfully contribute their experience and learn from others
  • Create norms, expectations, and single points of involved leadership around making collaborative progress towards concrete deliverables that help evolve MCP

It will also form the foundation for cross-group initiatives, such as maintaining a calendar of live meetings.

Specification

The technical specification should describe the syntax and semantics of any new protocol feature. The specification should be detailed enough to allow competing, interoperable implementations. A PR with the changes to the specification should be provided.

Interest Groups (IG) [Problems]

Goal: facilitate discussion and knowledge-sharing among MCP community members with similar interests surrounding some MCP sub-topic or context. The focus is on collecting problems that may or may not be worth solving with SEPs or other community artifacts.

Expectations:

  • At least one substantive thread / conversation per month
  • AND/OR a live meeting attended by 3+ unaffiliated individuals

Examples:

  • Security in MCP (currently: #security)
  • Auth in MCP (currently: #auth)
  • Using MCP in an internal enterprise setting (currently: #enterprise-wg)
  • Tooling and practices surrounding hosting MCP servers (currently: #hosting-wg)
  • Tooling and practices surrounding implementing MCP clients (currently: #client-implementors)

Lifecycle:

  • Creation begins by filling out a template in #wg-ig-group-creation Discord channel
  • A community moderator will review and call for a vote in the (private) #community-moderators Discord channel. Majority positive vote by members over a 72h period approves creation of the group. Can be reversed at any time (e.g. after more input comes in). Core and lead maintainers can veto.
  • Facilitator(s) and Maintainer(s) responsible for organizing IG into meeting expectations
    • Facilitator is an informal role responsible for shepherding or speaking for a group
    • Maintainer is an official representative from the MCP steering group (not required for every group to have this)
  • IG is retired only when community moderators or core+ maintainers decide it is not meeting expectations
    • This means successful IG’s will live on in perpetuity

Creation Template:

  • Facilitator(s)
  • Maintainer(s) (optional)
  • Flag potential overlap with other IG’s
  • How this IG differentiates itself from the related IG’s
  • First topic you want to discuss

There is no requirement to be part of an IG to start a WG, or even to start a SEP. However, forming consensus in IG’s to support justifying the creation of a WG is often a good idea. Similarly, citing IG or WG support of a SEP helps the SEP as well.

Working Groups (WG) [Solutions]

Goal: facilitate MCP community collaboration on a specific SEP, themed series of SEPs, or officially endorsed Project.

Expectations:

  • Minimum monthly progress towards at least one SEP or spec-related implementation OR holds maintenance responsibilities for a Project
  • Facilitator(s) is/are responsible for fielding status update requests by community moderators or maintainers

Examples:

  • Registry
  • Inspector
  • Tool Filtering
  • Server Identity

Lifecycle:

  • Creation begins by filling out a template in #wg-ig-group-creation Discord channel
  • A community moderator will review and call for a vote in the (private) #community-moderators Discord channel. Majority positive vote by members over a 72h period approves creation of the group. Can be reversed at any time (e.g. after more input comes in). Core and lead maintainers can veto.
  • Facilitator(s) and Maintainer(s) responsible for organizing WG into meeting expectations
    • Facilitator is an informal role responsible for shepherding or speaking for a group
    • Maintainer is an official representative from the MCP steering group (not required for every group to have this)
  • WG is retired when either:
    • Community moderators or core+ maintainers decide it is not meeting expectations
    • The WG does not have a WIP Issue/PR for at least a month, or has completed all Issues/PRs it intends to pursue.

Creation Template:

  • Facilitator(s)
  • Maintainer(s) (optional)
  • Explanation of interest/use cases (ideally from an IG but can come from anywhere)
  • First Issue/PR/SEP you intend to procure

WG/IG Facilitators

A “Facilitator” role in a WG or IG does not result in a maintainership role across the MCP organization. It is an informal role into which anyone can self-nominate, responsible for helping shepherd discussions and collaboration within the group.

Core Maintainers reserve the right to modify the list of Facilitators and Maintainers for any WG/IG at any time.

PR for the changes to our documentation we'd want to enact this SEP: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1350

Rationale

The rationale explains why particular design decisions were made. It should describe alternate designs that were considered and related work. The rationale should provide evidence of consensus within the community and discuss important objections or concerns raised during discussion.

The design above comes from experience in facilitating the creation of + observing the behavior of informal “Community Working Groups” in the CWG Discord, and leading one of / participating in / observing the “Steering Committee Working Groups”. While the Steering WG’s were usually informally created by Lead Maintainers, the CWG Discord had a lightweight WG-creation process that involved similar steps to the proposal above (community members would propose WG’s in #working-group-ideation, and moderators would create channels from that collaboration).

As precedent, the WG and IG concepts here are similar to W3C’s notion of Working Groups and Interest Groups.

Considerations

In proposing the WG/IG design, we took the following into consideration:

Clear on-ramp for community involvement

A very common question for folks looking to invest in the MCP ecosystem is, "how do I get involved?"

These IG and WG abstractions help provide an elegant on-ramp:

  1. Join the Discord, follow the conversation in IGs relevant to you. Attend live calls. Participate.
  2. Offer to facilitate calls. Contribute your use cases in SEP proposals and other work.
  3. When you're comfortable contributing to deliverables, jump in to contribute to WG work.
  4. Do this for a period of time, get noticed by WG maintainers to get nominated as a new maintainer.
Minimal changes to existing governance structure

We did not want this change to introduce new elections, appointments, or other notions of leadership. We leverage community moderators to thumbs-up creation of new groups, allow core maintainers to veto, maintainership status stays unchanged, and the notion of "facilitator" is new but self-nominated, so does not introduce any new governance processes.

Alignment with current status quo

There is a clear "migration" path for the existing "CWG" working groups and Steering working groups - just a matter of sorting out what is "working" vs. "interest", but functionally this proposal stays out of the way of changing anything that has been working within each group's existing structure.

Nature of requests for gathering spaces

It has been clear from the requests to CWG that some groups form with a motivation to collaborate on some deliverable (e.g. search-tools), and others form due to common interests and a want for sub-community but not yet specific deliverables (e.g. enterprise). Hence, we separate the motivations into Working Groups vs. Interest Groups.

Potential for overlap in scope

In the requests for new group spaces, it is sometimes non-obvious why a new one needs to exist. For example, the stated motivation for enterprise at times sounded like it may just be another flavor of hosting. We ultimately settled on a distinction that made it clear one was not a direct subset of the other, but the concern of making clear boundaries between groups (and letting community moderators / maintainers centralize the decision-making around "what are the right layers of abstraction") is what led to the questions in the creation templates around e.g. "flag potential overlap with other IG’s".

Path to retiring stale groups

Many working groups in the old CWG and Steering models have gone stale since creation. They serve no real purpose and should be retired. For this, we introduce the formal concept of facilitators and optional maintainers in groups; and the community moderator right to retire them. By having at least informal leadership in place per group, a moderator can easily make the decision to retire a group if everyone is in agreement to proceed.

Alternatives Considered

Hierarchy between IGs and WGs

We considered requiring that WGs be owned or spawned by a "sponsor" IG, for the purpose of more clearly exhibiting a progression of ideas to the community; but decided against this requiring to avoid adding a new layer of governance and alignment with how the less formal groups works today.

A single WG concept (instead of both WG and IG)

There has been regular tension in both CWG and the Steering group around the question of "is XYZ really a working group? how will maintainership work?" By making IG's explicitly discussion-oriented and maintainership involvement optional, we create a space to drive those discussions without requiring some formal expectation of deliverables like we might in a well-defined WG.

Free-for-all WG/IG creation process

While very community-driven, the concern of group overlap would quickly fragment the conversations and collaboration to an untenable level; we need a centralized point of discernment here.

Backward Compatibility

All SEPs that introduce backward incompatibilities must include a section describing these incompatibilities and their severity. The SEP must explain how the author proposes to deal with these incompatibilities.

There is no major change suggested in the day to day of existing groups - the expectations laid out of IGs and WGs are easily met by existing active groups as long as they keep doing as they are doing.

A migration path for all groups is laid out below.

Reference Implementation

The reference implementation must be completed before any SEP is given status “Final”, but it need not be completed before the SEP is accepted. While there is merit to the approach of reaching consensus on the specification and rationale before writing code, the principle of “rough consensus and running code” is still useful when it comes to resolving many discussions of protocol details.

The below is the suggested migration path for each group. "Migration" just involves acknowledgement of this SEP and the expectations of each group, plus methodology for possible eventual retirement (or immediate retirement, in some cases).

After this SEP is approved, we can ping each of the groups to confirm they are on board with the migration plan.

Steering Working Groups

  • All official SDK groups --> Working Groups
  • Registry --> Working Group
  • Documentation --> Working Group
  • Inspector --> Working Group
  • Auth --> Interest Group + some WGs: client-registration, improve-devx, profiles, tool-scopes
  • Agents --> Working Group [Long Running / Async Tool Calls; unless we want an Agents IG on top of that?]
  • Connection Lifetime --> Retire
  • Streaming --> Retire
  • Spec Compliance --> Retire (good idea but stale; would be good for someone to spearhead a new Working Group)
  • Security --> Interest Group (perhaps with Security Best Practices WG?)
  • Transports --> Interest Group
  • Server Identity --> Working Group
  • Governance --> Working Group (or Retire if no more work here?)

Community Working Groups

  • agent-comms --> Retire
  • enterprise --> Interest Group (request a proposal to start)
  • hosting --> Interest Group (request a proposal to start)
  • load-balancing --> Retire
  • model-awareness --> Working Group (request a proposal to start)
  • search-tools (tool-filtering) --> Working Group
  • server-identity --> merge with Steering equivalent
  • security --> merge with Steering equivalent
  • server-identity --> merge with Steering equivalent
  • tool-interfaces --> Retire
  • ui --> Interest Group
  • schema-validation --> Retire (same as Steering equivalent)

SEP-1303 Input Validation Errors as Tool Execution Errors

Final · Standards Track · Created 2025-08-05

  • Status: Final
  • Type: Standards Track
  • Created: 2025-08-05
  • Author(s): @fredericbarthelet
  • Issue: #1303

Abstract

This SEP proposes treating tools input validation errors as Tool Execution Errors rather than Protocol Errors. This change would enable language models to receive validation error feedback in their context window, allowing them to self-correct and successfully complete tasks without human intervention, significantly improving task completion rate.

Motivation

Language models can learn from tool input validation error messages and retry a tools/call with corrected parameters accordingly, but only if they receive the error feedback in their context window. Protocol Errors are catch at the application level by the MCP Client. Only Tool Execution Errors are forwarded back to the model as JSON-RPC responses. With the current specifications, models cannot see these error messages and thus cannot self-correct, leading to repeated failures and poor user experiences.

Problem Statement

Consider a flight booking tool that validates departure dates using the following zod validation schema:

departureDate: z.string()
  .regex(/^\d{2}\/\d{2}\/\d{4}$/, "date must be in dd/mm/yyyy format")
  .superRefine((dateStr, ctx) => {
    const date = parseDateFr(dateStr);
    if (date.getTime() < Date.now()) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message:
          "Dates must be in the future. Current date is " +
          formatDateFr(new Date()),
      });
    }
    return true;
  })
  .describe("Departure date in dd/mm/yyyy format");

Tool expected input JSON schema can only describe the regex statement. The actual programmatic check that the date is in the past cannot be expressed here as JSON schema. Even when a model provides a syntactically correct date that passes JSON schema validation, there is no guarantee it will be in the future. When a validation error is raised and returned as a Protocol Error:

  1. The model doesn't receive the error message explaining why the date was rejected
  2. The model repeats the same mistake multiple times (e.g., Cursor typically consistently sends dates in 2024 when the user only specify day and month or relative date and repeats the same tools/call request 3 times without getting any information as to why the tools call fails)
  3. The task fails despite the model being capable of correcting itself if given proper feedback
  4. Users experience frustration and must manually intervene

Benefits of This Proposal

  1. Higher Task Completion Rates: Models can self-correct validation errors without human intervention
  2. Better User Experience: Reduced failures and faster task completion
  3. Leverages Model Capabilities: Modern LLMs excel at understanding and responding to error messages
  4. Reduced API Calls: Fewer retry attempts as models correct themselves on the first error

Specification

Current Behavior

The tool errors specification currently provides ambiguous guidance:

  • "Invalid arguments" should be treated as Protocol Error
  • "Invalid input data" should be treated as Tool Execution Error

This ambiguity leads to inconsistent implementations where valuable error feedback is lost.

Proposed Change

Clarify the specification with the following changes:

  1. Removes the "invalid argument" category from Protocol Errors.
  2. Tool Execution Errors should be used for all tool argument validation failures (merging invalid argument and invalid input data under a new input validation errors category)

Specification Text Changes

Update the error handling section to include:

## Error Handling

Tools use two error reporting mechanisms:

1. **Protocol Errors**: Standard JSON-RPC errors for issues like:

   - Unknown tools
   - Server errors

2. **Tool Execution Errors**: Reported in tool results with `isError: true`:
   - API failures
   - Input validation errors
   - Business logic errors

Implementation

Before (Protocol Error)

// Model submits past date
request: {
  ...
  method: "tools/call",
  params: {
    name: "book_flight",
    arguments: {
      departureDate: "12/12/2024"  // Past date
    }
  }
}

// Server returns Protocol Error
response: {
  ...
  error: {
    code: -32602,
    message: "Invalid params"
  }
}

// Model retries blindly with another past date
// This cycle repeats until failure

After (Tool Execution Error)

// Model submits past date
request: {
  ...
  method: "tools/call",
  params: {
    name: "book_flight",
    arguments: {
      departureDate: "12/12/2024"  // Past date
    }
  }
}

// Server returns Tool Execution Error (visible to model)
response: {
  ...
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Dates must be in the future. Current date is 08/08/2025"
      }
    ],
    "isError": true
  }
}

// Model understands the error and corrects itself
request: {
  method: "tools/call",
  params: {
    name: "book_flight",
    arguments: {
      departureDate: "12/12/2025"  // Future date
    }
  }
}

Backwards Compatibility

This change is backwards compatible as it:

  • Does not alter the protocol structure
  • Only clarifies existing ambiguous behavior
  • Maintains all existing error types and formats
  • Improves behavior without breaking existing implementations

Servers implementing the clarified behavior will provide better model self-recovery while continuing to work with all existing clients.

References

SEP-1319 Decouple Request Payload from RPC Methods Definition

Final · Standards Track · Created 2025-08-08

  • Status: Final
  • Type: Standards Track
  • Created: 2025-08-08
  • Author(s): @kurtisvg
  • Issue: #1319

Abstract

This SEP proposes a structural refactoring of the Model Context Protocol (MCP) specification. The core change is to define payload of requests (e.g., CallToolRequest) as independent definitions and have the RPC method definitions refer to these models. This decouples the definition of the data payload from the definition of the remote procedure that transports it, leading to a clearer, more modular, and more maintainable specification.

Motivation

The current MCP specification tightly couples the data payload of a request with the JSON-RPC method that transports it. This design presents several challenges:

  • Reduced Clarity: It forces developers to mentally parse the JSON-RPC transport structure just to understand the core data being exchanged. This increases cognitive load and makes the specification difficult to read and implement correctly.
  • Hindered Maintainability: Defining data structures inline prevents their reuse across different methods, leading to redundancy and making future updates to the protocol more complex and error-prone.
  • Tightly Coupled to JSON-RPC: Most critically, this tight coupling to JSON-RPC is the primary blocker for defining bindings for other transport protocols. To support transports like gRPC (which is currently a popular ask from the community), a transport-agnostic definition of its request and response messages. The current structure makes this practically impossible.

By refactoring the specification to separate the data model (the "what") from the RPC method (the "how"), this proposal will create a clearer, more modular specification. This change will immediately improve the developer experience and, most importantly, pave the way for the future evolution of MCP across multiple transports.

Specification

The proposal introduces the following principle: All data structures used as parameters (params) or results (result) for RPC methods should be defined as standalone, named schemas. The RPC method definitions will then use references to these schemas.

Current Approach (Inline Definition):

The RPC method definition contains the full structure of its parameters and results.

export interface CallToolRequest extends Request {
  method: "tools/call";
  params: {
    name: string;
    arguments?: { [key: string]: unknown };
  };
}

Proposed Approach (Decoupled Definition):

First, the data models for the request and response are defined as top-level schemas.

/**
 * Parameters for a `tools/call` request.
 *
 * @category tools/call
 */
export interface CallToolRequestParams extends RequestParams {
  name: string;
  arguments?: { [key: string]: unknown };
}

Then, the RPC method definition becomes much simpler, merely referring to these models.

export interface CallToolRequest extends Request {
  method: "tools/call";
  params: CallToolRequestParams;
}

Rationale

The proposed solution—separating payload definitions from the RPC method—was chosen as the most direct and non-disruptive path to achieving the goals outlined in the motivation.

This approach establishes a clear architectural boundary between two distinct concerns:

  1. The Data Layer: The transport-agnostic payload definition (e.g., CallToolRequestParams), which represents the core information being exchanged.
  2. The Transport Layer: The protocol-specific wrapper (e.g., the JSON-RPC CallToolRequest object), which describes how the data is sent.

This architectural separation is superior to maintaining separate, parallel specifications for each transport (e.g., one for JSON-RPC, another for gRPC), which would introduce significant maintenance overhead and risk inconsistencies.

Crucially, this design refactors the specification document itself but intentionally leaves the on-the-wire format unchanged. This makes the proposal fully backward-compatible, requiring no changes from existing, compliant clients and servers. In short, this change is a strategic, foundational improvement that enables future growth without penalizing the current ecosystem.

Backward Compatibility

This proposal is a non-breaking change for existing implementations. It is a refactoring of the specification document itself and does not alter the on-the-wire JSON format of the protocol messages. A client or server that is compliant with the old specification structure will remain compliant with the new one, as the resulting JSON payloads are identical.

The primary impact is on developers who read the specification and on tools that parse the specification to generate code or documentation.

SEP-1330 Elicitation Enum Schema Improvements and Standards Compliance

Final · Standards Track · Created 2025-08-11

  • Status: Final
  • Type: Standards Track
  • Created: 2025-08-11
  • Author(s): chughtapan
  • Issue: #1330

Abstract

This SEP proposes improvements to enum schema definitions in MCP, deprecating the non-standard enumNames property in favor of JSON Schema-compliant patterns, and introducing additional support for multi-select enum schemas in addition to single choice schemas. The new schemas have been validated against the JSON specification.

Schema Changes: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1148 Typescript SDK Changes: https://github.com/modelcontextprotocol/typescript-sdk/pull/1077 Python SDK Changes: https://github.com/modelcontextprotocol/python-sdk/pull/1246 Client Implementation: https://github.com/evalstate/fast-agent/pull/324/files Working Demo: https://asciinema.org/a/anBvJdqEmTjw0JkKYOooQa5Ta

Motivation

The existing schema for enums uses a non-standard approach to adding titles to enumerated values. It also limits use of enums in Elicitation (and any other schema object that should adopt EnumSchema in the future) to a single selection model. It is a common pattern to ask the user to select multiple entries. In the UI, this amounts to the difference between using checkboxes or radio buttons.

For these reasons, we propose the following non-breaking minor improvements to the EnumSchema for improving user and developer experience.

  • Keep the existing EnumSchema as "Legacy"
    • It uses a non-standard approach for adding titles to enumerated values
    • Mark it as Legacy but still support it for now.
    • As per @dsp-ant When we have a proper deprecation strategy, we'll mark it deprecated
  • Introduce the distinction between Untitled and Titled enums.
    • If the enumerated values are sufficient, no separate title need be specified for each value.
    • If the enumerated values are not optimal for display, a title may be specified for each value.
  • Introduce the distinction between Single and Multi-select enums.
    • If only one value can be selected, a Single select schema can be used
    • If more than one value can be selected, a Multi-select schema can be used
  • In ElicitResponse, add array as an additionalProperty type
    • Allows multiple selection of enumerated values to be returned to the server

Specification

1. Mark Current EnumSchema with Non-Standard enumNames Property as "Legacy"

The current MCP specification uses a non-standard enumNames property for providing display names for enum values. We propose to mark enumNames property as legacy, suggest using TitledSingleSelectEnum, a standards compliant enum type we define below.

// Continue to support the current EnumSchema as Legacy

/**
 * Legacy: Use TitledSingleSelectEnumSchema instead.
 * This interface will be removed in a future version.
 */
export interface LegacyEnumSchema {
  type: "string";
  title?: string;
  description?: string;
  enum: string[];
  enumNames?: string[]; // Titles for enum values (non-standard, legacy)
}

2. Define Single Selection Enums (with Titled and Untitled varieties)

Enums may or may not need titles. The enumerated values may be human readable and fine for display. In which case an untitled implementation using the JSON Schema keyword enum is simpler. Adding titles requires the enum array to be replaced with an array of objects using const and title.

// Single select enum without titles
export type UntitledSingleSelectEnumSchema = {
  type: "string";
  title?: string;
  description?: string;
  enum: string[]; // Plain enum without titles
};

// Single select enum with titles
export type TitledSingleSelectEnumSchema = {
  type: "string";
  title?: string;
  description?: string;
  oneOf: Array<{
    const: string; // Enum value
    title: string; // Display name for enum value
  }>;
};

// Combined single selection enumeration
export type SingleSelectEnumSchema =
  UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema;

3. Introduce Multiple Selection Enums (with Titled and Untitled varieties)

While elicitation does not support arbitrary JSON types like arrays and objects so clients can display the selection choice easily, multiple selection enumerations can be easily implemented.

// Multiple select enums without titles
export type UntitledMultiSelectEnumSchema = {
  type: "array";
  title?: string;
  description?: string;
  minItems?: number; // Minimum number of items to choose
  maxItems?: number; // Maximum number of items to choose
  items: {
    type: "string";
    enum: string[]; // Plain enum without titles
  };
};

// Multiple select enums with titles
export type TitledMultiSelectEnumSchema = {
  type: "array";
  title?: string;
  description?: string;
  minItems?: number; // Minimum number of items to choose
  maxItems?: number; // Maximum number of items to choose
  items: {
    oneOf: Array<{
      const: string; // Enum value
      title: string; // Display name for enum value
    }>;
  };
};

// Combined Multiple select enumeration
export type MultiSelectEnumSchema =
  UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema;

4. Combine All Varieties as EnumSchema

The final EnumSchema rolls up the legacy, multi-select, and single-select schemas as one, defined as:

// Combined legacy, multiple, and single select enumeration
export type EnumSchema =
  SingleSelectEnumSchema | MultiSelectEnumSchema | LegacyEnumSchema;

5. Extend ElicitResult

The current elicitation result schema only allows returning primitive types. We extend this to include string arrays for MultiSelectEnums:

export interface ElicitResult extends Result {
  action: "accept" | "decline" | "cancel";
  content?: { [key: string]: string | number | boolean | string[] }; // string[] is new
}

Instance Schema Examples

Single-Select Without Titles (No change)

{
  "type": "string",
  "title": "Color Selection",
  "description": "Choose your favorite color",
  "enum": ["Red", "Green", "Blue"],
  "default": "Green"
}

Legacy Single Select With Titles

{
  "type": "string",
  "title": "Color Selection",
  "description": "Choose your favorite color",
  "enum": ["#FF0000", "#00FF00", "#0000FF"],
  "enumNames": ["Red", "Green", "Blue"],
  "default": "#00FF00"
}

Single-Select with Titles

{
  "type": "string",
  "title": "Color Selection",
  "description": "Choose your favorite color",
  "oneOf": [
    { "const": "#FF0000", "title": "Red" },
    { "const": "#00FF00", "title": "Green" },
    { "const": "#0000FF", "title": "Blue" }
  ],
  "default": "#00FF00"
}

Multi-Select Without Titles

{
  "type": "array",
  "title": "Color Selection",
  "description": "Choose your favorite colors",
  "minItems": 1,
  "maxItems": 3,
  "items": {
    "type": "string",
    "enum": ["Red", "Green", "Blue"]
  },
  "default": ["Green"]
}

Multi-Select with Titles

{
  "type": "array",
  "title": "Color Selection",
  "description": "Choose your favorite colors",
  "minItems": 1,
  "maxItems": 3,
  "items": {
    "anyOf": [
      { "const": "#FF0000", "title": "Red" },
      { "const": "#00FF00", "title": "Green" },
      { "const": "#0000FF", "title": "Blue" }
    ]
  },
  "default": ["Green"]
}

Rationale

  1. Standards Compliance: Aligns with official JSON Schema specification. Standard patterns work with existing JSON Schema validators
  2. Flexibility: Supports both plain enums and enums with display names for single and multiple choice enums.
  3. Client Implementation: shows that the additional overhead of implementing a group of checkboxes v/s a single checkbox is minimal: https://github.com/evalstate/fast-agent/pull/324/files

Backwards Compatibility

The LegacyEnumSchema type maintains backwards compatible during the migration period. Existing implementations using enumNames will continue to work until a protocol-wide deprecation strategy is implemented, and this schema is removed.

Reference Implementation

Schema Changes: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1148 Typescript SDK Changes: https://github.com/modelcontextprotocol/typescript-sdk/pull/1077 Python SDK Changes: https://github.com/modelcontextprotocol/python-sdk/pull/1246 Client Implementation: https://github.com/evalstate/fast-agent/pull/324/files Working Demo: https://asciinema.org/a/anBvJdqEmTjw0JkKYOooQa5Ta

Security Considerations

No security implications identified. This change is purely about schema structure and standards compliance.

Appendix

Validations

Using stored validations in the JSON Schema Validator at https://www.jsonschemavalidator.net/ we validate:

  • All of the example instance schemas from this document against the proposed JSON meta-schema EnumSchema in the next section.
  • Valid and invalid values against the example instance schemas from this document.
Legacy Single Selection
Single Selection
Multiple Selection

JSON meta-schema

This is our proposal for the replacement of the current EnumSchema in the specification’s schema.json.

{
  "$schema": "https://json-schema.org/draft-07/schema",
  "definitions": {
    // New Definitions Follow
    "UntitledSingleSelectEnumSchema": {
      "type": "object",
      "properties": {
        "type": { "const": "string" },
        "title": { "type": "string" },
        "description": { "type": "string" },
        "enum": {
          "type": "array",
          "items": { "type": "string" },
          "minItems": 1
        }
      },
      "required": ["type", "enum"],
      "additionalProperties": false
    },

    "UntitledMultiSelectEnumSchema": {
      "type": "object",
      "properties": {
        "type": { "const": "array" },
        "title": { "type": "string" },
        "description": { "type": "string" },
        "minItems": {
          "type": "number",
          "minimum": 0
        },
        "maxItems": {
          "type": "number",
          "minimum": 0
        },
        "items": {
          "type": "object",
          "properties": {
            "type": { "const": "string" },
            "enum": {
              "type": "array",
              "items": { "type": "string" },
              "minItems": 1
            }
          },
          "required": ["type", "enum"],
          "additionalProperties": false
        }
      },
      "required": ["type", "items"],
      "additionalProperties": false
    },

    "TitledSingleSelectEnumSchema": {
      "type": "object",
      "required": ["type", "anyOf"],
      "properties": {
        "type": { "const": "string" },
        "title": { "type": "string" },
        "description": { "type": "string" },
        "anyOf": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["const", "title"],
            "properties": {
              "const": { "type": "string" },
              "title": { "type": "string" }
            },
            "additionalProperties": false
          }
        }
      },
      "additionalProperties": false
    },

    "TitledMultiSelectEnumSchema": {
      "type": "object",
      "required": ["type", "anyOf"],
      "properties": {
        "type": { "const": "array" },
        "title": { "type": "string" },
        "description": { "type": "string" },
        "anyOf": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["const", "title"],
            "properties": {
              "const": { "type": "string" },
              "title": { "type": "string" }
            },
            "additionalProperties": false
          }
        }
      },
      "additionalProperties": false
    },

    "LegacyEnumSchema": {
      "properties": {
        "type": {
          "type": "string",
          "const": "string"
        },
        "title": { "type": "string" },
        "description": { "type": "string" },
        "enum": {
          "type": "array",
          "items": { "type": "string" }
        },
        "enumNames": {
          "type": "array",
          "items": { "type": "string" }
        }
      },
      "required": ["enum", "type"],
      "type": "object"
    },

    "EnumSchema": {
      "oneOf": [
        { "$ref": "#/definitions/UntitledSingleSelectEnumSchema" },
        { "$ref": "#/definitions/UntitledMultiSelectEnumSchema" },
        { "$ref": "#/definitions/TitledSingleSelectEnumSchema" },
        { "$ref": "#/definitions/TitledMultiSelectEnumSchema" },
        { "$ref": "#/definitions/LegacyEnumSchema" }
      ]
    }
  }
}

SEP-1577 Sampling With Tools

Final · Standards Track · Created 2025-09-30

  • Status: Final
  • Type: Standards Track
  • Created: 2025-09-30
  • Author(s): Olivier Chafik (@ochafik)
  • Issue: #1577
SEP Number #1577
Title Sampling With Tools
Author Olivier Chafik
Sponsor @bhosmer-ant
Status Draft
Created 2025-09-29
Specification MCP 2025-06-18
Prototype https://github.com/modelcontextprotocol/typescript-sdk/pull/991
PR https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1796
SDKs https://github.com/modelcontextprotocol/python-sdk/pull/1594 https://github.com/modelcontextprotocol/typescript-sdk/pull/1101

Updates:

  • Oct 1: renamed tool_choice -> toolChoice (+ "none" value); removed exotic stopReasons "refusal" & "other"; allowed {CreateMessageResult,SamplingMessage}.content to be single contents or arrays of contents;
  • Oct 6: aligned ToolResultContent on CallToolResult (support image / audio); added "Possible Follow Ups" section.
  • Oct 10: updated reference impl example w/ simple tool registry (unify mcp tools w/ tool loop tools, see comment below) and a "choose your own adventure" game that uses sampling w/ tools + elicitation.
  • Oct 27: aligned ToolResultContent.content on CallToolResult.content (using ContentBlock); added ToolResultContent._meta
  • Nov 5:
    • kept stopReason as open string but w/ redundant explicit enums for visibility
    • removed requirement to throw when includeContext not matching advertised ClientCapabilities.sampling.context
    • mitigates backwards compatibility issue of CreateMessageResult.content being an array of contents OR a single content by saying sampling MUST NOT return an array in earlier spec versions (+ acknowledging SDK updates of code w/ sampling will need small code changes)
  • Nov 7: renamed type ToolCallContent to ToolUseContent (to match its tool_use type & the toolUse stopReason). SEP was approved!
  • Nov 10: removing disable_parallel_tool_use / keeping for a later update as the Gemini API has no way to implement this for now.
  • Nov 11: added extra notes about Gemini API's function calling modes & roles; requiring SamplingMessage w/ tool result contents not be mixed w/ other content types

Abstract

This SEP introduces tools & toolChoice params to sampling/createMessage and soft-deprecates includeContext (fences thisServer & allServers under a capability). This allows MCP servers to run their own agentic loops using the client's tokens (still under the user supervision), and reduces the complexity of client implementations (context support becoming explicitly optional).

Motivation

  • Sampling doesn't support tool calling, although it's a cornerstone of modern agentic behaviour. Without explicit support for it, MCP servers that use Sampling can either try and emulate tool calling w/ complex prompting / custom parsing of the outputs, or are limited to simpler, non-agentic requests. Adding support for tool calling could unlock many novel use cases in the MCP ecosystem.

  • Context inclusion is ambiguously defined (see this doc): it makes it particularly tricky to fully implement sampling, which along with other precautions needed for sampling (unaffected by this SEP) may have contributed to low adoption of the feature in clients (feature was introduced in the MCP Nov 2024 spec).

Please note some related work:

  • MCP Sampling (@jerome3o-anthropic): extremely similar proposal:
    • Add same tools semantics,
    • Deprecate includeContext (doc explains why its semantics are ambiguous)
    • (goes further to suggest explicit context sharing, which is out of scope from this proposal)
  • Allow Prompt/Sampling Messages to contain multiple content blocks. #198
    • In this PR we've made {CreateMessageResult,SamplingMessage}.content to accept a single content or an array of contents. The result.content change is backwards incompatible but is required to support parallel tool calls. The SamplingMessage.content change then makes it much more natural to write a tool loop (see example in reference implementation: toolLoopSampling.ts)

In the "Possible Follow ups" Section below, we give examples of features that were kept out of scope from this SEP but which we took care to make this SEP reasonably compatible with.

Specification

Overview

  • Add traditional tool call support in CreateMessageRequest w/ tools (w/ JSON schemas) & toolChoice params, requiring a server-side tool loop
    • Sampling may now yield ToolCallBlock responses
    • Server needs to call tools by itself
    • Server calls sampling again with ToolResultParamBlock to inject tool results
    • toolChoice.mode can be “auto" | "required" | "none" to allow common structured outputs use case (see below for possible follow up improvements)
    • Fenced by new capability (sampling { tools {} })
  • Fix/update underspecified strings in CreateMessageResult:
    • stopReason: “endTurn" | "stopSequence" | “toolUse" | “maxToken" | string (explicit enums + open string for compat)
    • role: “assistant”
  • Soft-deprecate CreateMessageRequest.params.includeContext != ‘none’ (now fenced by capability)
    • Incentivize context-free sampling implementation

Protocol changes

  • sampling/createMessage
    • MUST throw an error when includeContext is “thisServer” | “allServers” but clientCapabilities.sampling.context is missing
    • MUST throw an error when tool or toolChoice are defined but clientCapabilities.sampling.tools is missing
    • Servers SHOULD avoid [includeContext](https://omcp.tech/specification/2025-06-18/schema#createmessagerequest) != ‘none’as values“thisServer”and“allServers”` may be removed in future spec releases.
    • CreateMessageRequest.messages MUST balance any “assistant” message w/ a ToolUseContent (and id: $id1) w/ a “user” message w/ a ToolResultContent (and tool_result_id: $id1)
      • Note: this is a requirement for Claude API implementation (parallel tool call must all be responded to in one go)
    • SamplingMessage with tool result content blocks MUST NOT contain other content types.

Schema changes

  • ClientCapabilities

    interface ClientCapabilities {
      ...
      sampling?: {
        context?: object; // NEW: Allows CreateMessageRequest.params.includeContext != "none"
        tools?: object;   // NEW: Allows CreateMessageRequest.params.{tools,toolChoice}
      };
    }
    
  • CreateMessageRequest (use existing Tool)

    interface CreateMessageRequest {
      method: “sampling/createMessage”;
      params: {
        ...
        messages: SamplingMessage[]; // Note: type updated, see below
        
        tools?: Tool[] // NEW (existing type)
    
        toolChoice?: ToolChoice // NEW
      };
    }
    
    interface ToolChoice { // NEW
      mode?: “auto” | "required" | "none";
      // disable_parallel_tool_use?: boolean; // Update (Nov 10): removed, see below
    }
    
    • Notes:
      • OpenAI vs. Anthropic API idioms to avoid parallel tool calls:
        • OpenAI: parallel_tool_calls: false (top-level param)
        • Anthropic: tool_choice.disable_parallel_tool_use: true
          • Preferred here as default value if unset is false (e.g. parallel tool calls allowed)
      • OpenAI vs. Anthropic API re/ tool_choice "none" vs. tools:
        • OpenAI: tools: [$Foo], tool_choice: "none" forbids any tool call
          • Preferred behaviour here
        • Anthropic: tools: [$Foo], tool_choice: {mode: "none"} may still call tool Foo
      • Gemini vs. OAI / Anthropic re/ disable_parallel_tool_use:
        • Gemini API has no way to disable parallel tool calls atm (unlike OAI / Anthropic APIs). Removing this flag for now, to be reintroduced when Gemini has any way of supporting it. Otherwise clients would get unexpected multiple tool calls (or alternatively if implemented that way, unexpected failures / costly retry until a single tool call is emitted)
        • Gemini API's Function calling modes have an ANY value that should match the proposed required
  • SamplingMessage:

    /*
      BEFORE:
      
      interface SamplingMessage {
        content: TextContent | ImageContent | AudioContent
        role: Role;
      }
    */
    
    type SamplingMessage = UserMessage | AssistantMessage; // NEW
    
    type AssistantMessageContent =
      TextContent | ImageContent | AudioContent | ToolUseContent;
    type UserMessageContent =
      TextContent | ImageContent | AudioContent | ToolResultContent;
    interface AssistantMessage {
      // NEW
      role: "assistant";
      content: AssistantMessageContent | AssistantMessageContent[];
    }
    
    interface ToolUseContent {
      // NEW
      type: "tool_use";
      name: string;
      id: string;
      input: object;
    }
    
    interface UserMessage {
      // NEW
      role: "user";
      content: UserMessageContent | UserMessageContent[];
    }
    
    interface ToolResultContent {
      // NEW
      _meta?: { [key: string]: unknown };
      type: "tool_result";
      toolUseId: string;
      content: ContentBlock[];
      structuredContent: object;
      isError?: boolean;
    }
    
  • Notes:

    • Differences of role vs. content type when it comes to tool calling between APIs:
      • OpenAI: role: “system" | “user" | “assistant" | “tool" (where tool is for tool results), while tool calls are nested in assistant messages, content is then typically null but some “OpenAI compatible” APIs accept non-null values
        • [
            { role: "user", content: "what is the temperature in london?" },
            {
              role: "assistant",
              content: "Let me use a tool...",
              tool_calls: [
                {
                  id: "call_1",
                  type: "function",
                  function: {
                    name: "get_weather",
                    arguments: '{"location": "London"}',
                  },
                },
              ],
            },
            {
              role: "tool",
              content: '{"temperature": 20, "condition": "sunny"}',
              tool_call_id: "call_1",
            },
          ];
          
      • Claude API: role: “user" | “assistant", tool use and result are passed through specially-typed message content parts:
        • [
            {
              "role": "user",
              "content": [
                {
                  "type": "text",
                  "text": "what is the temperature in london?"
                }
              },
            {
              "role": "assistant",
              "content": [
                {
                  "type": "text",
                  "text": "Let me use a tool..."
                },
                {
                  "type": "tool_use",
                  "id": "call_1",
                  "name": "get_weather",
                  "input": {"location": "London"}
                }
              ]
            },
            {
              "role": "user",
              "content": [
                {
                  "type": "tool_result",
                  "tool_call_id": "call_1",
                  "content": {"temperature": 20, "condition": "sunny"}
                }
              ]
            }
          ]
          
      • Gemini API:
        • function role (similar to OAI's tool role)
        • No tool call id concept (function calling: Gemini requires tool results to be provided in the exact same order as the tool use parts. An implementation could generate the tool call ids and use them to reorder the tool results if needed.
  • CreateMessageResult

    /*
      BEFORE:
    
      interface CreateMessageResult {
        _meta?: { [key: string]: unknown };
        content: TextContent | ImageContent | AudioContent;
        role: Role;
        stopReason?: string;
        [key: string]: unknown;
    }
    */
    interface CreateMessageResult {
      _meta?: { [key: string]: unknown };
    
      content: AssistantMessageContent | AssistantMessageContent[] // UPDATED
    
      role: "assistant"; // UPDATED
    
      stopReason?: “endTurn" | "stopSequence" | “toolUse" | “maxToken" | string // UPDATED
    
      [key: string]: unknown;
    }
    
    • Notes:
      • Backwards compatibility issue: returning CreateMessageResult.content as an array of contents OR a single content is problematic, so we propose:
        • sampling/createMessage MUST NOT return an array in CreateMessageResult.content before spec version Nov 2025.
          • This guarantees wire-level backwards-compatibility
        • Existing code that uses sampling may break w/ new SDK releases as it will need to test content to know if it's an array or a single block, and act accordingly.
        • This seems reasonable(?)
      • CreateMessageResult.stopReason field is currently defined as an open string, and the spec only mentions the endTurn as example value.
      • OpenAI vs. Anthropic API idioms
        • Finish/stop reason
          • OpenAI’s ChatCompletion: finish_reason: “stop” | “length” | “tool_use” (…?)
          • Anthropic: stop_reason: “end_turn” | “max_tokens” | “stop_sequence” | “tool_use” | “pause_turn” | “refusal”

Possible Follow ups

These are out of scope for this SEP, but care was taken not to preclude them, so where appropriate we give examples of how they could be implemented on top of / after this SEP.

Streaming support

See: Streaming tool use results #117

This could be important for some longer-running use cases or when latency is important, but would play better w/ streaming support in MCP tools.

A possible way to implement this would be to use notifications w/ payload, and possibly create a new method sampling/createMessageStreamed. Both should be orthogonal w/ this SEP (but we'd need to create delta types for results, similar to streaming APIs in inference API such as Claude API and OpenAI API).

Cache friendliness updates

Two bits needed here:

  • Introduce cache awareness
    • Implicit caching guidelines phrased as SHOULDs
    • Explicit cache points and TTL semantics as in the Claude API? (incl. beta behaviour for longer caching)
      • Pros: easy to implement for at least 1 implementor (Anthropic)
      • Cons: if hard to implement for others, unlikely to get approval.
    • “Whole prompt” / prompt-prefix cache w/ an explicit key as in the OpenAI API?
      • Pros:
        • simpler for users (no need to think about where the shared prefix stops)
        • implicitly supports updating the cache (maybe even as subtree)
      • Cons: possibly harder to implement / more storage inefficient
  • Introduce allowed_tools feature to enable / disable tools w/o breaking context caching

Allow client to call the server’s tools by itself in an agentic loop

From the server’s perspective, that would remove the need to call tools by itself / inject tool results in follow up sampling calls.

The MCP server would just allowlist its own tools in the sampling request, w/t a dedicated tool definition such as:

{
  type: "server-tool"; // MCP tool from same server.
  name: string;
}

Pros:

  • Safe, limited to that server’s tools.
  • If we propagate the mcp-session-id, can leverage keep any server-side session context / caching

Allow client to call any other MCP servers’ tools by itself in an agentic loop

Although this sounds similar to the previous one (allow only same server’s tools), this option wouldn’t need a protocol change / could be entirely done by the client as an implementation detail of their sampling support.

The end user would allowlist tools from any other MCP server for use in a sampling request, without the server having to ask for anything. The client UI would e.g. display a tool selection UI as part of the sampling approval flow, auto enabling tools from same server by default.

Pros:

  • Technically no spec change needed (if anything, mention this as a freedom clients have)
  • Possibly similar to what CreateMessageRequest.params.includeContext = thisServer / allServers intended semantics may have meant
    • CreateMessageRequest.params.allowImplicitToolCalls = “none” | “thisServer” | “allServers” (assuming we wanted to give the server any control over this)

Cons:

  • Classifier might be needed to avoid High potential for privacy leaks / abuse
    • If user approves Gmail MCP tool usage / delegation by mistake, server gets access to their private emails through sampling

Allow server to list & call clients’ tools (client/server → p2p)

If we say the client can now expose tools that the server can call, it opens a set of possibilities:

  • The client can “forward” other servers’ tools (maybe w/ some namespacing for seamless aggregation)
    • The server can then call these tools as part of its tool loop.
  • Client & Server semantics start to lose weight, we enter a more peer-to-peer, symmetrical relationship
    • Client could also ask a server for sampling, while we’re at it
    • Symmetry at the protocol layer, but still directionality at the transport layer (e.g. for HTTP transport, direction of POST requests still matters)

Simplify structured outputs use case

A major use case of sampling is to get outputs that conform to a given schema.

This is possible in OpenAI’s API for instance.

The most common workaround is to give a single tool and set tool_choice: "required", which guarantees the output is a ToolCall containing inputs that conform to the tool’s input schema.

While this SEP proposes we enable this "required"-based workaround, as a follow up it would be great to provide more explicit / simpler JSON schema support, which would also allow schema types not allowed in tool inputs (which require an object w/ properties, so one has to pick at least a name for their outputs, which requires thinking / interplay w/ the prompting strategy):

interface CreateMessageRequest {
  method: “sampling/createMessage”;
  params: {
    messages: SamplingMessage[];
    ...
    format: {
      type: "json_schema",
      "schema": {
        "type": "array",
        "minItems": 5,
        "maxItems": 100
      }
    }
  }

SEP-1613 Establish JSON Schema 2020-12 as Default Dialect for MCP

Final · Standards Track · Created 2025-10-06

  • Status: Final
  • Type: Standards Track
  • Created: 2025-10-06
  • Author(s): Ola Hungerford
  • Issue: #1613

Abstract

This SEP establishes JSON Schema 2020-12 as the default dialect for embedded schemas within MCP messages (tool inputSchema/outputSchema and elicitation requestedSchema fields). Schemas may explicitly declare alternative dialects via the $schema field. This resolves ambiguity that has caused compatibility issues between implementations.

Motivation

The MCP specification does not explicitly state which JSON Schema version to use for embedded schemas. This has caused:

  • Validation failures between clients and servers assuming different versions
  • Implementation divergence across SDK ecosystems
  • Developer uncertainty requiring arbitrary version choices

Community discussion (GitHub Discussion #366, PR #655) revealed that implementations were split between draft-07 and 2020-12, with multiple maintainers and community members expressing strong preference for 2020-12 as the default.

Specification

1. Default Dialect

Embedded JSON schemas within MCP messages MUST conform to JSON Schema 2020-12 when no $schema field is present.

2. Explicit Dialect Declaration

Schemas MAY include an explicit $schema field to declare a different dialect:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "name": { "type": "string" }
  }
}

3. Schema Validation Requirements

  • Schemas MUST be valid according to their declared or default dialect
  • The inputSchema field MUST NOT be null

For tools with no parameters, use one of these valid approaches:

  • true - accepts any input (most permissive)
  • {} - equivalent to true, accepts any input
  • { "type": "object" } - accepts any object with any properties
  • { "type": "object", "additionalProperties": false } - accepts only empty objects {}

Example for a tool with no parameters:

{
  "name": "get_current_time",
  "description": "Returns the current server time",
  "inputSchema": {
    "type": "object",
    "additionalProperties": false
  }
}

4. Scope of Application

This specification applies to:

  • tools/list response: inputSchema and outputSchema
  • prompts/elicit request: requestedSchema
  • Future MCP features embedding JSON Schema definitions

5. Implementation Requirements

Servers MUST:

  • Generate schemas conforming to 2020-12 by default
  • Include explicit $schema when using non-default dialects

Clients MUST:

  • Validate schemas according to declared or default dialect
  • Support at least JSON Schema 2020-12

Rationale

Why 2020-12?

  1. Ecosystem alignment: Python SDK (via Pydantic) and Go SDK implementations prefer/use 2020-12
  2. Modern features: Better validation capabilities and composition support
  3. Community preference: Multiple maintainers and community members in PR #655 discussion advocated for 2020-12 over draft-07
  4. Current standard: 2020-12 is the stable version as of 2025

Why allow explicit declaration?

  • Supports migration paths for existing schemas
  • Provides flexibility without protocol changes
  • Follows JSON Schema best practices

Alternatives considered

  • Draft-07 as default: Rejected after community feedback; older version with less capability
  • No default: Rejected as unnecessarily verbose; adds boilerplate
  • Multiple equal versions: Rejected; creates unpredictability and fragmentation

Backward Compatibility

This is technically a clarification, and not a breaking change:

  • Existing schemas without $schema default to 2020-12
  • Servers can add explicit $schema during transition
  • Basic schemas (type, properties, required) work across versions

Migration may be needed for schemas assuming draft-07 by default:

  • Schemas using dependencies (→ dependentSchemas + dependentRequired)
  • Positional array validation (→ prefixItems)

Migration strategy: Add explicit $schema: "http://json-schema.org/draft-07/schema#" during transition, then update to 2020-12 features.

Reference Implementation

SDK Implementations

Python SDK - Already compatible:

  • Uses Pydantic for schema generation
  • Pydantic defaults to 2020-12 via .model_json_schema()

Go SDK - Implemented 2020-12:

  • Explicit 2020-12 implementation completed
  • Confirmed by @samthanawalla in PR #655 discussion

Other SDKs:

  • May require updates but based on other examples, there should be straightforward or out-of-the-box options to support this. I can add more examples here or we can create issues to follow up on these after acceptance.

Security Implications

No specific security implications have been identified from establishing 2020-12 as the default dialect. The clarification reduces ambiguity that could lead to validation mismatches between implementations, which is a minor security improvement through increased predictability.

Implementations should use well-maintained JSON Schema validator libraries and keep them updated, as with any dependency.

SEP-1330: Elicitation Enum Schema Improvements

SEP-1330 proposes deprecating the non-standard enumNames property in favor of JSON Schema 2020-12 compliant patterns. This work is directly enabled by establishing 2020-12 as the default dialect.

Implementation Consideration:
As noted in SEP-1330 discussion, there is some concern about parsing complexity with advanced JSON Schema features like oneOf and anyOf. However, these features are part of the JSON Schema standard and well-supported by mature validator libraries. Implementations can balance standards compliance with their parsing needs by using well-tested JSON Schema validation libraries.

SEP-834: Full JSON Schema 2020-12 Support

This SEP establishes the foundation (default dialect) while SEP-834 addresses comprehensive support for 2020-12 features.

Open Questions

The schema for the spec itself references draft-07 and the typescript-json-schema package we use to generate it only supports draft-07.

Options:

  1. Update schema generation script to patch to 2020-12 after generation (this is what I did in the current PR)
  2. Switch to a different schema generator that supports 2020-12
  3. Leave as-is since it doesn't actually conflict with the spec?

Personally I'd prefer (1) in the short term and then (2) as a follow-up.

SEP-1686 Tasks

Final · Standards Track · Created 2025-10-20

  • Status: Final
  • Type: Standards Track
  • Created: 2025-10-20
  • Author(s): Surbhi Bansal, Luca Chang
  • Issue: #1686

Abstract

This SEP is preserved as a historical record of the experimental tasks feature shipped in the 2025-11-25 specification. The code examples below are non-normative pseudocode written against the v1 SDKs. The draft specification moves tasks out of the core protocol and into the io.modelcontextprotocol/tasks extension (SEP-2663).

This SEP improves support for task-based workflows in the Model Context Protocol (MCP). It introduces both the task primitive and the associated task ID, which can be used to query the state and results of a task, up to a server-defined duration after the task has completed. This primitive is designed to augment other requests (such as tool calls) to enable call-now, fetch-later execution patterns across all requests for servers that support this primitive.

Motivation

The current MCP specification supports tool calls that execute a request and eventually receive a response, and tool calls can be passed a progress token to integrate with MCP’s progress-tracking functionality, enabling host applications to receive status updates for a tool call via notifications. However, there is no way for a client to explicitly request the status of a tool call, resulting in states where it is possible for a tool call to have been dropped on the server, and it is unknown if a response or a notification may ever arrive. Similarly, there is no way for a client to explicitly retrieve the result of a tool call after it has completed — if the result was dropped, clients must call the tool again, which is undesirable for tools expected to take minutes or more. This is particularly relevant for MCP servers abstracting existing workflow-based APIs, such as AWS Step Functions, Workflows for Google Cloud, or APIs representing CI/CD pipelines, among other applications.

Today, it is possible for individual MCP servers to represent tools in a way that enables this, with certain compromises. For example, a server may expose a long_running_tool and wish to support this pattern, splitting it into three separate tools to accommodate this:

  1. start_long_running_tool: This would start the work represented by long_running_tool and return a tracking token of some kind, such as a job ID.
  2. get_long_running_tool_status(token): This would accept the tracking token and return the current status of the tool call, informing the caller that the operation is still ongoing.
  3. get_long_running_tool_result(token): This would accept the tracking token and return the result of the tool call, if it is available.

Representing a tool in this way seems to solve for the use case, but it introduces a new problem: Tools are generally-expected to be orchestrated by an agent, and agent-driven polling is both unnecessarily expensive and inconsistent — it relies on prompt engineering to steer an agent to poll at all. In the original long_running_tool case, the client had no way of knowing if a response would ever be received, while in the start_long_running_tool case, the application has no way of knowing if the agent will orchestrate tools according to the specific contract of the server.

It is also impossible for the host application to take ownership of this orchestration, as this tool-splitting is both conventions-based and may be implemented in different ways across MCP servers — one server may have three tools for one conceptual operation (as in our example), or it may have more, in the case of more complex, multi-step operations.

On the other hand, if active task polling is not needed, existing MCP servers can fully-wrap a workflow API in a single tool call that polls for a result, but this introduces an undesirable implementation cost: an MCP server wrapping an existing workflow API is a server that only exists for polling other systems.

Affected Customer Use Cases These concerns are backed by real use cases that Amazon has seen both internally and with their external customers (identities redacted where non-public):

1. Healthcare & Life Sciences Data Analysis Challenge: Amazon’s customers in the healthcare and life sciences industry are attempting to use MCP to wrap existing computational tools to analyze molecular properties and predict drug interactions, processing hundreds of thousands of data points per job from chemical libraries through multiple inference models simultaneously. These complex, multi-step workflows require a way to actively check statuses, as they take upwards of several hours, making retries undesirable. Current Workaround: Not yet determined. Impact: Cannot integrate with real-time research workflows, prevents interactive drug discovery platforms, and blocks automated research pipelines. These customers are looking for best practices for workflow-based tool calls and have noted the lack of first-class support in MCP as a concern. If these customers do not have a solution for long-running tool calls, they will likely forego MCP and continue using their existing platforms. Ideal: Concurrent and poll-able tool calls as an answer for operations executing in the range of a few minutes, and some form of push notification system to avoid blocking their agents on long analyses on the order of hours. This SEP supports the former use case, and offers a framework that could extend to support the latter.

2. Enterprise Automation Platforms Challenge: Amazon’s large enterprise customers are looking to develop internal MCP platforms to automate SDLC processes across their organizations, extending to sales, customer service, legal, HR, and cross-divisional teams. They have noted they have long-running agent and agent-tool interactions, supporting complex business process automation. Current Workaround: Not yet determined. Considering an application-level system outside of MCP backed by webhooks. Impact: Limitations related to the host application being unaware of tool execution state prevent complex business process automation and limit sophisticated multi-step operations. These customers want to dispatch processes concurrently and collect their results later, and are noting the lack of explicit late-retrieval as a concern — and are considering involved application-level notification systems as a possible workaround. Ideal: Built-in mechanisms for actively checking the status of ongoing work to avoid needing to implement notification systems specific to their own tool conventions themselves.

3. Code Migration Workflows Challenge: Amazon has automated code migration and transformation tools to perform upgrades across its own codebases and those of external customers, and is attempting to wrap those tools in MCP servers. These migrations analyze dependencies, transform code to avoid deprecated runtime features, and validate changes across multiple repositories. These migrations range from minutes to hours depending on migration scope, complexity, and validation requirements. Current Workaround: Developers implement manual tracking by splitting a job into create and get tools, forcing models to manage state and repeatedly poll for completion. Impact: Poor developer experience due to needing to replicate this hand-rolled polling mechanism across many tools. One team had to debug an issue where the model would hallucinate job names if it hadn’t listed them first. Validating that this does not happen across many tools in a large toolset is time-consuming and error-prone. Ideal: Support natively polling tool state at the data layer to support pushing a tool to the background and avoiding blocking other tasks in the chat session, while still supporting deterministic polling and result retrieval. The team needs the same pattern across many tools in their MCP servers, and wants a common solution across them, which this SEP directly supports.

4. Test Execution Platforms Challenge: Amazon’s internal test infrastructure executes comprehensive test suites including thousands of cases, integration tests across services, and performance benchmarks. They have built an MCP server wrapping this existing infrastructure. Current Workaround: For streaming test logs, the MCP server exposes a tool that can read a range of log lines, as it cannot effectively notify the client when the execution is complete. There is not yet any workaround for executing test runs. Impact: Cannot run a test suite and stream its logs simultaneously without a single hours-long tool call, which would time out on either the client or the server. This prevents agents from looking into test failures in an incomplete test run until the entire test suite has completed, potentially hours later. Ideal: Support host application-driven tool polling for intermediate results, so a client can be notified when a long-running tool is complete. This SEP does not fully-support this use case (it does enable polling), but the Task execution model can be extended to do so, as discussed in the “Future Work” section.

5. Deep Research Challenge: Deep research tools spawn multiple research agents to gather and summarize information about topics, going through several rounds of search and conversation turns internally to produce a final result for the caller application. The tool takes an extended amount of time to execute, and it is not always clear if the tool is still executing. Current Workaround: The research tool is split into a separate create tool to create a report job and a get tool to get the status/result of that job later. Impact: When using this with host applications, the agent sometimes runs into issues calling the get tool repeatedly — in particular, it calls the tool once before ending its conversation turn, claiming to be "waiting" before calling the tool again. It cannot resume until receiving a new user message. This also complicates expiration times, as it is not possible to predict when the client will retrieve the result when this occurs. It is possible to work around this by adding a wait tool for the model, but this prevents the model from doing anything else concurrently. Ideal: Support polling a tool call’s state in a deterministic way and notify the model when a result is ready, so the tool result can be immediately retrieved and deleted from the server. Other than notifying the model (a host application concern), this SEP fully supports this use case.

6. Agent-to-Agent Communication (Multi-Agent Systems) Challenge: One of Amazon’s internal multi-agent systems for customer question answering faces scenarios where agents require significant processing time for complex reasoning, research, or analysis. When agents communicate through MCP, slow agents cause cascading delays throughout this system, as agents are forced to wait on their peers to complete their work. Current Workaround: Not yet determined. Impact: Communication pattern creates cascading delays, prevents parallel agent processing, and degrades system responsiveness for other time-sensitive interactions. Ideal: Some method to allow agents to perform other work concurrently and get notified once long-running tasks complete. This SEP supports this use case by enabling host applications to implement background polling for select tool calls without blocking agents.

These use cases demonstrate that a mechanism to actively track tool calls and defer results is a real requirement for these types of MCP deployments in production environments.

Integration with Existing Architectures Many workflow-driven systems already provide active execution-tracking capabilities with built-in status metadata, monitoring, and data retention policies. This proposal enables MCP servers to expose these existing APIs with thin MCP wrappers while maintaining their existing reliability.

Benefits for Existing Architectures:

  • Leverage Existing State Management: Systems like AWS Step Functions, Workflows for Google Cloud, and CI/CD platforms already maintain execution state, logs, and results. MCP servers can expose these systems' existing APIs without pushing the responsibility of polling to a fallible agent.
  • Preserve Native Monitoring: Existing monitoring, alerting, and observability tools continue to work unchanged. The execution happens almost entirely within the existing workflow-management system.
  • Reduce Implementation Overhead: Server implementers don't need to build new state management, persistence, or monitoring infrastructure. They can focus on the MCP protocol mapping of their existing APIs to tasks.

This SEP simplifies integration with existing workflows and allows workflow services to continue to manage their own state while delivering a quality customer experience, rather than offloading to agent-polling or building MCP servers that do nothing but poll other services.

Specification

This SEP introduces a mechanism for requestors (which can be either clients or servers, depending on the direction of communication) to augment their requests with tasks. Tasks are durable state machines that carry information about the underlying execution state of the request they wrap, and are intended for requestor polling and deferred result retrieval. Each task is uniquely identifiable by a requestor-generated task ID.

1. User Interaction Model

Tasks are designed to be application-driven—receivers tightly-control which requests (if any) support task-based execution and manage the lifecycles of those tasks; meanwhile, requestors own the responsibility for augmenting requests with tasks, and for polling on the results of those tasks.

Implementations are free to expose tasks through any interface pattern that suits their needs—the protocol itself does not mandate any specific user interaction model.

2. Capabilities

Servers and clients that support task-augmented requests MUST declare a tasks capability during initialization. The tasks capability is structured by request category, with boolean properties indicating which specific request types support task augmentation.

Refer to https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1732 for details.

3. Protocol Messages

3.1. Creating Tasks

To create a task, requestors send a request with the modelcontextprotocol.io/task key included in _meta, with a taskId value representing the task ID. Requestors MAY include a keepAlive, with a value representing how long after completion the requestor would like the task results to be kept for.

Request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "some_method",
  "params": {
    "_meta": {
      "modelcontextprotocol.io/task": {
        "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
        "keepAlive": 60000
      }
    }
  }
}
3.2. Getting Tasks

To retrieve the state of a task, requestors send a tasks/get request:

Request:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tasks/get",
  "params": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "_meta": {
      "modelcontextprotocol.io/related-task": {
        "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
      }
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "keepAlive": 30000,
    "pollFrequency": 5000,
    "status": "submitted",
    "_meta": {
      "modelcontextprotocol.io/related-task": {
        "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
      }
    }
  }
}
3.3. Retrieving Task Results

To retrieve the result of a completed task, requestors send a tasks/result request:

Request:

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tasks/result",
  "params": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "_meta": {
      "modelcontextprotocol.io/related-task": {
        "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
      }
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"
      }
    ],
    "isError": false,
    "_meta": {
      "modelcontextprotocol.io/related-task": {
        "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
      }
    }
  }
}
3.4. Task Creation Notification

When a receiver creates a task, it MUST send a notifications/tasks/created notification to inform the requestor that the task has been created and polling can begin.

Notification:

{
  "jsonrpc": "2.0",
  "method": "notifications/tasks/created",
  "params": {
    "_meta": {
      "modelcontextprotocol.io/related-task": {
        "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
      }
    }
  }
}

The task ID is conveyed through the modelcontextprotocol.io/related-task metadata key. The notification parameters are otherwise empty.

This notification resolves the race condition where a requestor might attempt to poll for a task before the receiver has finished creating it. By sending this notification immediately after task creation, the receiver signals that the task is ready to be queried via tasks/get.

Receivers that do not support tasks (and thus ignore task metadata in requests) will not send this notification, allowing requestors to fall back to waiting for the original request response.

3.5. Listing Tasks

To retrieve a list of tasks, requestors send a tasks/list request. This operation supports pagination.

Request:

{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tasks/list",
  "params": {
    "cursor": "optional-cursor-value"
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "tasks": [
      {
        "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
        "status": "working",
        "keepAlive": 30000,
        "pollFrequency": 5000
      },
      {
        "taskId": "abc123-def456-ghi789",
        "status": "completed",
        "keepAlive": 60000
      }
    ],
    "nextCursor": "next-page-cursor"
  }
}
3.6 Deleting Tasks

To explicitly delete a task and its associated results, requestors send a tasks/delete request.

Request:

{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "tasks/delete",
  "params": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "_meta": {
      "modelcontextprotocol.io/related-task": {
        "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
      }
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "_meta": {
      "modelcontextprotocol.io/related-task": {
        "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
      }
    }
  }
}

4. Behavior Requirements

These requirements apply to all parties that support receiving task-augmented requests.

4.1. Task Support and Handling
  1. Receivers that do not support task augmentation on a request MUST process the request normally, ignoring any task metadata in _meta.
  2. Receivers that support task augmentation MAY choose which request types support tasks.
4.2. Task ID Requirements
  1. Task IDs MUST be a string value.
  2. Task IDs SHOULD be unique across all tasks controlled by the receiver.
  3. The receiver of a request with a task ID in its _meta MAY validate that the provided task ID has not already been associated with a task controlled by that receiver.
4.3. Task Status Lifecycle
  1. Tasks MUST begin in the submitted status when created.
  2. Receivers MUST only transition tasks through the following valid paths:
    1. From submitted: may move to working, input_required, completed, failed, cancelled, or unknown
    2. From working: may move to input_required, completed, failed, cancelled, or unknown
    3. From input_required: may move to working, completed, failed, cancelled, or unknown
    4. Tasks in completed, failed, cancelled, or unknown status MUST NOT transition to any other status (terminal states)
  3. Receivers MAY move directly from submitted to completed if execution completes immediately.
  4. The unknown status is a terminal fallback state for unexpected error conditions. Receivers SHOULD use failed with an error message instead when possible.

Task Status State Diagram:

submittedworkingterminalinput_requiredTerminalstates:• completed• failed• cancelled• unknown
4.4. Input Required Status
  1. When a receiver sends a request associated with a task (e.g., elicitation, sampling), the receiver MUST move the task to the input_required status.
  2. The receiver MUST include the modelcontextprotocol.io/related-task metadata in the request to associate it with the task.
  3. When the receiver receives all required responses, the task MAY transition out of input_required status (typically back to working).
  4. If multiple related requests are pending, the task SHOULD remain in input_required status until all are resolved.
4.5. Keep-Alive and Resource Management
  1. Receivers MAY override the requested keepAlive duration.
  2. Receivers MUST include the actual keepAlive duration (or null for unlimited) in tasks/get responses.
  3. After a task reaches a terminal status (completed, failed, or cancelled) and its keepAlive duration has elapsed, receivers MAY delete the task and its results.
  4. Receivers MAY include a pollFrequency value (in milliseconds) in tasks/get responses to suggest polling intervals. Requestors SHOULD respect this value when provided.
4.6. Result Retrieval
  1. Receivers MUST only return results from tasks/result when the task status is completed.
  2. Receivers MUST return an error if tasks/result is called for a task in any other status.
  3. Requestors MAY call tasks/result multiple times for the same task while it remains available.
  1. All requests, notifications, and responses related to a task MUST include the modelcontextprotocol.io/related-task key in their _meta, with the value set to an object with a taskId matching the associated task ID.
  2. For example, an elicitation that a task-augmented tool call depends on MUST share the same related task ID with that tool call's task.
4.8. Task Cancellation
  1. When a receiver receives a notifications/cancelled notification for the JSON-RPC request ID of a task-augmented request, the receiver SHOULD immediately move the task to the cancelled status and cease all processing associated with that task.
  2. Due to the asynchronous nature of notifications, receivers MAY not cancel task processing instantaneously. Receivers SHOULD make a best-effort attempt to halt execution as quickly as possible.
  3. If a notifications/cancelled notification arrives after a task has already reached a terminal status (completed, failed, cancelled, or unknown), receivers SHOULD ignore the notification.
  4. After a task reaches cancelled status and its keepAlive duration has elapsed, receivers MAY delete the task and its metadata.
  5. Requestors MAY send notifications/cancelled at any time during task execution, including when the task is in input_required status. If a task is cancelled while in input_required status, receivers SHOULD also disregard any pending responses to associated requests.
  6. Because notifications do not provide confirmation of receipt, requestors SHOULD continue to poll with tasks/get after sending a cancellation notification to confirm the task has transitioned to cancelled status. If the task does not transition to cancelled within a reasonable timeframe, requestors MAY assume the cancellation was not processed.
4.9. Task Listing
  1. Receivers SHOULD use cursor-based pagination to limit the number of tasks returned in a single response.
  2. Receivers MUST include a nextCursor in the response if more tasks are available.
  3. Requestors MUST treat cursors as opaque tokens and not attempt to parse or modify them.
  4. If a task is retrievable via tasks/get for a requestor, it MUST be retrievable via tasks/list for that requestor.
4.10 Task Deletion
  1. Receivers MAY accept or reject delete requests for any task at their discretion.
  2. If a receiver accepts a delete request, it SHOULD delete the task and all associated results and metadata.
  3. Receivers MAY choose not to support deletion at all, or only support deletion for tasks in certain statuses (e.g., only terminal statuses).
  4. Requestors SHOULD delete tasks containing sensitive data promptly rather than relying solely on keepAlive expiration for cleanup.

5. Message Flow

https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1686#issuecomment-3452378176

6. Data Types

Task

A task represents the execution state of a request. The task metadata includes:

  • taskId: Unique identifier for the task
  • keepAlive: Time in milliseconds that results will be kept available after completion
  • pollFrequency: Suggested time in milliseconds between status checks
  • status: Current state of the task execution
Task Status

Tasks can be in one of the following states:

  • submitted: The request has been received and queued for execution
  • working: The request is currently being processed
  • input_required: The request is waiting on additional input from the requestor
  • completed: The request completed successfully and results are available
  • failed: The task lifecycle itself encountered an error, unrelated to the associated request logic
  • cancelled: The request was cancelled before completion
  • unknown: A terminal fallback state for unexpected error conditions when the receiver cannot determine the actual task state
Task Metadata

When augmenting a request with task execution, the modelcontextprotocol.io/task key is included in _meta:

{
  "modelcontextprotocol.io/task": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "keepAlive": 60000
  }
}

Fields:

  • taskId (string, required): Client-generated unique identifier for the task
  • keepAlive (number, optional): Requested duration in milliseconds to retain results after completion
Task Creation Notification

When a receiver creates a task, it sends a notifications/tasks/created notification to signal that the task is ready for polling. The notification has empty params, with the task ID conveyed through the modelcontextprotocol.io/related-task metadata key:

{
  "jsonrpc": "2.0",
  "method": "notifications/tasks/created",
  "params": {
    "_meta": {
      "modelcontextprotocol.io/related-task": {
        "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
      }
    }
  }
}

This notification enables requestors to begin polling without encountering race conditions where the task might not yet exist on the receiver.

Task Get Request

The tasks/get request retrieves the current state of a task:

{
  taskId: string; // The task identifier to query
}
Task Get Response

The tasks/get response includes:

{
  taskId: string; // The task identifier
  status: TaskStatus; // Current task state
  keepAlive: number | null; // Actual retention duration in milliseconds, null for unlimited
  pollFrequency?: number; // Suggested polling interval in milliseconds
  error?: string; // Error message if status is "failed"
}
Task Result Request

The tasks/result request retrieves the result of a completed task:

{
  taskId: string; // The task identifier to retrieve results for
}
Task Result Response

The tasks/result response returns the original result that would have been returned by the request:

{
  // The structure matches the result type of the original request
  // For example, a tools/call task would return CallToolResult structure
  [key: string]: unknown;
}

The result structure depends on the original request type. The receiver returns the same result structure that would have been returned if the request had been executed without task augmentation.

Task List Request

The tasks/list request retrieves a list of tasks:

{
  cursor?: string; // Optional cursor for pagination
}
Task List Response

The tasks/list response includes:

{
  tasks: Array<{
    taskId: string;           // The task identifier
    status: TaskStatus;       // Current task state
    keepAlive: number | null; // Retention duration in milliseconds, null for unlimited
    pollFrequency?: number;   // Suggested polling interval in milliseconds
    error?: string;           // Error message if status is "failed"
  }>;
  nextCursor?: string;        // Cursor for next page, absent if no more results
}

All requests, responses, and notifications associated with a task MUST include the modelcontextprotocol.io/related-task key in _meta:

{
  "modelcontextprotocol.io/related-task": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
  }
}

This associates messages with their originating task across the entire request lifecycle.

7. Error Handling

Tasks use two error reporting mechanisms:

  1. Protocol Errors: Standard JSON-RPC errors for protocol-level issues
  2. Task Execution Errors: Errors in the underlying request execution, reported through task status
7.1. Protocol Errors

Receivers MUST return standard JSON-RPC errors for the following protocol error cases:

  • Invalid or nonexistent taskId in tasks/get, tasks/list, or tasks/result: -32602 (Invalid params)
  • Invalid or nonexistent cursor in tasks/list: -32602 (Invalid params)
  • Request with a taskId that was already used for a different task (if the receiver validates task ID uniqueness): -32602 (Invalid params)
  • Attempting to retrieve result when task is not in completed status: -32602 (Invalid params)
  • Internal errors: -32603 (Internal error)

Receivers SHOULD provide informative error messages to describe the cause of errors.

Example: Task not found

{
  "jsonrpc": "2.0",
  "id": 70,
  "error": {
    "code": -32602,
    "message": "Failed to retrieve task: Task not found"
  }
}

Example: Task expired

{
  "jsonrpc": "2.0",
  "id": 71,
  "error": {
    "code": -32602,
    "message": "Failed to retrieve task: Task has expired"
  }
}

NOTE: Receivers are not obligated to retain task metadata indefinitely. It is compliant behavior for a receiver to return a "not-found" error if it has purged an expired task.

Example: Result requested for incomplete task

{
  "jsonrpc": "2.0",
  "id": 72,
  "error": {
    "code": -32602,
    "message": "Cannot retrieve result: Task status is 'working', not 'completed'"
  }
}

Example: Duplicate task ID (if receiver validates uniqueness)

{
  "jsonrpc": "2.0",
  "id": 73,
  "error": {
    "code": -32602,
    "message": "Task ID already exists: 786512e2-9e0d-44bd-8f29-789f320fe840"
  }
}
7.2. Task Execution Errors

When the underlying request fails during execution, the task moves to the failed status. The tasks/get response SHOULD include an error field with details about the failure:

{
  taskId: string;
  status: "failed";
  keepAlive: number | null;
  pollFrequency?: number;
  error?: string;  // Description of what went wrong
}

Example: Task with execution error

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "status": "failed",
    "keepAlive": 30000,
    "error": "Tool execution failed: API rate limit exceeded"
  }
}

For tasks that wrap requests with their own error semantics (like tools/call with isError: true), the task should still reach completed status, and the error information is conveyed through the result structure of the original request type.

8. Security Considerations

8.1. Task Isolation and Access Control
  1. Receivers SHOULD scope task IDs to prevent unauthorized access:
    1. Bind tasks to the session that created them (if sessions are supported)
    2. Bind tasks to the authentication context (if authentication is used)
    3. Reject tasks/get, tasks/list, or tasks/result requests for tasks from different sessions or auth contexts
  2. Receivers that do not implement session or authentication binding SHOULD document this limitation clearly, as task results may be accessible to any requestor that can guess the task ID.
  3. Receivers SHOULD implement rate limiting on:
    1. Task creation to prevent resource exhaustion
    2. Task status polling to prevent denial of service
    3. Task result retrieval attempts
    4. Task listing requests to prevent denial of service
8.2. Resource Management

WARNING: Task results may persist longer than the original request execution time. For sensitive operations, requestors should carefully consider the security implications of extended result retention and may want to retrieve results promptly and request shorter keepAlive durations.

  1. Receivers SHOULD:
    1. Enforce limits on concurrent tasks per requestor
    2. Enforce maximum keepAlive durations to prevent indefinite resource retention
    3. Clean up expired tasks promptly to free resources
  2. Receivers SHOULD:
    1. Document maximum supported keepAlive duration
    2. Document maximum concurrent tasks per requestor
    3. Implement monitoring and alerting for resource usage
8.3. Audit and Logging
  1. Receivers SHOULD:
    1. Log task creation, completion, and retrieval events for audit purposes
    2. Include session/auth context in logs when available
    3. Monitor for suspicious patterns (e.g., many failed task lookups, excessive polling)
  2. Requestors SHOULD:
    1. Log task lifecycle events for debugging and audit purposes
    2. Track task IDs and their associated operations

Rationale

Design Decision: Generic Task Primitive

The decision to implement tasks as a generic request augmentation mechanism (rather than tool-specific or method-specific) was made to maximize protocol simplicity and flexibility.

Tasks are designed to work with any request type in the MCP protocol, not just tool calls. This means that resources/read, prompts/get, sampling/createMessage, and any future request types can all be augmented with task metadata. This approach provides significant benefits over a tool-specific design.

From a protocol perspective, this design eliminates the need for separate task implementations per request type. Instead of defining different async patterns for tools versus resources versus prompts, a single set of task management methods (tasks/get and tasks/result) works uniformly across all request types. This uniformity reduces cognitive load for implementers and creates a consistent experience for applications using the protocol.

The generic design also provides implementation flexibility. Servers can choose which requests support task augmentation without requiring protocol changes or version negotiation. If a server doesn't support tasks for a particular request type, it simply ignores the task metadata and processes the request normally. This allows servers to add task support to requests incrementally, starting with high-value operations and expanding over time based on actual usage patterns.

Architecturally, tasks are treated as metadata rather than a separate execution model. They augment existing requests rather than replacing them. The original request/response flow remains intact—the request still gets a response eventually. Tasks simply provide an additional polling-based mechanism for result retrieval. This design ensures that related messages (such as elicitations during task execution) can be associated consistently via the modelcontextprotocol.io/related-task metadata key, regardless of the underlying request type.

Design Decision: Metadata-Based Augmentation

Using _meta for task information rather than dedicated request parameters was chosen to maintain a clear separation of concerns between request semantics and execution tracking.

Task information is fundamentally orthogonal to request semantics. The task ID and keepAlive duration don't affect what the request does—they only affect how the result is retrieved and retained. A tools/call request performs the same operation whether or not it includes task metadata. The task metadata simply provides an alternative mechanism for accessing the result.

By placing task information in _meta, we create a clear architectural boundary between "what to execute" (request parameters) and "how to track execution" (task metadata). This boundary makes it easier for implementers to reason about the protocol. Request parameters define the operation being performed, while metadata provides orthogonal concerns like progress tracking, task management, and other execution-related information.

This approach also provides natural backward compatibility. Servers that don't support tasks can ignore the _meta content without breaking request processing. The request parameters remain valid and complete, so the operation can proceed normally. This means no protocol version negotiation is required—the new functionality is purely additive and non-disruptive.

SDKs can provide ergonomic abstractions over the task primitive while maintaining the separation of concerns, for example:

// === MCP SDK (Pseudocode based loosely on modelcontextprotocol/typescript-sdk) ===

/**
 * NEW: A request that resolves to a result, either directly or by polling a task.
 */
class PendingRequest<TResult> {
  constructor(readonly protocol: Protocol, readonly result: Promise<TResult>, readonly taskId?: string) {}

  /**
   * Waits for a result, calling onTaskStatus if provided and a task was created.
   */
  async result({ onTaskStatus }): Promise<TResult> => {
    if (!onTaskStatus || !this.taskId) {
      // No task listener or task ID provided, just block for the result
      return await result;
    }

    // Whichever is successful first (or a failure if all fail) is returned.
    return Promise.any([
      result, // Blocks for result
      (async () => {
        // Blocks for a notifications/tasks/created with the provided task ID
        await this.protocol.waitForTask(this.taskId);
        return await taskHandler(this.taskId);
      })(),
    ]);
  }

  /**
   * Encapsulates polling for a result, calling onTaskStatus after querying the task.
   */
  private async taskHandler({ onTaskStatus }): Promise<TResult> => {
    // Poll for completion
    let task: Task;
    do {
      task = await this.protocol.getTask(this.taskId);
      await onTaskStatus(task);
      await sleep(task.pollFrequency ?? DEFAULT_POLLING_INTERNAL);
    } while (!task.isTerminal());

    // Process result
    return await this.protocol.getTaskResult(this.taskId);
  }
}

/**
 * Simplified/partial client session implementation for illustration purposes.
 * Extends a base class it shares with the server.
 */
class Client extends Protocol {
  /**
   * Existing request method, but with most implementation refactored to beginCallTool
   */
  async callTool<TResult>(
    params: CallToolRequest['params'],
    resultSchema: Schema<TResult>,
  ) {
    // Existing request methods can be changed to reuse new methods exposed for
    // separating request/response flows.
    const request = await this.beginCallTool(params, resultSchema);
    return request.result();
  }

  /**
   * NEW: Low-level method that starts a tool call and returns a PendingRequest
   * object for more granular control.
   */
  async beginCallTool<TResult>(
    params: CallToolRequest['params'],
    resultSchema: Schema<TResult>,
  ) {
    const request = await this.beginRequest({ method: 'tools/call', params }, resultSchema, options);
    return request;
  }
}

// === HOST APPLICATION ===

// Begin a tool call with task support
const pending: PendingRequest<CallToolResult> = await client.beginCallTool(
  {
    name: "analyze_dataset",
    arguments: { dataset: "large_file.csv" },
  },
  CallToolResultSchema,
  {
    keepAlive: 3600000,
  },
);

// Client code can assume tasks are supported, and the fallback case can be handled internally
const result = await pending.result({
  onTaskStatus: async (task) => {
    await sendLatestStateSomewhere(task);
  },
});

As the design does not alter the basic request semantics, the existing form would continue to work as well:

const result = await client.callTool(
  {
    name: "analyze_dataset",
    arguments: { dataset: "large_file.csv" },
  },
  CallToolResultSchema,
);

Design Decision: Client-Generated Task IDs

The choice to have clients generate task IDs rather than having servers assign them provides several critical benefits:

Idempotency and Fault Tolerance: The primary benefit is enabling idempotent task creation. When a client generates the task ID, it can safely retry a task-augmented request if it doesn't receive a response, knowing that the server will recognize the duplicate task ID and return an error. This is essential for reliable operation over unreliable networks:

  • If a request times out, the client can safely retry without creating duplicate tasks
  • If a connection drops before the response arrives, the client can reconnect and retry
  • The server validates task ID uniqueness and returns an error for duplicates, confirming whether the task was created

With server-generated task IDs, a timeout or connection failure creates uncertainty—the client doesn't know whether the task was created, and has no safe way to retry without potentially creating duplicate tasks.

Simplicity for Clients: Client-generated task IDs simplify the client's implementation by eliminating the need to correlate the initial response with a task identifier. The client can immediately begin polling for task status using the task ID it generated, without needing to parse the response to extract a server-assigned identifier. This is particularly valuable for asynchronous programming models where the client may want to store the task ID before the response arrives.

Trade-offs for Servers: The main trade-off is that servers wrapping existing workflow systems with their own task identifiers will generally handle this by maintaining a mapping between the client-provided task IDs and the underlying system's identifiers. For example, an MCP server wrapping AWS Step Functions might receive a client-generated task ID like "client-abc-123" and need to track that it corresponds to Step Functions execution ARN "arn:aws:states:...:exec-xyz".

This requires:

  • Persistent storage for the task ID mapping (typically a simple key-value store)
  • Maintaining the mapping for the task's keepAlive duration
  • Handling mapping lookups for task status and result retrieval

However, this complexity is typically minor compared to the overall work of integrating an existing workflow system into MCP. Most workflow systems already require state management for tracking execution, and maintaining a task ID mapping is a straightforward addition. The mapping structure is simple (client task ID maps to an internal identifier), and can be implemented using existing databases or key-value stores such a server likely already uses for other state management.

Design Decision: Task Creation Notification

The decision to use a notifications/tasks/created notification rather than altering the response semantics (as #1391 proposed) acknowledges the asynchronous nature of task creation and enables efficient race patterns between task-based polling and traditional request/response flows.

When a server creates a task, it must signal to the client that the task is ready for polling. There are at least two possible approaches: (1) the initial request could return synchronously with task metadata, or (2) the server could send a notification. This proposal uses notifications for several key reasons:

  1. Notifications enable fire-and-forget request processing. The server can accept the request, begin processing it, and send the notification once the task is created, without needing to block the initial request/response cycle. This is particularly important for servers that dispatch work to background systems or queues—they can acknowledge the request immediately and send the notification once the background system confirms task creation.
  2. Notifications support the race pattern that enables graceful degradation. Clients can race between waiting for the original request's response and waiting for the notifications/tasks/created notification. If the server doesn't support tasks, no notification arrives and the original response wins. If the server does support tasks, the notification typically arrives first (or approximately simultaneously), enabling polling to begin. A synchronous response would force clients to wait for the response before knowing whether to poll or not.
  3. Notifications avoid ambiguity with existing protocol semantics. If the initial request response included task metadata and the client then polled for results, it would change the implied meaning of existing notification types:
    1. Progress notifications: The current MCP specification requires that progress notifications reference tokens that "are associated with an in-progress operation." While "operation" is not formally defined, the implied understanding is that an operation is bounded by a request/response pair—progress notifications stop when the response is sent. With a synchronous response containing task metadata, progress notifications would need to continue while the task executes, expanding the implied meaning of "operation" to include asynchronous tasks that outlive the original request/response cycle. The notification-based approach avoids this semantic expansion by keeping progress notifications tied to the initial request's lifecycle, while future task-based progress can be cleanly associated via modelcontextprotocol.io/related-task metadata. We recommend that a future SEP clarify the definition of "operation" in the progress specification.
    2. Cancellation semantics: With the notification-based approach, notifications/cancelled clearly targets the original request ID and causes the associated task to move to cancelled status, maintaining a clean separation between request cancellation and task lifecycle management.

While the notification is required by the specification for servers that create tasks, there are edge cases where it may be unavailable:

  • sHTTP without stream support: In environments where either the client or the server does not support SSE streams, notifications cannot be delivered. In such cases, clients may choose to proactively poll with tasks/get using exponential backoff, though this is nonstandard and may result in unnecessary polling attempts if the server doesn't support tasks.
  • Degraded connection scenarios: If the notification is lost in transit, clients should implement reasonable timeout behavior and fall back to the original response.

The standard and recommended approach is to wait for the notifications/tasks/created notification before beginning polling. Proactive polling without waiting for the notification should be considered a fallback mechanism for constrained environments only.

Design Decision: No Capabilities Declaration

Unlike other protocol features such as tools, resources, and prompts, tasks do not require capability negotiation. This decision was made to enable graceful degradation and per-request flexibility.

Task support can be determined implicitly through usage rather than explicitly through capability declarations. When a client sends a task-augmented request, the server will process it according to its capabilities. If the server doesn't support tasks for that request type, it simply ignores the task metadata and returns the result normally through the original request/response flow. The client can then detect the lack of task support by attempting to call tasks/get and handling any errors that result.

This approach eliminates the need for complex handshakes or feature detection protocols. Clients can optimistically try task augmentation and gracefully fall back to direct response handling if needed. This makes the protocol more resilient and easier to implement.

Additionally, this design provides per-request flexibility that would be difficult to express through capabilities. A server might support tasks on some request types but not others, or support might vary based on runtime conditions such as resource availability or load. Requiring granular capability declarations per request type would significantly complicate the protocol without providing substantial benefits. The implicit detection model is simpler and more flexible.

Alternative Designs Considered

Tool-Specific Async Execution: An earlier version of this proposal (#1391) focused specifically on tool calls, introducing an invocationMode field on tool definitions to mark tools as supporting synchronous, asynchronous, or both execution modes. This approach would have added dedicated fields to the tool call request and response structures, with server-side capability declarations to indicate support for async tool execution.

While this design would have addressed the immediate need for long-running tool calls, it was rejected in favor of the more general task primitive for several reasons. First, it artificially limited the async execution pattern to tools when other request types have similar needs. Resources can be expensive to read, prompts can require complex processing, and sampling requests may involve lengthy user interactions. Creating separate async patterns for each request type would lead to protocol fragmentation and inconsistent implementation patterns.

Second, the tool-specific approach required more complex capability negotiation and version handling. Servers would need to filter tool lists based on client capabilities, and SDKs would need to manage different invocation patterns for sync versus async tools. This complexity would ripple through every layer of the implementation stack.

Finally, the tool-specific design didn't address the broader architectural need for deferred result retrieval across all MCP request types. By generalizing to a task primitive that augments any request, this proposal provides a consistent pattern that can be applied uniformly across the protocol. More importantly, this foundation is extensible to future protocol messages and features such as subtasks, making it a more appropriate building block for the protocol's evolution.

Transport-Layer Solutions: An alternative approach would be to solve for this purely at the transport layer, without introducing a new data-layer primitive. Several proposals (#1335, #1442, #1597) address transport-specific concerns such as connection resilience, request retry semantics, and stream management for sHTTP. These are valuable improvements that can mitigate many scaling and reliability challenges associated with requests that may take extended time to complete.

However, transport-layer solutions alone are insufficient for the use cases this SEP addresses. Even with perfect transport-layer reliability, several data-layer concerns remain:

First, servers and clients need a way to communicate expectations about execution patterns. Without this, host applications cannot make informed decisions about UX patterns—should they block, show a spinner, or allow the user to continue working? An annotation alone could signal that a request might take extended time, but provides no mechanism to actively check status or retrieve results later.

Second, transport-layer solutions cannot provide visibility into the execution state of a request that is still in progress. If a request stops sending progress notifications, the client cannot distinguish between "the server is doing expensive work" and "the request was lost." Transport-level retries can confirm the connection is alive, but cannot answer "is this specific request still executing?" This visibility is critical for operations where users need confidence their work is progressing.

Third, different transports would require different mechanisms for these concerns. The sHTTP proposals adjust stream management and retry semantics to fulfill these requirements, but stdio has no equivalent extension points. This creates transport-specific fragmentation where implementers must solve the same problems differently depending on their choice of transport. Data-layer operations provides consistent semantics across all transports.

Finally, deferred result retrieval and active status checks are data-layer concerns that cannot be addressed by transport improvements alone. The ability to retrieve a result multiple times, specify retention duration, and handle cleanup is orthogonal to how the underlying messages are delivered.

Resource-Based Approaches: Another possible approach would be to leverage existing MCP resources for tracking long-running operations. For example, a tool could return a linked resource that communicates operation status, and clients could subscribe to that resource to receive updates when the operation completes. This would allow servers to represent task state using the resource primitive, potentially with annotations for suggested polling frequency.

While this approach is technically feasible and servers remain free to adopt such conventions, it suffers from similar limitations as the tool-splitting pattern described in the Motivation section. Like the start_tool and get_tool convention, a resource-based tracking system would be convention-based rather than standardized, creating several challenges:

The most fundamental issue is the lack of a consistent way for clients to distinguish between ordinary resources (meant to be exposed to models) and status-tracking resources (meant to be polled by the application). Should a status resource be presented to the model? How should the client correlate a returned resource with the original tool call? Without standardization, different servers would implement different conventions, forcing clients/hosts/models to handle each server's particular approach. Extending resources with task-like semantics (such as polling frequency, keepalive durations, and explicit status states) would create a new and distinct purpose for resources that would be difficult to distinguish from their existing purpose as model-accessible content.

The resource subscription model has one additional issue: as it is push-based, it requires clients to wait for notifications of resource changes rather than actively polling for status. While this works for some use cases, it doesn't address scenarios where clients need to actively check status—for example, proactively and deterministically checking if work is still progressing, which is the original intent of this proposal.

The task primitive addresses these concerns by providing a standardized, protocol-level mechanism specifically designed for this use case, with consistent semantics that any client can leverage without host applications needing to understand server-specific conventions. While resource-based tracking remains possible for servers that prefer it and/or are already using it, this SEP provides a first-class alternative that solves the broader set of requirements identified previously.

Backward Compatibility

This SEP introduces no backward incompatibilities. All existing MCP functionality remains unchanged:

Compatibility Guarantees:

  • Existing requests work identically with or without task metadata
  • Servers that don't understand tasks process requests normally
  • No protocol version negotiation required
  • No capability declarations needed

Graceful Degradation:

  • Clients race between waiting for the original request's response and waiting for the notifications/tasks/created notification followed by polling
  • Whichever completes first (original response or task-based retrieval) is used by the client
  • If a server doesn't support tasks, no notifications/tasks/created is sent, and the original request's response is used
  • If a server supports tasks, the notifications/tasks/created notification is sent, enabling the client to begin polling for results
  • This race pattern ensures graceful degradation without requiring capability negotiation or version detection
  • Partial support is possible—servers can support tasks on some requests but not others

Adoption Path:

  • Servers can implement task support incrementally, starting with high-value request types
  • Clients can opportunistically use tasks where supported
  • No coordination required between client and server updates

Future Work

The task primitive introduced in this SEP provides a foundation for several important extensions that will enhance MCP's workflow capabilities.

Push Notifications

While this SEP focuses on client-driven polling, future work could introduce server-initiated notifications for task state changes. This would be particularly valuable for operations that take hours or longer, where continuous polling becomes impractical.

A notification-based approach would allow servers to proactively inform clients when:

  • A task completes or fails
  • A task reaches a milestone or significant state transition
  • A task requires input (complementing the input_required status)

This could be implemented through webhook-style mechanisms or persistent notification channels, depending on the transport capabilities. The proposed task ID and status model provides the necessary infrastructure for servers to identify which tasks warrant notifications and for clients to correlate notifications with their outstanding tasks.

Intermediate Results

The current task model returns results only upon completion. Future extensions could enable tasks to report intermediate results or progress artifacts during execution. This would support use cases where servers can produce partial outputs before final completion, such as:

  • Streaming analysis results as they become available
  • Reporting completed phases of multi-step operations
  • Providing preview data while full processing continues

Intermediate results would build on the proposed task ID association mechanism, allowing servers to send multiple result notifications or response messages tied to the same task ID throughout its lifecycle.

Nested Task Execution

A significant future enhancement is support for hierarchical task relationships, where a task can spawn subtasks as part of its execution. This would enable complex, multi-step workflows orchestrated by the server.

In a nested task model, a server could:

  • Create subtasks in response to a parent task reaching a state that requires additional operations
  • Communicate subtask requirements to the client, potentially including required tool calls or sampling requests
  • Track subtask completion and use subtask results to advance the parent task
  • Maintain provenance through task ID hierarchies, showing the relationship between parent and child tasks

For example, a complex analysis task might spawn several subtasks for data gathering, each represented by its own task ID but associated with the parent task. The parent task would remain in a pending state (potentially in a new tool_required status) until all required subtasks complete.

This hierarchical model would support sophisticated server-controlled workflows while maintaining the client's ability to monitor and retrieve results at any level of the task tree.

Example nested task flow
ServerClientServerClientClient Creates Parent TaskServer determines subtasks neededServer Responds with Subtask RequirementsClient initiates subtasksClient polls subtasksAll subtasks complete, parent continuestools/call "deploy_application"_meta: {taskId: "deploy-123"}notifications/tasks/createdtasks/get (taskId: "deploy-123")status: workingtasks/get (taskId: "deploy-123")status: workingchildTasks: [{ taskId: "build-456", toolName: "run_build", arguments: {...}}, { taskId: "test-789", toolName: "run_tests", arguments: {...}}]tools/call "run_build"_meta: {taskId: "build-456", parentTaskId: "deploy-123"}notifications/tasks/createdtools/call "run_tests"_meta: {taskId: "test-789", parentTaskId: "deploy-123"}notifications/tasks/createdtasks/get (taskId: "build-456")status: completedtasks/get (taskId: "test-789")status: completedtasks/get (taskId: "deploy-123")status: completedtasks/result (taskId: "deploy-123")Deployment complete

Potential Data Model Extensions: The task status response could be extended to include parent and child task relationships:

{
  taskId: string;
  status: TaskStatus;
  keepAlive: number | null;
  pollFrequency?: number;
  error?: string;

  // Extensions for nested tasks
  parentTaskId?: string;        // ID of parent task, if this is a subtask
  childTasks?: Array<{          // Subtasks required by this task
    taskId: string;             // Pre-generated task ID for the subtask
    toolName: string;           // Tool to call for this subtask
    arguments?: object;         // Arguments for the tool call
  }>;
}

This would allow clients to:

  • Discover subtasks required by a parent task through the childTasks array
  • Initiate the required subtask tool calls using the pre-generated task IDs and provided arguments
  • Navigate the task hierarchy by following parent/child relationships via parentTaskId
  • Monitor all subtasks by polling each child task ID
  • Wait for all subtasks to complete before checking parent task completion

The existing task metadata and status lifecycle are designed to be forward-compatible with these extensions.

SEP-1699 Support SSE polling via server-side disconnect

Final · Standards Track · Created 2025-10-22

  • Status: Final
  • Type: Standards Track
  • Created: 2025-10-22
  • Author(s): Jonathan Hefner (@jonathanhefner)
  • Issue: #1699

Abstract

This SEP proposes changes to the Streamable HTTP transport in order to mitigate issues regarding long-running connections and resumability.

Motivation

The Streamable HTTP transport spec does not allow servers to close a connection while computing a result. In other words, barring client-side disconnection, servers must maintain potentially long-running connections.

Specification

When a server starts an SSE stream, it MUST immediately send an SSE event consisting of an id and an empty data string in order to prime the client to reconnect with that event ID as the Last-Event-ID.

Note that the SSE standard explicitly permits setting data to an empty string, and says that the appropriate client-side handling is to record the id for Last-Event-ID but otherwise ignore the event (i.e., not call the event handler callback).

At any point after the server has sent an event ID to the client, the server MAY disconnect at will. Specifically, this part of the MCP spec will be changed from:

The server SHOULD NOT close the SSE stream before sending the JSON-RPC response for the received JSON-RPC request

To:

The server MAY close the connection before sending the JSON-RPC response if it has sent an SSE event with an event ID to the client

If a server disconnects, the client will interpret the disconnection the same as a network failure, and will attempt to reconnect. In order to prevent clients from reconnecting / polling excessively, the server SHOULD send an SSE event with a retry field indicating how long the client should wait before reconnecting. Clients MUST respect the retry field.

Rationale

Servers may disconnect at will, avoiding long-running connections. Sending a retry field will prevent the client from hammering the server with inappropriate reconnection attempts.

Backward Compatibility

  • New Client + Old Server: No changes. No backward incompatibility.
  • Old Client + New Server: Client should interpret an at-will disconnect the same as a network failure. retry field is part of the SSE standard. No backward incompatibility if client already implements proper SSE resuming logic.

Additional Information

This SEP supersedes (in part) SEP-1335.

SEP-1730 SDKs Tiering System

Final · Standards Track · Created 2025-10-29

  • Status: Final
  • Type: Standards Track
  • Created: 2025-10-29
  • Author(s): Inna Harper, Felix Weinberger
  • Issue: #1730

Abstract

This SEP proposes a tiering system for Model Context Protocol (MCP) SDKs to establish clear expectations for feature support, maintenance commitments, and quality standards. The system defines three tiers of SDK support with objective, measurable criteria for classification.

Motivation

The MCP ecosystem needs SDK harmonization to help users make informed decisions. Users currently face challenges:

  • Feature Support Uncertainty: No standardized way to know which SDKs support specific MCP features (OAuth, client/server/system features, like sampling, transports)
  • Maintenance Expectations: Unclear commitment levels for bug fixes, security patches, and feature updates
  • Implementation Timelines: No visibility into when SDKs will support new protocol versions and features

Specification

Tier Definitions

Tier 1: fully supported

SDKs in this tier provides full protocol implementation and is well supported

Requirements:

  • Feature complete and full support of the protocol
    • All conformance tests pass
    • New protocol features before the new spec version release. (There is two week window between Release Candidate and the new protocol version release)
  • SDK maintenance
    • Acknowledge and triage issues within two business days
    • Resolve security and critical bugs within seven days
    • Stable release and SDK versioning clearly documented
  • Documentation
    • Comprehensive documentation with examples for all features
    • Published dependency update policy
Tier 2: commitment to be fully supported

SDKs with established implementations actively working toward full protocol support.

Requirements:

  • Feature complete and full support of the protocol
    • 80% of conformance tests pass
    • New protocol features implemented within six months
  • SDK maintenance
    • Active issue tracking and management
    • At least one stable release
  • Documentation
    • Basic documentation covering core features
    • Published dependency update policy
  • Commitment to move to Tier1
    • Published roadmap showing intent to achieve Tier 1 or, if SDK will remain in Tier 2 indefinitely, a transparent roadmap about the direction of the SDK and reasons for not being feature complete
Tier 3: Experimental

Early-stage or specialized SDKs exploring the protocol space.

Characteristics:

  • No feature completeness guarantees
  • No stable release requirement
  • May focus on specific use cases or experimental features
  • No timeline commitments for updates
  • Suitable for niche implementations that may remain at this tier

Conformance Testing

All SDKs must undergo conformance testing using protocol trace validation: for details see Conformance Testing RFC (forthcoming). This SEP is not focusing on Conformance testing. For the initial version of tiering, we will go with the simplified version where we would have an Example server for each SDK and run simplified conformance tests against those.

ValidatorTest SuiteSDKValidatorTest SuiteSDKExecute test scenarioProtocol messagesSubmit traceCompliance reportPass/Fail result

Compliance Scoring:

  • SDKs receive a percentage score based on test results
  • Scores can be displayed as badges (e.g., "90% MCP Compliant")
  • Tier 1: 100% compliance required
  • Tier 2: 80% compliance required
  • Tier 3: No minimum requirement

Tier Advancement Process

  1. Self-Assessment: Maintainers evaluate their SDK against tier criteria
  2. Application: Submit tier advancement request with evidence
  3. Review: Community review period (2 weeks)
  4. Validation: Automated conformance testing, github stats on issues
  5. Decision: Tier assignment by MCP maintainers

Tier Relegation Process

  1. Auto validation:
    1. compliance tests continuously not passing for four week for Tier 1
    2. 20% of compliance tests continuously not passing for four week for Tier 2
  2. Issues:
    1. Issues are not addressed within two months

Requirements matrix

Feature SDK A SDK B SDK C
Protocol Features support (Conformance tests) 85% 60%% 100%
GitHub support stats 10 days 100 days 5 days
Documentation (self reported) Good Minimal Good
Tier (computed from above) Tier 2 Tier 3 Tier 1

Rationale

Why Three Tiers?

  • Tier 1 ensures users have well supported, fully-featured SDK
  • Tier 2 provides a clear pathway for improving SDKs
  • Tier 3 allows experimentation without creating barriers to entry

Why Time-Based Commitments?

While the community raised concerns about rigid timelines, they provide:

  • Clear expectations for users
  • Measurable goals for maintainers
  • Flexibility through tier progression

Why Not Just Feature Matrices?

Feature matrices alone don't communicate:

  • Maintenance commitment
  • Quality standards
  • Support expectations

The tiering system combines feature support with quality guarantees.

Alternatives Considered

1. Feature Matrix Only

Rejected because: Doesn't communicate maintenance commitments or quality standards

2. Percentage-Based Scoring

Rejected because: Too granular and doesn't capture qualitative aspects like support

3. Properties-Based System

Rejected because: Multiple overlapping properties could confuse users

4. Latest Version Listing Only

Rejected because: Simply listing "supports MCP date" fails to capture critical information:

  • Version support may be incomplete (e.g., supports <date> except OAuth)
  • No indication of maintenance commitment or issue response times
  • Lacks information about security patch timelines
  • Doesn't communicate dependency update policies
  • Version numbers alone don't indicate production readiness

5. No Formal System

Rejected because: Current ad-hoc approach creates uncertainty for users

Backward Compatibility

This proposal introduces a new classification system with no breaking changes:

  • Existing SDKs continue to function
  • Classification is opt-in initially
  • Grace period for existing SDKs to achieve tier status

Security Implications

  • Tier 1 SDKs must address security issues within 7 days
  • All tiers encouraged to follow security best practices
  • Conformance tests include security validation

Implementation Plan

  • Finalize simplified conformance test suite - Nov 4, 2025
  • SDK maintainers self-assess and apply for tiers - Nov 14, 2025
  • Initial tier assignments - before the November spec release
  • Implement full compliance tests
  • Implement automatic issue tracking analysis for SDKs

Community Impact

SDK Maintainers

  • Clear goals for improvement
  • Recognition for quality implementations
  • Structured pathway for advancement

SDK Users

  • Informed selection of SDKs
  • Clear expectations for support
  • Confidence in tier 1 implementations

Ecosystem

  • Improved overall SDK quality
  • Standardized feature support
  • Healthy competition between implementations

References

Appendix

Simplified conformance tests

While we are working on a comprehensive proposal for conformance testing which will take some time to implement, we want to move forward with at least some automated way to check if SDK has a full set of features. We will start from Servers features set, as we have many more servers than clients and the vast majority of developers using SDKs are Server implementers.

The most straightforward approach is to have an Example Server for each SDK, similar to to Everything Server. Then we will have Conformance Test Client with all the test cases we want to be able to test, for example:

  • execute “hello world” tool
  • Get prompt
  • Get completion
  • Get resource template
  • Receive notifications

What is needed form SDKs maintainers: implement everything server based on a spec. Spec will look like:

  • Tool “say_hello” to return simple text
  • Tool “show_image” to return and image
  • Tool “tool_with_logging” to return structured output in a format <> and log three events: start, process, end
  • Tool "tool_with_notifications" to return structured output in a format <> and have two notifications <>

Given well defined spec for the server and SDK documentation, it should be easy to implement it with the help of any coding agent. We want to check it into each SDKs repo as it will serve as an example for server implementers.

Once each SDK has an Everything server, we will run the Conformance Test Client against it.

SEP-1850 PR-Based SEP Workflow

Final · Process · Created 2025-11-20

Abstract

This SEP formalizes the pull request-based SEP workflow that stores proposals as markdown files in the seps/ directory of the Model Context Protocol specification repository. The workflow assigns SEP numbers from pull request numbers, maintains version history in Git, and replaces the previous GitHub Issues-based process. This establishes a file-based approach as the canonical way to author, review, and accept SEPs.

Motivation

The issue-based SEP process introduced several challenges:

  • Dispersed content: Proposal content was scattered across GitHub issues, linked documents, and pull requests, making review and archival difficult.
  • Difficult collaboration: Maintaining long-form specifications in issue bodies made iterative edits and multi-contributor collaboration harder.
  • Limited version control: GitHub issues don't provide the same version control capabilities as Git-managed files.
  • Unclear status management: The process lacked clear mechanisms for tracking status transitions and ensuring consistency between different sources of truth.

A file-based workflow addresses these issues by:

  • Keeping every SEP in version control alongside the specification itself
  • Providing Git's built-in review tooling, history, and searchability
  • Linking SEP numbers to pull requests to eliminate manual bookkeeping
  • Surfacing all discussion in the pull request thread
  • Using PR labels in conjunction with file status for better discoverability

Specification

1. Canonical Location

  • Every SEP lives in seps/{NUMBER}-{slug}.md in the specification repository
  • The SEP number is always the pull request number that introduces the SEP file
  • The seps/ directory serves as the single source of truth for all SEPs

2. Author Workflow

  1. Draft the proposal in seps/0000-{slug}.md using 0000 as a placeholder number
  2. Open a pull request containing the draft SEP and any supporting materials
  3. Request a sponsor from the Maintainers list; tag potential sponsors from MAINTAINERS.md
  4. After the PR number is known, amend the commit to rename the file to {PR-number}-{slug}.md and update the header (SEP-{PR-number} and PR: #{PR-number})
  5. Wait for sponsor assignment: Once a sponsor agrees, they will assign themselves and update the status to Draft

3. Sponsor Responsibilities

A Sponsor is a Core Maintainer or Maintainer who champions the SEP through the review process. The sponsor's responsibilities include:

  • Reviewing the proposal and providing constructive feedback
  • Requesting changes based on community input
  • Managing status transitions by:
    • Ensuring that the Status field in the SEP markdown file is accurate
    • Applying matching PR labels to keep them in sync with the file status
    • Communicating status changes via PR comments
  • Initiating formal review when the SEP is ready (moving from Draft to In-Review)
  • Raising to Core-Maintainers ensuring the SEP is presented at the Core Maintainer meeting and that author and sponsor present.
  • Ensuring quality standards are met before advancing the proposal
  • Tracking implementation progress and ensuring reference implementations are complete before Final status

4. Review Flow

Status progression follows: Draft → In-Review → Accepted → Final

Additional terminal states: Rejected, Withdrawn, Superseded, Dormant

Dormant status: If a SEP does not find a sponsor within six months, Core Maintainers may close the PR and mark the SEP as dormant.

Reference implementations must be tracked via linked pull requests or issues and must be complete before marking a SEP as Final.

5. Documentation

  • docs/community/sep-guidelines.mdx serves as the contributor-facing instructions
  • seps/README.md provides the concise reference for formatting, naming, sponsor responsibilities, and acceptance criteria
  • Both documents must reflect this workflow and be kept in sync

6. SEP File Structure

Each SEP must include:

# SEP-{NUMBER}: {Title}

- **Status**: Draft | In-Review | Accepted | Rejected | Withdrawn | Final | Superseded | Dormant
- **Type**: Standards Track | Informational | Process
- **Created**: YYYY-MM-DD
- **Author(s)**: Name <email> (@github-username)
- **Sponsor**: @github-username (or "None" if seeking sponsor)
- **PR**: https://github.com/modelcontextprotocol/specification/pull/{NUMBER}

## Abstract

## Motivation

## Specification

## Rationale

## Backward Compatibility

## Security Implications

## Reference Implementation

7. Status Management via PR Labels

To improve discoverability and filtering:

  • Sponsors must apply PR labels that match the SEP status (draft, in-review, accepted, final, etc.)
  • Both the markdown Status field and PR labels should be kept in sync
  • The markdown file serves as the canonical record (versioned with the proposal)
  • PR labels enable easy filtering and searching for SEPs by status
  • Only sponsors should modify status fields and labels; authors should request changes through their sponsor

8. Legacy Considerations

  • Contributors may optionally open a GitHub Issue for early discussion, but the authoritative SEP text lives in seps/
  • Issues should link to the relevant file once a pull request exists
  • SEP numbers are derived from PR numbers, not issue numbers

Rationale

Why File-Based?

Storing SEPs as files keeps authoritative specs versioned with the code, mirroring successful processes used by PEPs (Python Enhancement Proposals) and other standards bodies. This approach:

  • Provides built-in version control via Git
  • Enables standard code review workflows
  • Maintains clear history of all changes
  • Supports multi-contributor collaboration
  • Integrates naturally with the specification repository

Why PR Numbers?

Using pull request numbers:

  • Eliminates race conditions around manual numbering
  • Creates natural traceability between proposal and discussion
  • Prevents number conflicts
  • Simplifies the contribution process
  • Maintains a single discussion thread for review

Why PR Labels?

Adding PR labels alongside the file status:

  • Enables quick filtering of SEPs by status without opening files
  • Provides immediate visibility of SEP states in PR lists
  • Supports GitHub's search and filter capabilities
  • Complements the canonical markdown status field
  • Reduces friction for maintainers managing multiple SEPs

Making This the Primary Process

Maintaining two overlapping canonical processes risked divergence and created confusion for contributors. Establishing the file-based approach as the primary method:

  • Reduces cognitive overhead for new contributors
  • Ensures consistency in the SEP corpus
  • Simplifies maintenance for sponsors
  • Aligns with industry best practices

Backward Compatibility

  • Existing issue-based SEPs remain valid and require no migration
  • Historical GitHub Issue links continue to work
  • Future SEPs should reference the new file locations in seps/
  • Maintainers may optionally backfill historical SEPs into seps/ for archival purposes

Security Implications

No new security considerations beyond the standard code review process for pull requests.

Reference Implementation

  • This pull request (#1850) implements the canonical instructions in both seps/README.md and docs/community/sep-guidelines.mdx
  • The process has been updated to reflect the PR-based workflow with status management via labels
  • This SEP document itself serves as an example of the new format

Vote

This SEP was accepted unanimously by the MCP Core Maintainers with a vote of 8 yes's, 0 no's and 0 absent votes on Friday December 28th, 2025 in a Discord poll.

SEP-1865 MCP Apps - Interactive User Interfaces for MCP

Final · Extensions Track · Created 2025-11-21

  • Status: Final
  • Type: Extensions Track
  • Created: 2025-11-21
  • Author(s): Ido Salomon (@idosal), Liad Yosef (@liadyosef), Olivier Chafik (@olivierchafik), Jerome Swannack (@jeromeswannack), Jonathan Hefner (@jonathanhefner), Anton Pidkuiko (@antonpidkuiko), Nick Cooper (@nickcooper), Bryan Ashley (@bryanashley), Alexi Christakis (@alexichristakis)
  • Sponsor: None (seeking sponsor)
  • PR: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865
The full extension specification is maintained in the [ext-apps repository](https://github.com/modelcontextprotocol/ext-apps/tree/main/specification).

Abstract

This SEP proposes an extension to MCP (per SEP-1724) that enables servers to deliver interactive user interfaces to hosts. MCP Apps introduces a standardized pattern for declaring UI resources via the ui:// URI scheme, associating them with tools through metadata, and facilitating bi-directional communication between the UI and the host using MCP's JSON-RPC base protocol. This extension addresses the growing community need for rich, interactive experiences in MCP-enabled applications, maintaining security, auditability, and alignment with MCP's core architecture. The initial specification focuses on HTML resources (text/html;profile=mcp-app) with a clear path for future extensions.

Motivation

MCP lacks a standardized way for servers to deliver rich, interactive user interfaces to hosts. This gap blocks many use cases that require visual presentation and interactivity that go beyond plain text or structured data. As more hosts adopt this capability, the risk of fragmentation and interoperability challenges grows.

MCP-UI has demonstrated the viability and value of MCP apps built on UI resources and serves as a community playground for the UI spec and SDK. Fueled by a dedicated community, it developed the bi-directional communication model and the HTML, external URL, and remote DOM content types. MCP-UI's adopters, including hosts and providers such as Postman, HuggingFace, Shopify, Goose, and ElevenLabs, have provided critical insights and contributions to the community.

OpenAI's Apps SDK, launched in November 2025, further validated the demand for rich UI experiences within conversational AI interfaces. The Apps SDK enables developers to build rich, interactive applications inside ChatGPT using MCP as its backbone.

The architecture of both the Apps SDK and MCP-UI has significantly informed the design of this specification.

However, without formal standardization:

  • Servers cannot reliably expect UI support via MCP
  • Each host may implement slightly different behaviors
  • Security and auditability patterns are inconsistent
  • Developers must maintain separate implementations or adapters for different hosts (e.g., MCP-UI vs. Apps SDK)

This SEP addresses the current limitations through an optional, backwards-compatible extension that unifies the approaches pioneered by MCP-UI and the Apps SDK into a single, open standard.

Specification

The full specification can be found at modelcontextprotocol/ext-apps.

At a high level, MCP Apps extends the Model Context Protocol to enable servers to deliver interactive user interfaces to hosts. This extension introduces:

  • UI Resources: Predeclared resources using the ui:// URI scheme
  • Resource Discovery: Tools reference UI resources via metadata
  • Bi-directional Communication: UI iframes communicate with hosts using standard MCP JSON-RPC protocol
  • Security Model: Mandatory iframe sandboxing with auditable communication

This specification focuses on HTML content (text/html;profile=mcp-app) as the initial content type, with extensibility for future formats.

As an extension, MCP Apps is optional and must be explicitly negotiated between clients and servers through the extension capabilities mechanism (see Capability Negotiation section in the full specification).

Rationale

Predeclared resources vs. inline embedding

UI is modeled as predeclared resources (ui://), referenced by tools via metadata. This allows:

  • Hosts to prefetch templates before tool execution, improving performance
  • Separation of presentation (template) from data (tool results), facilitating caching
  • Security review of UI resources

Alternatives considered:

  • Embedded resources: Current MCP-UI approach, where resources are returned in tool results. Although it's more convenient for server development, it was deferred due to the gaps in performance optimization and the challenges in the UI review process.
  • Resource links: Predeclare the resources but return links in tool results. Deferred due to the gaps in performance optimization.

Reusing MCP JSON-RPC instead of a custom protocol

Reuses existing MCP infrastructure (type definitions, SDKs, etc.). JSON-RPC offers advanced capabilities (timeouts, errors, etc.).

Alternatives considered:

  • Custom message protocol: Current MCP-UI approach with message types like tool, intent, prompt, etc. These message types can be translated to a subset of the proposed JSON-RPC messages.
  • Global API object: Rejected because it requires host-specific injection and doesn't work with external iframe sources. Syntactic sugar may still be added on the server/UI side.

HTML-only MVP

  • HTML is universally supported and well-understood
  • Simplest security model (standard iframe sandbox)
  • Allows screenshot/preview generation (e.g., via html2canvas)
  • Sufficient for most observed use cases
  • Provides a clear baseline for future extensions

Alternatives considered:

  • Include external URLs in MVP: This is one of the easiest content types for servers to adopt, as it's possible to embed regular apps. However, it was deferred due to concerns around model visibility, inability to screenshot content, and review process. It may effectively be supported with the SEP's new externalIframes capability.

Backward Compatibility

The proposal is an optional extension to the core protocol. Existing implementations continue working without changes.

Security Implications

Hosting interactive UI content from potentially untrusted MCP servers requires careful security consideration.

Based on the threat model, MCP Apps proposes the following mitigations:

  • Iframe sandboxing: All UI content runs in sandboxed iframes with restricted permissions
  • Predeclared templates: Hosts can review HTML content before rendering
  • Auditable messages: All UI-to-host communication goes through loggable JSON-RPC
  • User consent: Hosts can require explicit approval for UI-initiated tool calls

A full threat model analysis and mitigations are available in the full specification.

Reference Implementation

  • MCP-UI client and server SDKs support the patterns proposed in this spec.
  • ext-apps repository contains a prototype implementation by Olivier Chafik.

SEP-1913 Trust and Sensitivity Annotations

Draft · Standards Track · Created 2025-06-11

Imported from upstream. Proposed as modelcontextprotocol#1913 by @SamMorrowDrums on 2025-11-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.

Related Issue: #711

Abstract

This SEP proposes a unified security metadata model for MCP that combines trust annotations (data-in-transit) with action security metadata (tool behavior, including sensitivity categories). Together, these primitives allow clients and servers to track, propagate, and enforce trust boundaries on data as it flows through tool invocations, while also declaring what tools are allowed to do with that data.

The proposal introduces:

  1. Extended tool annotations with declarable annotation sets for pre-execution metadata via Tool Resolution
  2. Action security metadata describing input destinations, output sources, and real-world outcomes
  3. Result annotations for marking data characteristics in tool responses
  4. Request annotations for propagating trust context to servers
  5. Propagation rules ensuring trust markers persist across session boundaries
  6. Malicious activity signaling for security alerting and compliance

This pattern enables:

  • Deterministic enforcement through declarative contracts for tool behavior
  • Data exfiltration prevention by tracking when sensitive data flows to open-world destinations
  • Prompt injection defense by marking untrusted data sources
  • Compliance workflows via human-in-the-loop escalation for risky operations
  • Attribution tracking for audit trails and provenance
  • Pre-execution assessment via integration with Tool Resolution (#1862) for argument-derived annotations

Note: This SEP provides primitives, not a complete security solution. It enables host applications and registries to define situation-specific policies while avoiding over-specification of universal rules.

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 data; Final also needs conformance tests and the reference documentation chapter.

This draft incorporates the Action Security Metadata proposal (SEP-2061) by adding inputMetadata and returnMetadata to ToolAnnotations.

Motivation

The Problem with Untracked Data Flow

As MCP adoption grows, data flows across tool boundaries without standardized trust metadata:

1. Indirect Prompt Injection

Data from untrusted sources (web pages, emails, user-generated content) enters the context without markers indicating its origin. An attacker can embed instructions in this data that the model may execute.

Recent research demonstrates the severity: Trail of Bits showed how hidden content in GitHub issues can inject invisible prompt attacks that cause agents to insert backdoors in pull requests. The IBM/Google/Microsoft "Design Patterns for Securing LLM Agents" paper (arXiv:2506.08837) states:

"Once an LLM agent has ingested untrusted input, it must be constrained so that it is impossible for that input to trigger any consequential actions."

2. Data Exfiltration

Sensitive information (credentials, PII, proprietary data) can be passed to tools that write to external destinations. Without declared data classifications and action metadata, clients cannot enforce policies like "don't email private repo content to external addresses."

3. Cross-Organization Boundaries

In multi-organization scenarios, what's "internal" in one context may be "external" in another. Current MCP has no way to express or propagate these distinctions.

4. Compliance Requirements

Regulated industries (healthcare, finance) need audit trails and sensitivity classifications. Without standardized annotations, each implementation reinvents this wheel.

The Problem with Undeclared Tool Actions

MCP today treats all tool calls as equivalent at the protocol level. A tool that reads drafts and a tool that sends emails are indistinguishable (other than the most basic static Annotations), even though their security and privacy implications are radically different. Implementations fall back to heuristics or model inference to decide when to request user consent or block operations.

This creates concrete risks:

  • Prompt injection can trigger destructive or irreversible actions using untrusted input.
  • Data exfiltration can occur when sensitive data is passed to tools that transmit externally.
  • User consent cannot be meaningfully enforced without knowing a tool's real-world impact.
  • Distrust by default leads to confirmation fatigue and bad user experience.

Action security metadata provides a declarative contract that describes where inputs go, where outputs originate, and what outcomes the tool can cause. This complements trust annotations, which track data characteristics in transit.

Enabling Security Architecture Patterns

Recent research proposes several design patterns for secure LLM agents. Trust annotations provide the primitives these patterns need at protocol boundaries:

Pattern Trust Annotation Role
Plan-Then-Execute Planning phase uses openWorldHint and action metadata to determine allowed tool sequences
Map-Reduce Isolated agents return results with annotations; aggregation enforces sensitivity policies
Dual LLM Privileged LLM receives annotations on quarantined results to make display decisions
ShardGuard Coordination service uses annotations to select sanitization functions and opaque value policies
FIDES (IFC) Annotations serve as data labels for deterministic information flow control

Without protocol-level annotations, each security architecture must reinvent data classification.

Critical: Malicious Activity Detection

MCP servers are increasingly implementing detection for:

  • Prompt injection attempts
  • Secret/credential leakage
  • Anomalous request patterns

Without standardized signaling, these security findings are lost between tool boundaries. The maliciousActivityHint annotation is essential for:

  • User/admin alerting in compliance scenarios
  • Human-in-the-loop review and escalation
  • Audit logging of suspicious activity

Example: Email MCP Scenario

Consider a user asking an AI to: "Take the salary spreadsheet and email it to my accountant"

With annotations:

  1. File MCP resolves returnMetadata to indicate internal origin (and sensitivity: "financial" for this path)
  2. Email MCP declares inputMetadata.destination: "public" and outcomes: "irreversible"
  3. Policy triggers: block, escalate, or require user confirmation

Without trust annotations: The email goes through with no checks.

Specification

Terminology

Agent session (or simply "session" in this SEP): A client-side concept representing a sequence of user interactions where subsequent requests share the same agent context window. This is the conversation or task boundary as perceived by the user—for example, a chat thread in an AI assistant or a task execution in an agentic workflow.

This is distinct from MCP session, which refers to the protocol-level connection between a client and server (as defined in the MCP specification). Trust annotations propagate across an agent session, which may span multiple MCP sessions or interact with multiple MCP servers.

Annotation Types

Extended Tool Annotations

This SEP extends the existing ToolAnnotations interface with trust-related fields. This unified approach means:

  • Static declarations: Tools declare the set of annotations they might return in tools/list
  • Resolved declarations: tools/resolve selects concrete annotations for specific arguments
  • Response declarations: CallToolResult includes annotations describing actual returned data

In tools/list, annotation fields express possible values. For boolean hints, true means the hint may be set; false or omission means it will never be set. For enum-like fields (for example inputMetadata.outcomes), tools MAY declare a set of possible values using an array; tools/resolve and responses SHOULD return a single resolved value.

/**
 * Extended ToolAnnotations with trust and security metadata fields.
 * These fields follow a possible-set → resolved-value pattern:
 * tools/list declares potential annotations, tools/resolve resolves them.
 */
interface ToolAnnotations {
  // Existing hints (unchanged)
  title?: string;
  readOnlyHint?: boolean;
  destructiveHint?: boolean;
  idempotentHint?: boolean;
  openWorldHint?: boolean;

  // NEW: Trust extensions

  /**
   * Indicates detected or suspected malicious activity in the request
   * or response content. Clients SHOULD surface this to users and
   * MAY invoke elicitation for user review.
   */
  maliciousActivityHint?: boolean;

  /**
   * Attribution for data provenance. Lists sources contributing to
   * this response for audit and compliance purposes.
   *
   * Attribution strings SHOULD use URIs that meaningfully identify the source:
   * - MCP server origins: "mcp://server-name.domain/path/to/resource"
   * - HTTPS sources: "https://api.example.com/endpoint"
   * - Known local files with domain context: "local://WORKSTATION.corp.acme.com/path/file"
   * - Anonymous local files (unknown origin): "local:anonymous/path/file"
   * - Organization resources: "urn:org:acme:hr:salaries"
   *
   * Avoid opaque file:// URIs that don't convey meaningful provenance.
   */
  attribution?: string[];

  /**
   * Declarative contract for how the tool handles input data.
   */
  inputMetadata?: InputMetadata;

  /**
   * Declarative contract for the origin and sensitivity of outputs.
   */
  returnMetadata?: ReturnMetadata;
}

Note on openWorldHint: This existing field gains additional meaning in the trust context:

  • Tool-level (static): "This tool may access external systems"
  • Resolved (via tools/resolve): "This specific operation will access external/untrusted sources"
  • Response-level: "This response contains data from untrusted sources"

Examples of untrusted sources requiring openWorldHint: true:

  • Web page content (potential invisible prompt injection)
  • Email bodies (user-controlled content)
  • GitHub issues, PR descriptions, comments (attacker-controlled)
  • User-generated content from any platform
  • Search results from Bing, Google, or other web searches
  • Database query results with user-supplied data
Action Security Metadata

Action security metadata declares a tool's behavioral contract: where inputs may be stored or transmitted, where outputs originate, and what outcomes the tool can cause. These fields live on ToolAnnotations and follow the same possible-set → resolved-value pattern as other tool annotations:

  • Static declarations in tools/list describe the set of possible behaviors
  • Resolved declarations in tools/resolve select concrete values based on specific arguments

Unlike trust annotations (which describe actual data in transit), action metadata is declarative and describes what the tool is allowed to do with data.

interface InputMetadata {
  /**
   * Where input data may be stored or transmitted.
   */
  destination:
    | "ephemeral"
    | "system"
    | "user"
    | "internal"
    | "public"
    | Array<"ephemeral" | "system" | "user" | "internal" | "public">;

  /**
   * Sensitivity class(es) the tool may accept.
   */
  sensitivity: DataClass | DataClass[];

  /**
   * Real-world impact of invoking the tool.
   */
  outcomes:
    | "benign"
    | "consequential"
    | "irreversible"
    | Array<"benign" | "consequential" | "irreversible">;
}

interface ReturnMetadata {
  /**
   * Origin of returned data.
   */
  source:
    | "untrustedPublic"
    | "trustedPublic"
    | "internal"
    | "user"
    | "system"
    | Array<
        "untrustedPublic" | "trustedPublic" | "internal" | "user" | "system"
      >;

  /**
   * Sensitivity class(es) the tool may return.
   */
  sensitivity: DataClass | DataClass[];
}

type DataClass =
  | "none"
  | "user"
  | "pii"
  | "financial"
  | "credentials"
  | { regulated: { scopes: RegulatoryScope[] } };

type RegulatoryScope = string;

DataClass keeps sensitivity simple for common cases while allowing regulated data to be scoped. The regulated form declares applicable regimes; it does not assert compliance.

RegulatoryScope accepts arbitrary strings. The following are suggested examples for common regimes: GDPR, CCPA, HIPAA, GLBA, PCI-DSS, FERPA, COPPA, SOX.

For action metadata, enum-valued fields MAY be arrays in tools/list to indicate possible values; tools/resolve SHOULD return a single resolved value.

Destination

Specifies where input data may be stored or transmitted.

  • ephemeral — Data received will not be stored in any way.
  • system — Data is stored by the platform and not accessible to users or developers.
  • user — Data is stored and visible only to the end user.
  • internal — Data is stored and visible to a restricted internal audience.
  • public — Data may be transmitted to or stored in publicly accessible systems.
Outcomes

Describes the real-world impact of invoking the tool.

  • benign — No persistent state change outside the tool's execution context, or changes limited to private drafts that are not transmitted or shared.
  • consequential — Creates, updates, or deletes persistent state that is visible outside the tool's private context and can be programmatically reversed.
  • irreversible — Produces effects that cannot be undone through the same API or that trigger external side effects (e.g., sending messages, deleting data, publishing content).
Source

Indicates the origin of returned data.

  • untrustedPublic — Public but unverified sources.
  • trustedPublic — Public but curated or verified sources.
  • internal — Internal systems or datasets.
  • user — User-provided or user-owned data.
  • system — Generated or derived by the platform itself.
Result Annotations

Tools MAY include annotations in their results to describe data characteristics:

Clients MAY include annotations in requests to communicate trust context:

interface RequestAnnotations {
  /**
   * Whether prior context includes open-world (untrusted) data.
   */
  openWorldHint?: boolean;

  /**
   * Aggregated attribution from prior context.
   */
  attribution?: string[];
}

Protocol Changes

Tool Call Request Extension

The CallToolRequest params MAY include trust annotations:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "send_email",
    "arguments": {
      "to": "external@example.com",
      "subject": "Report",
      "body": "..."
    },
    "_meta": {
      "annotations": {
        "attribution": ["mcp://file-server.acme.local/hr/salaries.xlsx"]
      }
    }
  }
}
Tool Call Result Extension

The CallToolResult MAY include trust annotations at the response level:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "File contents..."
      }
    ],
    "_meta": {
      "annotations": {
        "attribution": ["mcp://file-server.acme.local/hr/salaries.xlsx"]
      }
    }
  }
}
Response Annotations Scope

Response annotations describe the entire response as a unit. Servers MUST aggregate annotations across all content:

  • openWorldHint: If any content is open-world, the response has that hint
  • maliciousActivityHint: If any content is flagged, the response has that hint
  • attribution: Union of all sources contributing to the response

Note: Per-item annotations (marking individual content items differently) are explicitly out of scope for this SEP. This simplifies implementation and avoids complexity around item-level tracking. If needed, per-item annotations can be proposed in a future SEP.

Schema Definitions

ToolAnnotations Extension

The following fields are added to the existing ToolAnnotations interface:

/**
 * Extended ToolAnnotations with trust and security metadata fields.
 */
export interface ToolAnnotations {
  // ... existing fields ...

  // Trust extensions
  maliciousActivityHint?: boolean;
  attribution?: string[];

  // Action security metadata
  inputMetadata?: InputMetadata;
  returnMetadata?: ReturnMetadata;
}

Array forms indicate possible values and are intended for tools/list (and tools/resolve when it cannot fully resolve). Responses should return single concrete values.

JSON Schema Additions

Add to the existing ToolAnnotations definition in schema.json, and introduce new $defs for action metadata:

{
  "ToolAnnotations": {
    "properties": {
      "maliciousActivityHint": {
        "description": "Indicates detected or suspected malicious activity.",
        "type": "boolean"
      },
      "attribution": {
        "description": "Sources contributing to this data for audit purposes.",
        "type": "array",
        "items": { "type": "string" }
      },
      "inputMetadata": { "$ref": "#/$defs/InputMetadata" },
      "returnMetadata": { "$ref": "#/$defs/ReturnMetadata" }
    }
  },
  "InputMetadata": {
    "type": "object",
    "properties": {
      "destination": {
        "oneOf": [
          {
            "type": "string",
            "enum": ["ephemeral", "system", "user", "internal", "public"]
          },
          {
            "type": "array",
            "items": {
              "type": "string",
              "enum": ["ephemeral", "system", "user", "internal", "public"]
            }
          }
        ]
      },
      "sensitivity": { "$ref": "#/$defs/DataClassOrArray" },
      "outcomes": {
        "oneOf": [
          {
            "type": "string",
            "enum": ["benign", "consequential", "irreversible"]
          },
          {
            "type": "array",
            "items": {
              "type": "string",
              "enum": ["benign", "consequential", "irreversible"]
            }
          }
        ]
      }
    },
    "required": ["destination", "sensitivity", "outcomes"],
    "additionalProperties": false
  },
  "ReturnMetadata": {
    "type": "object",
    "properties": {
      "source": {
        "oneOf": [
          {
            "type": "string",
            "enum": [
              "untrustedPublic",
              "trustedPublic",
              "internal",
              "user",
              "system"
            ]
          },
          {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "untrustedPublic",
                "trustedPublic",
                "internal",
                "user",
                "system"
              ]
            }
          }
        ]
      },
      "sensitivity": { "$ref": "#/$defs/DataClassOrArray" }
    },
    "required": ["source", "sensitivity"],
    "additionalProperties": false
  },
  "DataClassOrArray": {
    "oneOf": [
      { "$ref": "#/$defs/DataClass" },
      { "type": "array", "items": { "$ref": "#/$defs/DataClass" } }
    ]
  },
  "DataClass": {
    "oneOf": [
      {
        "type": "string",
        "enum": ["none", "user", "pii", "financial", "credentials"]
      },
      {
        "type": "object",
        "properties": {
          "regulated": {
            "type": "object",
            "properties": {
              "scopes": {
                "type": "array",
                "items": { "$ref": "#/$defs/RegulatoryScope" }
              }
            },
            "required": ["scopes"],
            "additionalProperties": false
          }
        },
        "required": ["regulated"],
        "additionalProperties": false
      }
    ]
  },
  "RegulatoryScope": {
    "type": "string",
    "examples": [
      "GDPR",
      "CCPA",
      "HIPAA",
      "GLBA",
      "PCI-DSS",
      "FERPA",
      "COPPA",
      "SOX"
    ]
  }
}

Integration with Tool Resolution

When used with the Tool Resolution mechanism (SEP #1862), trust annotations can be derived from arguments before tool execution. The same applies to action security metadata: servers MAY refine inputMetadata and returnMetadata based on the specific arguments. This enables clients to make policy decisions without invoking the tool.

Pre-execution Annotation Refinement

Servers supporting tools/resolve MAY return trust annotations in the resolved tool's annotations field. This follows the same pattern as other tool annotations: the tool declares the set of possible annotations in tools/list, and tools/resolve selects concrete values for the given arguments.

// Request: What annotations will apply if I read this file?
{
  "method": "tools/resolve",
  "params": {
    "name": "read_file",
    "arguments": { "path": "/internal/salaries.xlsx" }
  }
}

// Response: Server knows this path is internal and classified as financial
{
  "tool": {
    "name": "read_file",
    "annotations": {
      "readOnlyHint": true,
      "openWorldHint": false,
      "attribution": ["mcp://file-server.acme.local/hr/salaries.xlsx"],
      "returnMetadata": {
        "source": "internal",
        "sensitivity": "financial"
      }
    }
  }
}

This allows clients to:

  1. Apply policies before execution (block, escalate, require confirmation)
  2. Avoid unnecessary tool calls when policy would reject the result anyway
  3. Inform users upfront about action metadata implications (destination, source, outcomes)
Limitations for Dynamic Content

Pre-execution annotations work best when metadata can be determined from arguments alone (e.g., file paths, database names). For tools that search or aggregate content:

  • Search tools: Results may contain mixed sources—annotations cannot be fully determined until results are parsed
  • List operations: Individual items may have different sensitivity levels
  • Aggregation tools: Combined results inherit the maximum sensitivity of all inputs

In these cases, servers SHOULD return the narrowest possible set at resolve time, and MAY fall back to conservative single values if they cannot determine a precise resolution. Actual results should still include concrete annotations.

Action Metadata Examples
Read Email Drafts Action
{
  "name": "read_drafts",
  "description": "Read the user's email drafts.",
  "inputSchema": {
    "type": "object",
    "additionalProperties": false,
  },
  "annotations": {
    "inputMetadata": {
      "destination": "ephemeral",
      "sensitivity": "none",
      "outcomes": "benign",
    },
    "returnMetadata": {
      "source": "user",
      "sensitivity": "pii",
    },
  },
}
List Email Inbox Action
{
  "name": "list_inbox",
  "description": "List recent emails in the user's inbox.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "limit": { "type": "number" },
    },
    "required": [],
    "additionalProperties": false,
  },
  "annotations": {
    "inputMetadata": {
      "destination": "ephemeral",
      "sensitivity": "none",
      "outcomes": "benign",
    },
    "returnMetadata": {
      "source": "untrustedPublic",
      "sensitivity": ["pii", "user"],
    },
  },
}
Send Email Action
{
  "name": "send_email",
  "description": "Send an email on behalf of the user.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "to": { "type": "string", "format": "email" },
      "subject": { "type": "string" },
      "body": { "type": "string" },
    },
    "required": ["to", "subject", "body"],
    "additionalProperties": false,
  },
  "annotations": {
    "inputMetadata": {
      "destination": "public",
      "sensitivity": ["pii", "user"],
      "outcomes": "irreversible",
    },
    "returnMetadata": {
      "source": "system",
      "sensitivity": "none",
    },
  },
}

Behavior Requirements

Propagation Rules
  1. Boolean union: If openWorldHint is ever true, it MUST persist for the session
  2. Attribution accumulation: attribution lists SHOULD be merged (union) across context boundaries
Server Responsibilities
  1. Servers MAY emit trust annotations in responses when they have knowledge of data characteristics
  2. Servers MUST respect trust annotations in requests when making policy decisions
  3. Servers MAY refuse operations when trust annotations indicate policy violations
  4. Servers SHOULD include maliciousActivityHint when detecting suspicious patterns
  5. Servers SHOULD return early or invoke elicitation if potentially malicious activity is detected mid-way through exection of a task.
  6. Servers SHOULD declare possible inputMetadata/returnMetadata sets in tools/list, and MAY refine these via tools/resolve to concrete values when arguments are known.
Client Responsibilities
  1. Clients MUST propagate annotations according to propagation rules
  2. Clients MAY enforce basic policies (block, escalate, require confirmation)
  3. Clients SHOULD surface maliciousActivityHint to users
  4. Clients MAY present attribution data as part of confirmation dialogs
  5. Clients SHOULD consider action security metadata for policy and consent decisions, and MUST treat all annotations as untrusted hints unless the server is trusted.

Enforcement Examples

Example 1: Email with Open-World Data
Email MCPWeb MCPClientUserEmail MCPWeb MCPClientUserPropagate annotationsalt[Policy: Block external + open-world][Policy: Escalate]"Summarize this webpage and email it to accountant@external.com"tools/call (fetch URL)Result (openWorldHint: true, attribution: [url])tools/call (send email)annotations: {openWorldHint: true}Error: Cannot send untrusted content externallyRequires confirmation"Send open-world content to external address?"Confirmtools/call (with user confirmation)
Example 2: Malicious Content Detection
LLMWeb MCPClientUserLLMWeb MCPClientUserDetects prompt injectionin page content"Summarize this webpage"tools/call (fetch URL)Result (maliciousActivityHint: true,openWorldHint: true)⚠️ Warning: Potential malicious content detectedApply additional filteringContext with warnings attached

Policy Layer (Non-Normative)

This SEP intentionally does not specify policies. Example policies that hosts/registries might implement:

// Example policy rules (not part of spec)
const examplePolicies = {
  rules: [
    {
      name: "block-open-world-to-external",
      effect: "block",
      conditions: {
        and: [
          { fact: "request.annotations.openWorldHint", equals: true },
          {
            fact: "tool.annotations.inputMetadata.destination",
            equals: "public",
          },
        ],
      },
    },
    {
      name: "escalate-malicious",
      effect: "escalate",
      conditions: {
        fact: "response.annotations.maliciousActivityHint",
        equals: true,
      },
    },
    {
      name: "confirm-irreversible-actions",
      effect: "escalate",
      conditions: {
        fact: "tool.annotations.inputMetadata.outcomes",
        equals: "irreversible",
      },
    },
  ],
};

Rationale

Design Decisions

Why Defense-in-Depth, Not Binary Trust?

A common objection is: "Nothing external is truly safe—why bother with annotations?" This misunderstands the goal.

Trust annotations implement defense-in-depth, not binary security:

  1. LLMs should validate any input — absolutely correct
  2. MCP clients provide additional context — they know which servers are trusted, which outputs came from open-world tools, and can apply incremental validation
  3. Layered validation is more accurate — because the client has full context (session history, user permissions, organizational policies), it can make more precise decisions than the LLM alone

Even trusted MCP servers can return unpredictable data. Outputs from tools like web search or cloud storage queries may contain content the server cannot fully vet. Annotations enable servers to signal when results come from "signed" (known-safe) versus "unsigned" (untrusted) sources.

The point isn't to guarantee safety—it's to provide the primitives for layered defense.

Why Hints, Not Absolute Classifications?

Following the existing MCP annotation pattern (e.g., readOnlyHint, destructiveHint), we use "hint" suffix because:

  1. Servers may not have complete knowledge of data sensitivity
  2. Classifications vary by context (what's sensitive to one org may not be to another)
  3. Encourages layered defense rather than relying solely on annotations

Action security metadata uses DataClass for categorical declarations (PII, financial, regulated scopes). Implementations can map these categories to internal sensitivity levels if desired, but the protocol does not mandate a specific scale.

Why Not Information Flow Control (IFC) Labels?

@JustinCappos suggested IFC-style categorical labels instead of linear sensitivity. This is a valid approach with tradeoffs:

Approach Pros Cons
Categorical (DataClass) Simple, familiar, easy to implement Less expressive for complex flows
Full IFC labels (namespaced) More precise data compartmentalization Requires label namespace management

Microsoft's FIDES system demonstrates enterprise IFC for LLM workflows—Mark Russinovich noted that "the goal is to get data labels added to those protocols." Meanwhile, research into architectural approaches like ShardGuard suggests that compartmentalization (planning LLM + isolated execution) can achieve IFC-like properties without explicit labeling.

Key insight: These approaches are complementary, not competing. Architectural patterns (compartmentalization, isolation) work within a system, while protocol annotations enable security across system boundaries. A ShardGuard coordination service benefits from knowing incoming data is labeled financial or pii; a FIDES policy engine needs standardized labels to evaluate.

This SEP starts with a simple categorical approach. Future SEPs could add namespaced labels as an extension, potentially using reverse-DNS notation for organization-specific labels (e.g., com.mycorp.project.classified).

Why Separate openWorldHint for Data vs. Tools?

The existing Tool openWorldHint indicates the tool has unbounded access. The data openWorldHint indicates the data itself originates from or may be exposed to untrusted sources. These are distinct:

  • A tool accessing a public API has openWorldHint: true on the tool
  • Data fetched from that API has openWorldHint: true on the result
  • An internal tool might return data that was originally from external sources
Why Include maliciousActivityHint?

This is the most critical annotation. From the original proposal:

"If no other part of this RFC is adopted, the inclusion of maliciousActivityHint is essential."

Without this, security signals are lost between tool boundaries. Servers implementing threat detection have no standardized way to communicate findings.

Alternatives Considered

Alternative 1: Separate TrustAnnotations Type

Define a separate TrustAnnotations interface distinct from ToolAnnotations, placed in _meta.annotations on tools and responses.

Why rejected:

  • Creates unnecessary nesting (tool._meta.annotations vs tool.annotations)
  • The paradigm already fits: existing ToolAnnotations use a possible-set → resolved-value pattern
  • openWorldHint already exists in ToolAnnotations and naturally extends to trust context
  • Tool Resolution (SEP #1862) refines annotations based on arguments—trust fields benefit from this same pattern
  • Simpler implementation: one annotations field to check, not two
Alternative 2: Full IFC System

Implement complete Information Flow Control with declassifiers, label lattices, and taint tracking.

Why rejected (for now):

  • Significantly more complex to implement
  • Requires solving namespace management
  • May require certification for declassifiers
  • Can be added in future SEP if simpler approach proves insufficient
Alternative 3: Organization-Specific Schemas

Let each organization define their own annotation vocabularies (HIPAA, PCI, etc.).

Why rejected (for now):

  • No interoperability between organizations
  • Each MCP server must understand multiple schemas
  • Can be added as extension mechanism in future
Alternative 4: No Specification

Leave trust handling to individual implementations.

Why rejected:

  • Security signals lost at tool boundaries
  • No common language for policy enforcement
  • Every implementation reinvents the wheel
  • Compliance workflows impossible to standardize

Backward Compatibility

This proposal is fully backward compatible:

For Existing Servers

  • No changes required
  • Servers without annotation support continue working
  • Annotations are optional metadata in _meta

For Existing Clients

  • No changes required
  • Clients can ignore annotations they don't understand
  • Operations proceed normally without annotation support

Graceful Degradation

  • Missing annotations treated as unknown (not as "safe")
  • Clients should apply appropriate defaults for unlabeled data
  • No enforcement happens without annotation support

Security Implications

Trust Model

Trust annotations are metadata provided by servers and clients. They do not replace security controls:

  1. Malicious servers could omit or misrepresent annotations

    • Mitigation: Apply same trust evaluation as other server outputs
    • Defense in depth: Additional scanning at client/host level
  2. Annotation stripping if intermediaries don't propagate

    • Mitigation: Propagation rules are MUST requirements
    • Monitoring: Hosts can detect missing expected annotations
  3. False positives for maliciousActivityHint

    • Mitigation: Present as warning, not automatic block
    • User agency: Human-in-the-loop for final decisions
  4. Incorrect or malicious action metadata

    • Risk: Tools may understate destinations, sources, or outcomes
    • Mitigation: Treat metadata as hints, prefer conservative policies, and audit high-risk tools

Not a Complete Solution

This SEP explicitly acknowledges limitations:

  • Cannot prevent all data exfiltration (side channels exist)
  • Cannot ensure perfect classification (servers have limited knowledge)
  • Cannot replace proper access controls
  • Cannot prevent malicious servers from lying

The goal is reducing attack surface, not perfect security.

Privacy Considerations

Annotation propagation may reveal information:

  • Attribution lists show data sources
  • Sensitivity levels indicate data classification

Implementations should consider:

  • Whether to share annotations across MCP servers
  • Minimizing attribution detail where appropriate
  • User consent for annotation tracking

Reference Implementation

Server Implementation (TypeScript)

import { Server } from "@modelcontextprotocol/sdk/server/index.js";

const server = new Server(
  { name: "file-server", version: "1.0.0" },
  { capabilities: { tools: {} } },
);

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args, _meta } = request.params;

  // Check incoming annotations for policy decisions
  const incomingAnnotations = _meta?.annotations ?? {};

  if (name === "write_file") {
    const targetPath = args.path as string;

    // Example policy: Don't write open-world data to public locations
    if (incomingAnnotations.openWorldHint && isPublicPath(targetPath)) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        "Cannot write open-world data to public location",
      );
    }
  }

  if (name === "read_file") {
    const filePath = args.path as string;
    const content = await readFile(filePath);

    // Classify the file and return with annotations
    const annotations = classifyFile(filePath, content);

    return {
      content: [{ type: "text", text: content }],
      _meta: { annotations },
    };
  }
});

function classifyFile(path: string, content: string): TrustAnnotations {
  const annotations: TrustAnnotations = {
    attribution: [`file://${path}`],
  };

  // Example classification logic
  if (containsSecrets(content)) {
    annotations.maliciousActivityHint = true; // Secret in unexpected place
  }

  return annotations;
}

Client Implementation (TypeScript)

import { Client } from "@modelcontextprotocol/sdk/client/index.js";

class TrustTrackingClient {
  private sessionAnnotations: TrustAnnotations = {};

  async callTool(
    client: Client,
    name: string,
    args: Record<string, unknown>,
  ): Promise<CallToolResult> {
    // Include accumulated session annotations in request
    const result = await client.callTool({
      name,
      arguments: args,
      _meta: { annotations: this.sessionAnnotations },
    });

    // Propagate annotations from result
    const resultAnnotations = result._meta?.annotations ?? {};
    this.propagateAnnotations(resultAnnotations);

    // Handle malicious activity detection
    if (resultAnnotations.maliciousActivityHint) {
      await this.alertUser(
        "⚠️ Potential malicious content detected",
        resultAnnotations,
      );
    }

    return result;
  }

  private propagateAnnotations(newAnnotations: TrustAnnotations): void {
    // Boolean union for hints
    if (newAnnotations.openWorldHint) {
      this.sessionAnnotations.openWorldHint = true;
    }

    // Merge attribution
    if (newAnnotations.attribution) {
      this.sessionAnnotations.attribution = [
        ...new Set([
          ...(this.sessionAnnotations.attribution ?? []),
          ...newAnnotations.attribution,
        ]),
      ];
    }
  }

  private async alertUser(
    message: string,
    annotations: TrustAnnotations,
  ): Promise<void> {
    // Implementation depends on host UI
    console.warn(message, annotations);
  }
}

MCP Ecosystem

Complementary SEPs
  • SEP Tool Resolution (#1862): Provides argument-specific metadata refinement. Trust annotations can be returned via tools/resolve to enable pre-execution policy decisions (see "Integration with Tool Resolution" section).
  • SEP-2061 Action Security Metadata: Merged into this SEP as the inputMetadata and returnMetadata contract for tool behavior.
  • SEP Tool Requirements (#1385): Proposes a requires field for pre-execution capability/permission validation. Trust annotations complement this by describing what the result contains, while requires describes what's needed to call the tool.
  • SEP Security Schemes (#1488): OpenAI's proposal for per-tool OAuth scope declarations. Operates at the authentication layer; trust annotations operate at the data layer.

Several SEPs propose tool-level annotations that complement this SEP's data-level annotations:

SEP Scope Description Relationship
#1487 trustedHint Tool-level Marks whether a tool itself is trusted A trusted tool can still return untrusted data
#1560 secretHint Tool-level Marks tools whose outputs may contain sensitive data Static declaration; this SEP provides dynamic per-response granularity
#1561 unsafeOutputHint Tool-level Marks tool outputs potentially unsafe for prompt injection Similar to openWorldHint but at tool vs. data level

Key distinction: Tool-level hints say "this tool may return open-world or risky data" while data-level annotations say "this specific response contains open-world data or flagged content." Both are needed:

  • Tool hints enable static policy prechecks based on declared possible values
  • Data annotations enable precise dynamic enforcement
  • A single tool (e.g., read_file) can return both open-world and internal results depending on which file is read
Existing Primitives
  • Tool Annotations: Existing ToolAnnotations (readOnlyHint, destructiveHint, etc.) describe tool behavior. Trust annotations describe data characteristics.
  • Elicitation (spec): Provides mechanism for user confirmation that can be triggered by trust annotations.
  • mcp-context-protector (Trail of Bits): Wrapper server implementing trust-on-first-use and guardrails. Trust annotations would enhance its policy decisions.

Academic Research

  • Design Patterns for Securing LLM Agents (arXiv:2506.08837): Six patterns (Action-Selector, Plan-Then-Execute, Map-Reduce, Dual LLM, Code-Then-Execute, Context-Minimization) that benefit from data classification primitives.
  • CaMeL (Google DeepMind): Code-Then-Execute pattern with DSL for taint tracking. Trust annotations provide simpler protocol-level equivalent.
  • Information Flow Control: Academic IFC literature (decentralized label model, taint tracking) provides theoretical foundation. This SEP takes a pragmatic subset suitable for protocol adoption.

Industry Approaches

  • FIDES (Microsoft): Deterministic IFC for enterprise LLM workflows. Trust annotations align with their goal of protocol-level data labels.
  • ShardGuard: Compartmentalized execution architecture using planning LLM, opaque values, and coordination service. Demonstrates IFC-like properties achievable through architecture, complementing protocol-level annotations.

Open Questions

  1. Label namespaces: Should we support organization-specific labels? If so, what namespace format (reverse-DNS, URIs)? @olaservo suggested organizations could publish standardized schemas (e.g., HIPAA classifications, PCI compliance levels).

  2. Declassification: Who can remove sensitivity markers? Should there be a certification process for declassifiers? @JustinCappos raised the complexity of IFC declassification—perhaps compartmentalized execution (separate programs pre/post declassification) is simpler than in-program taint tracking.

  3. Taint persistence: If sensitive data is written to storage and read back, should labels persist? How? This is a known challenge in IFC systems and becomes messier with models that may retrain on labeled input.

  4. Model retraining: If a model retrains on labeled data, does the label apply to future outputs?

  5. Cross-server trust: Should annotations be shared between MCP servers, or kept client-side only? There are privacy implications to sharing sensitivity metadata across servers.

  6. Confidence levels: Should servers express confidence in their classifications?

  7. Granularity: Should annotations apply per-result-item rather than per-response? Resolved: Response annotations reflect the entire response (aggregated across content). Per-item annotations are out of scope for this SEP.

  8. Tool Resolution integration: Should tools/resolve be required to return the narrowest possible set for tools with dynamic content, or is this just guidance?

Acknowledgments

Thanks to the community discussion on #711, particularly:

  • @JustinCappos for IFC insights and label namespace discussion
  • @olaservo for organization-specific vocabulary suggestions
  • @Mossaka for the provenance/classification split proposal
  • @localden for shepherding the issue

SEP-1932 DPoP Profile for MCP

Draft · Standards Track · Created 2025-12-05

Note: This SEP defines an optional security extension for MCP that enables sender-constrained access tokens through DPoP (RFC 9449: Demonstrating Proof of Possession).

Imported from upstream. Proposed as modelcontextprotocol#1932 by @PieterKas on 2025-12-05, 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 defines DPoP (Demonstrating Proof of Possession) as an optional extension for the Model Context Protocol to support sender-constrained access tokens. The extension binds OAuth 2.0 access tokens to cryptographic key pairs controlled by MCP clients, requiring clients to demonstrate possession of the corresponding private key with each request. The proposal uses OAuth 2.0 Demonstrating Proof-of-Possession (DPoP) (RFC 9449).

Motivation

MCP’s current authorization model uses bearer tokens, which can be reused by an unauthorized party if the token is leaked, intercepted or exfiltrated. If an access token is intercepted, through network eavesdropping, compromised logs, or other means, an attacker use the token from a MCP client under its control to access protected MCP servers until the token expires.

DPoP addresses this by making tokens "sender-constrained" so that even if an attacker obtains an access token, they cannot use it without also possessing or controlling the correspondiong client's private key. This significantly raises the bar for attackers.

This extension is particularly valuable for:

  • High-security environments handling sensitive data
  • Long-lived access tokens
  • Deployments where token theft risk is elevated
  • Compliance requirements mandating proof of possession or sender constrained tokens

Specification

This extension requries OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP) (RFC 9449) for use in the Model Context Protocol. This proposal does not define extensions to (RFC 9449) to maximise interoperability, simplify implementation and accelerate deployment while preserving the security properties to minimise the risks that arise from token exfiltration and replay required for MCP clients and servers. It does not preclude the use of the extension mechanisms defined in RFC 9449.

Stateless DPoP Proof Replay Protection

RFC 9449 Section 11.1 provides specific guidance on replay protection mechanisms that adress the risks of a DPoP Proofs being replayed.

MCP servers that is not capable of keeping state or perform global jti tracking provides DPoP proof replay protection by enforcing short iat acceptance windows of +/- 5 minutes and standard RFC 9449 claim validation. A stateless MCP server may provide additional replay protection by using a server supplied nonce as defined in RFC 9449 (e.g. by using an encrypted timestamp as the nonce value which can be decrypted, parsed and verified to be within an acceptable time window when returned in DPoP Proof) (e.g. by encrypting a timestamp using an Authenticated Encryption with Associated Data (AEAD) scheme which can be decrypted, parsed and verified to be within an acceptable time window when returned in DPoP Proof).

Rationale

The purpose of this extension is to define a mechanism to sender constrain OAuth Access Tokens in MCP deployments using OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP) (RFC 9449).

This SEP intentionally adopts DPoP as defined in RFC 9449, without introducing MCP-specific claims, request body digests, or additional proof material. This design choice reflects a deliberate trade-off in favor of interoperability, deployability, and alignment with existing OAuth 2.0 security models.

RFC 9449 already provides the core security properties for sender constraining OAuth Access Tokens. When combined with TLS transport-layer security and appropriate token and proof lifetimes, these mechanisms are sufficient to significantly reduce the risks associated with token exfiltration and replay in MCP deployments, even if the MCP server is not capable of maintaining global state.

Backward Compatibility

This is an optional extension with no backward compatibility concerns. Existing MCP implementations continue to work unchanged. RFC 9449 is designed for graceful coexistence with existing bearer token deployments. Implementations adopting DPoP can do so incrementally:

  • Authorization servers can support both bearer and DPoP tokens simultaneously
  • Clients can be upgraded independently
  • MCP servers can accept both token types during migration

Security Implications

This extension protects against:

  • Token theft and replay: Even if tokens are intercepted, attackers cannot use them without access or control over the private key
  • Proof theft and replay: DPoP Proof replay mechanisms protect against proof replay. If an MCP server is unable to maintain state, short-lived DPoP proofs limits the exposure window while the server supplied nonce provides the MCP server to force the generation of a fresh DPoP proof to further limit the risk that a DPoP proof is being replayed.
  • Network-based attacks: DPoP significantly reduces value of network eavesdropping.

DPoP does not protect against:

  • Client compromise: If an attacker gains full access to the client system, they can access the private key and generate valid proofs
  • Authorization-level attacks: DPoP binds tokens to clients but doesn't address confused deputy or privilege escalation vulnerabilities

Reference Implementation

A reference implementation will be provided demonstrating:

  • Client-side DPoP proof generation
  • MCP server-side proof validation
  • Authorization server metadata configuration
  • Example integration with existing MCP implementations

Links will be added when implementations are available.

Performance Implications

DPoP introduces modest performance overhead:

  • Client-side: Additional cryptographic signing operation per request
  • Server-side: Additional cryptographic signature verification per request
  • Network: Slightly larger HTTP headers due to DPoP proof JWT
  • Latency: An extra round-trip for the first MCP server supplied nonce.

These costs are generally negligible compared to overall request processing time.

Testing Plan

Implementations should cover:

  1. Proof generation: Correct formatting of all required claims including content digest.
  2. Content digest calculation: Accurate SHA-256 hashing and base64 encoding.
  3. Validation logic: All validation steps per specification.
  4. Error handling: Appropriate HTTP 401 responses with error details.
  5. Time window: Correct handling of iat claims at boundaries.
  6. Algorithm support: All declared algorithms work correctly.

Test vectors will be provided in the reference implementation.

Alternatives Considered

HTTP Message Signatures

We considered RFC 9421 (HTTP Message Signatures) as an alternative. However, the use of HTTP Message signatures for sender constraining tokens have not been standardised by the OAuth community.

Custom content signing extension

An earlier draft of this SEP proposed a custom extension using the mechanisms defined in RFC 9449 that cryptographically bound the request body to the DPoP proof. This approach was not selected because it significantly increased implementation and operational complexity without providing sufficient additional security benefit. The final design therefore adopts DPoP as defined in RFC 9449 to maximize interoperability, simplicity, and deployability.

Open Questions

  1. Algorithm recommendations: Should future revisions mandate specific algorithms (e.g., ES256 minimum)?
  2. Validity window tuning: Should different validity windows be allowed for different security contexts?

Acknowledgments

This specification builds upon the OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP) (RFC 9449) and was refined based on input from Brian Campbell, one of the authors of (RFC 9449).

Additional details

A more detailed proposal can be viewed here: https://github.com/modelcontextprotocol/ext-auth/blob/pieterkas-dpop-extension/specification/draft/dpop-extension.mdx

SEP-1933 Workload Identity Federation

Draft · Standards Track · Created 2025-12-05

Imported from upstream. Proposed as modelcontextprotocol#1933 by @PieterKas on 2025-12-05, 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 defines Workload Identity Federation as an optional extension for the Model Context Protocol, enabling autonomous MCP clients to access MCP servers using existing workload identity credentials without requiring separate client registration or long-lived credentials. The proposal combines the OAuth 2.0 JWT Bearer grant type (RFC7523) with OpenID Connect Discovery 1.0 to allow MCP clients operating as workloads, such as those deployed in Kubernetes or cloud environments, to present platform-issued JWT credentials directly to authorization servers. This eliminates the need for client secrets, client identifiers, or separate credential management while maintaining security through issuer trust relationships and claim-based authorization. This docuemnt formalize a pattern that is broadly adopted in industry to increase interoperability and allow for scaled deployments while improving the overall security profile of deployments.

Motivation

Current MCP authorization mechanisms require clients to either participate in interactive OAuth flows or manage client credentials (secrets or private keys) for machine-to-machine authentication. For autonomous workloads, such as MCP clients running in Kubernetes, cloud platforms, or SPIFFE-enabled environments, this creates several operational challenges:

  1. Credential Management Overhead: Each MCP client deployment requires provisioning, storing, and rotating separate credentials for every MCP server it needs to access.
  2. Security Surface Expansion: Long-lived client secrets or private keys represent additional attack surfaces that must be protected.
  3. Platform Integration Gaps: Modern workload platforms already provide robust identity mechanisms (Kubernetes service accounts, cloud instance identities, SPIFFE identities), but MCP cannot leverage these existing credentials
  4. Operational Complexity: Separate credential lifecycles for platform identity and MCP access create coordination challenges and increase operational burden.

Workloads in modern deployment environments already possess cryptographically verifiable identity credentials issued by their platforms. The current MCP specification provides no mechanism to leverage these existing identities, forcing operators to implement parallel credential management systems. This SEP addresses this gap by defining how workloads can federate their existing platform identities to access MCP servers through trust relationships configured at the authorization server level.

Specification

Workload Identity Federation combines using a JWT from a trusted issuer as an authorization grant as defined in RFC7523 with automated issuer discovery as described in OpenID Connect Discovery 1.0 to obtain the RFC7517 JWK Key set of the trusted issuer which is used to validate the JWT authorization grant before the authorization server issues an access token.

As a prerequisite for this flow, the authorization server is configured with a set of issuers (defined by a URL), which are considered 'trusted issuers'. The nature of that trusted relationship and the mechanism by which the set is populated or maintained is out of scope of this document and left up to the implementer. JWTs not issued by these trusted issuers can be safely rejected by the authorization server.

The complete Workload Identity Federation flow proceeds as follows:

MCP ServerJWK SetLocationOIDCDiscoveryEndpointAuthorizationServerMCP ClientMCP ServerJWK SetLocationOIDCDiscoveryEndpointAuthorizationServerMCP Client2. Construct OpenID Provider Discovery Endpoint URL5. Extract `jwks_uri`8. Validate JWTAuthorization Grant11. Validate Access Token1. Request Access Token3. Request OpenID Provider Configuration Document4. Return OpenID Provider Configuration Document6. Request JWK Set7. Return JWK Set9. Return Access Token10. MCP Server Access12. MCP Server Response

Flow Steps

  1. Request Access Token: The MCP client requests an access token through a POST request to the authorization server's token endpoint using the JWT Bearer grant type as specified in RFC 7523 Section 2.1. The request includes:

    • grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer
    • assertion: The client's workload JWT credential
    • resource: The canonical URI of the target MCP server (as defined in the baseline MCP Authorization specification)
  2. Construct OpenID Provider Discovery Endpoint URL: The authorization server extracts the iss (issuer) claim from the JWT and determines if it is a trusted issuer. If the issuer is trusted, the authorization server constructs the OpenID Provider Discovery Endpoint URL from the issuer claim as described in OpenID Connect Discovery 1.0, Section 4.

  3. Request OpenID Provider Configuration Document: The authorization server requests the OpenID Provider Configuration Document as described in Section 4.1 of OpenID Connect Discovery 1.0.

  4. Return OpenID Provider Configuration Document The OpenID Provider Configuration Document is returned as described in Section 4.2 of OpenID Connect Discovery 1.0.

  5. Extract jwks_uri: The authorization server validates the OpenID Provider Configuration Document as described in Section 4.3 of OpenID Connect Discovery 1.0 and extracts the jwks_uri configuration information.

  6. Request JWK Set: The authorization server retrieves the RFC7517 JWK set containing the issuer's public keys from the TLS protected location specified by jwks_uri configuration information (the jwks_uri MUST use the https scheme).

  7. Return JWK Set: The JWK set is returned from the TLS protected location.

  8. Validate JWT Authorization Grant: The authorization server validates the JWT authorization grant according to RFC 7523 Section 3, with considerations specified in this document. This includes verifying the signature using the retrieved public keys and validating that the JWT claims meet the authorization server's configured trust and authorization requirements.

  9. Return Access Token: If JWT validation succeeds, the authorization server issues an access token suitable for accessing the specified MCP server and returns it to the MCP client per RFC 7523 Section 2.1.

  10. MCP Server Access: The MCP client makes a JSON-RPC request to the MCP server, including the access token rerieved in Step 9. The accesstoken is included in the Authorization header using the Bearer authentication scheme as specified in the baseline MCP Authorization specification.

  11. Validate Access Token: The MCP server validates the access token according to the baseline MCP Authorization specification, including verifying the token was issued for the MCP server as the intended audience.

  12. MCP Server Response: If the token is valid and the MCP client is authorized, the MCP server processes the request and returns the JSON-RPC response.

Trust Relationships

For functional Workload Identity Federation, trust must be established at multiple levels:

  1. Issuer Trust: The authorization server MUST trust the JWT issuer
  2. Workload Trust: The authorization server MUST trust the specific workload based on claims within the JWT

These two trust relationships serve different purposes and SHOULD be managed independently.

Issuer Trust Relationship

Issuer trust represents the foundational level of trust and determines whether JWT bearer tokens from a particular issuer are accepted. This trust relationship is between the authorization server and the JWT issuer only.

Issuer trust is typically established at a global or tenant-wide level and is subject to organizational policy and governance controls. Changes to issuer trust affect all workloads associated with that issuer simultaneously.

The mechanism for establishing issuer trust is implementation-specific and outside the scope of this specification. Common approaches include:

  • Maintaining an allowlist of trusted issuer URLs
  • Configuring issuer metadata endpoints for automatic key discovery
  • Associating issuers with specific tenants or organizational units
Workload Trust Relationship

Workload trust builds upon issuer trust and focuses on the relationship between the authorization server and individual workloads. Once the JWT bearer token is validated (signature verified and issuer trusted), the authorization server must determine whether the specific workload identified in the JWT claims should receive an access token.

Workload trust is typically established individually based on:

  • The sub (subject) claim identifying the workload
  • Additional claims such as namespace, tenant identifier, or service account
  • Combinations of multiple claims forming a compound identity

This trust is subject to different policy and governance controls than issuer trust, often allowing more granular per-workload authorization decisions.

Multi-Tenancy Considerations

When workload platforms or authorization servers support multiple tenants, additional care must be taken to prevent privilege escalation across tenant boundaries.

Authorization servers SHOULD:

  • Rely on JWT issuing keys bound to a single tenant of the workload platform, rather than a single issuing key for all tenants in a platform.
  • Use specific JWT claims to prevent any JWT signed by the issuer from being used to impersonate any principal.
  • NOT solely rely on JWT claims that can be controlled by any tenant.
  • MAY rely on a "tenant" claim if the claim value is issuer-controlled and corresponds to a single tenant.

See the Security Considerations section for additional details on privilege escalation prevention.

Rationale

The design leverages established OAuth and OpenID Connect standards (RFC 7523 JWT Bearer grants and OIDC Discovery) rather than creating MCP-specific mechanisms, ensuring broad interoperability with existing identity infrastructure and authorization servers. By using platform-issued JWTs directly, this approach leverages installed infrastrucutre and eliminates operational overhead while maintaining strong security through cryptographic verification, and the use of automated issuer discovery supports key rotation and reduces manual configuration burden in dynamic workload environments.

Design Decisions

Why JWT Bearer Grant Type?

The JWT Bearer grant type (RFC 7523) is a well-established OAuth 2.0 extension specifically designed for workload-to-service authentication scenarios. It provides:

  • Standardized protocol.
  • Clear validation requirements.
  • Broad implementation support across authorization servers.
  • Natural alignment with existing workload identity systems.

Why OpenID Connect Discovery?

OpenID Connect Discovery provides a standardized mechanism for:

  • Automatic discovery of issuer configuration.
  • Dynamic retrieval of signing keys via jwks_uri.
  • Reduced manual configuration burden.
  • Support for key rotation without service disruption.

This automation is critical for operational simplicity in dynamic workload environments where issuers may rotate keys frequently.

OpenID Connect Discovery is chosen over OAuth 2.0 Authorization Metadata Discovery since it is already broadly supported by workload identity issuance infrastructurem, specifically Kubernetes and SPIFFE based systems (neither support OAuth 2.0 Authorization Metadata Discovery).

Additional Details A detailed proposal is described at https://github.com/modelcontextprotocol/ext-auth/blob/pieterkas-wif-extension/specification/draft/workload-identity-federation.mdx

Alternatives Considered

Client Credentials Flow with Dynamic Registration

We considered extending the Client Credentials extension with dynamic client registration. This was rejected because:

  • It still requires managing per-client credentials (client secrets or private keys).
  • Dynamic registration adds complexity without solving the credential management problem.
  • Workloads already have cryptographically verifiable identities that would be unused.

Backward Compatibility

There are no backward compatibility concerns. This SEP introduces a new optional authorization extension and does not modify existing MCP authorization mechanisms. Implementation of this extension is entirely optional. Systems not requiring workload identity federation can continue using existing authorization mechanisms without modification.

Security Implications

This SEP builds on established security models from OAuth 2.0, OIDC Discovery, and JWK-based key distribution. Detailed and authoritative security guidance is available in:

Implementers MUST review these documents when deploying Workload Identity Federation.

JWT Validity and Lifetime

Workload Identity Federation relies on platform-provisioned, short-lived workload credentials (such as Kubernetes service account tokens, SPIFFE SVID-JWTs, or cloud-issued workload tokens). These credentials typically have strong issuance guarantees and short lifetimes, reducing the window in which a compromised token can be misused. Because these JWTs act as authorization grants, implementations MUST treat them as highly sensitive.

JWT validity SHOULD be set to the shortest possible duration allowable by overall system availability constraints. Short-lived JWTs (on the order of seconds to minutes) reduce the window of opportunity for stolen JWTs to be misused.

A secure channel (e.g., TLS) MUST be used when providing a JWT for Workload Identity Federation. JWTs SHOULD be protected from unauthorized access using operating system or platform access controls.

Identity Translation and Privilege Escalation

The translation of workload JWT claims to MCP server access tokens represents a critical trust boundary. Improper configuration of this translation can lead to privilege escalation attacks, particularly in multi-tenant environments. See the "Multi-Tenancy Considerations" section for specific guidance on multi-tenant deployments.

The following recommendations apply to all deployments:

  • Configuration SHOULD NOT permit the transcription of JWT claims to access token claims without additional validation
  • Authorization servers SHOULD implement explicit allowlists or policies defining which workload identities can access which MCP servers
  • Authorization servers SHOULD log and audit all token issuance decisions for security monitoring

Issuer Key Retrieval Security

Authorization servers MUST validate issuer URLs and ensure they use HTTPS. Authorization servers SHOULD implement the following protections:

  • Maintain an allowlist of permitted issuer domains to prevent malicious issuer discovery
  • Validate TLS certificates when retrieving issuer configuration and JWKs
  • Implement reasonable timeouts and rate limits on issuer discovery requests
  • Cache issuer configurations and JWKs appropriately to reduce attack surface
  • Monitor for issuer configuration changes that might indicate compromise

Reference Implementation

A reference implementation is required before this SEP can achieve "Final" status. The reference implementation should demonstrate:

  1. Authorization Server: Token endpoint accepting JWT bearer grants with:

    • OpenID Connect Discovery integration
    • JWT validation using retrieved JWKs
    • Configurable issuer and workload trust policies
  2. MCP Client: Workload client that:

    • Obtains platform-issued JWT credentials
    • Use this specification to present a JWT and receive an access token
    • Uses access token for MCP server requests
  3. Test Cases: Including:

    • Successful token issuence when a valid JWT is presented
    • Rejection of untrusted issuers
    • Rejection of unauthorized workloads
    • JWT signature validation
    • Replay attack prevention
    • Multi-tenant isolation

Performance Implications

The OpenID Connect Discovery process adds latency to the first token request for each issuer. However:

  • Issuer configurations and JWKs SHOULD be cached to minimize repeated discovery.
  • Discovery overhead is incurred once per issuer, not per workload.
  • The cost is comparable to other federated authentication mechanisms.

For high-throughput scenarios, authorization servers should implement:

  • Aggressive caching of issuer metadata and JWKs with appropriate TTLs.
  • Connection pooling for issuer discovery requests.
  • Pre-warming of cache for known trusted issuers.

Open Questions

  1. Token Refresh: Should workload clients be allowed to obtain refresh tokens, or should they always present fresh workload JWTs for new access tokens?

  2. Issuer Metadata Caching: Should the specification provide guidance on cache TTLs for issuer configurations and JWKs (OpenID Connect Discovery 1.0 does not define caching limits)?

  3. Error Response Detail: How much detail should authorization servers provide in error responses when JWT validation fails (for security vs. debuggability tradeoffs)? Is there anything we should add beyond (RFC7523)?

  4. Additional freshness and replay requirements Should authorization servers imposed additional token freshness (exp, iat, jti) and enforce strict replay protection for JWTs accepted as assertion grants?

  5. Guidance on Token Logging Should this specification give guidance to avoid logging, storing, or otherwise persisting JWT assertions (acces token logging is discouraged in existing RFCs, but not JWT assertion logging)?

Acknowledgments

This proposal draws on established OAuth 2.0 and OpenID Connect standards and incorporates established patterns from:

  • Kubernetes: Service account token projection provides JWTs that can be used for workload identity federation to cloud providers.
  • SPIFFE: The SPIFFE Verifiable Identity Document (SVID) format uses JWTs for workload identity in zero-trust environments.
  • Cloud Providers: AWS, Azure, and GCP all support workload identity federation using JWT bearer grants.

The proposal has been refined based on input from Brian Campbell, Den Delimarsky, Wills Dawson and Darin McAdams.

SEP-2028 Automatic \_meta to HTTP Header Forwarding for Distributed Tracing

Draft · Standards Track · Created 2025-12-29

  • Status: Draft
  • Type: Standards Track
  • Created: 2025-12-29
  • Author(s): Manah Khalil (@monahk)
  • PR: #2028

Imported from upstream. Proposed as modelcontextprotocol#2028 by @monahk on 2025-12-29, 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 standardized mechanism for MCP servers to automatically forward _meta fields from client requests to downstream HTTP calls using a group-based policy system. Header groups (e.g., trace-context, baggage) can be configured with policies (clear-and-use-meta, prefer-meta, ignore-meta) that control forwarding behavior and conflict resolution. W3C Trace Context fields are forwarded by default. Currently, _meta reaches the MCP server but is not forwarded, breaking end-to-end observability across the MCP boundary.

Motivation

Modern distributed systems rely on trace context propagation (OpenTelemetry, W3C Trace Context) for observability. The MCP protocol already supports passing metadata via _meta in request.params (established in Issue #246). However, there is no mechanism for MCP servers to forward this context to downstream services.

The Problem

When an MCP client sends a tool call with trace context:

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": { "location": "Dallas" },
    "_meta": {
      "traceparent": "00-e796ccb939d95b7c54d523095a9bd3b4-e515588135c1c901-01",
      "correlation_id": "mcp-webchat-1767041682815"
    }
  }
}

The MCP server receives _meta in request.params._meta, but when it makes HTTP calls to external APIs (e.g., AccuWeather, GitHub API, ClickUp API), no trace headers are forwarded:

[EXTERNAL API REQUEST RECEIVED]
Headers:
  accept: application/json
  user-agent: axios/1.13.2
  host: api.example.com

>>> NO trace context headers found <<<
>>> _meta is NOT being forwarded <<<

This was verified by intercepting HTTP traffic from an unmodified MCP server (@timlukahorstmann/mcp-weather) calling a mock API endpoint.

Prior Work

  • Issue #246: Established the _meta convention for client-to-server metadata propagation. Marked as "completed" because the protocol layer works.
  • PR #666 (typescript-sdk): Proposed automatic HTTP header forwarding with X-MCP-* prefix. Rejected with rationale: "This PR introduces transport-specific concerns (HTTP headers) into the MCP SDK without protocol-level support."
  • Discussion #801: Ongoing discussion about client-level request identifiers.

The rejection of PR #666 explicitly called for protocol-level support before SDK implementation. This SEP provides that protocol-level foundation.

Why This Matters

Without trace context propagation:

  • Distributed traces break at the MCP boundary
  • Debugging multi-service requests involving MCP tools is difficult
  • Organizations cannot correlate MCP tool calls with downstream API metrics
  • Enterprise observability requirements cannot be met

Specification

1. Group-Based Header Forwarding

This specification uses a group-based model where semantically related headers are treated as a unit. Each group has a single policy setting that controls both forwarding and conflict resolution.

1.1 Policies
Policy Behavior Use When
clear-and-use-meta If ANY header in the group exists in _meta, discard ALL existing headers in that group, then forward ALL from _meta Headers are tightly coupled (e.g., traceparent + tracestate)
prefer-meta For each header: if it exists in _meta, overwrite existing; otherwise preserve existing Headers are independent (e.g., baggage)
ignore-meta Never forward from _meta for this group; existing headers are always preserved Disable forwarding entirely
1.2 Behavior Matrix
Policy _meta has header? Existing header? Result
clear-and-use-meta Yes (any in group) Yes Clear ALL existing in group, forward ALL from _meta
clear-and-use-meta Yes (any in group) No Forward from _meta
clear-and-use-meta No Yes Keep existing
prefer-meta Yes Yes Overwrite that specific header with _meta value
prefer-meta Yes No Forward from _meta
prefer-meta No Yes Keep existing
ignore-meta Yes Yes Keep existing, ignore _meta
ignore-meta Yes No Do nothing
ignore-meta No Yes Keep existing
1.3 Predefined Groups

SDKs MUST support these predefined header groups:

Group Name Headers Default Policy Required Rationale
trace-context traceparent, tracestate clear-and-use-meta ["traceparent"] Tightly coupled per W3C Trace Context spec; tracestate is keyed to traceparent
baggage baggage prefer-meta — Independent W3C standard; can be replaced independently

Note: W3C Trace Context and W3C Baggage are separate standards that can version independently, hence they are separate groups with independent policies.

1.4 Custom Fields

All other _meta fields are NOT forwarded by default. Users can define custom groups to forward them:

_meta Field HTTP Header Default
correlation_id X-MCP-Correlation-Id Not forwarded
tenant_id X-MCP-Tenant-Id Not forwarded
{custom} X-MCP-{Custom} Not forwarded

Rationale: Custom fields could contain sensitive data (tenant IDs, user context, etc.) and should not leak to third-party APIs without explicit configuration.

2. Server SDK Behavior

2.1 Automatic Header Forwarding

MCP Server SDKs MUST automatically forward headers from _meta to outbound HTTP requests based on group policies. By default:

  • trace-context group (traceparent, tracestate) uses clear-and-use-meta policy with required: ["traceparent"]
  • baggage group uses prefer-meta policy
  • Custom fields are not forwarded unless explicitly configured

This behavior requires no code changes from MCP server authors.

2.2 Configuration API

SDKs MUST support configuring header groups via headerGroups:

const server = new Server({
  headerGroups: {
    // Override predefined group policy
    baggage: { policy: "ignore-meta" }, // Disable baggage forwarding

    // Per-header validators for W3C Trace Context format
    "trace-context": {
      policy: "clear-and-use-meta",
      validators: {
        traceparent: isValidTraceparent,
        tracestate: isValidTracestate,
      },
      // Presence evaluated after validation
      required: ["traceparent"],
    },

    // Define custom group for Datadog (with required header)
    datadog: {
      headers: [
        "x-datadog-trace-id",
        "x-datadog-parent-id",
        "x-datadog-sampling-priority",
      ],
      policy: "clear-and-use-meta", // These headers are coupled
      required: ["x-datadog-trace-id"], // Group skipped if trace-id missing
    },

    // Define custom group for internal headers
    internal: {
      headers: ["x-tenant-id", "x-request-id", "x-correlation-id"],
      policy: "prefer-meta", // These are independent
    },
  },
});
2.3 Context Accessor

Tool handlers MUST have access to _meta from the request context for manual use:

server.tool("get_weather", async (params, context) => {
  const meta = context.meta; // Access to full _meta object
  // Use manually if needed
});
2.4 Manual Header Extraction Utility

SDKs SHOULD provide a utility for manual header extraction when needed:

import { extractHttpHeaders } from "@modelcontextprotocol/sdk/utils";

const headers = extractHttpHeaders(context.meta, {
  groups: ["trace-context", "internal"], // Extract specific groups
});

3. Field Validation and Processing Order

SDKs MUST process _meta fields in the following order:

  1. Validation: Validate all _meta field values (see below)
  2. Per-header validation: If validators are configured for the group, invoke each header's validator; drop/ignore individual headers that fail validation
  3. Required check: For each group with a required array, verify all required headers are present (and not dropped by validation); skip the group entirely if any are missing
  4. Policy application: Apply the group's policy to determine final headers

Rationale for ordering: Per-header validators run before required presence checks so that malformed values are caught and dropped early. If a required header is dropped due to validation failure, the group is skipped entirely—preventing forwarding of an incomplete set.

3.1 Validation Rules

SDKs MUST validate _meta fields before forwarding:

  • Values MUST be strings
  • Values MUST contain only visible ASCII characters (0x21-0x7E) and spaces (0x20)
  • Values MUST NOT contain control characters or newlines (prevents header injection)
  • Individual values SHOULD be limited to 256 characters
  • Total _meta size SHOULD be limited to 8KB

Invalid fields MUST be silently dropped (not cause errors).

4. Configuration Options

4.1 Header Group Configuration
Property Type Required Description
policy "clear-and-use-meta" | "prefer-meta" | "ignore-meta" Yes Forwarding and conflict policy
headers string[] No* Headers in this group
required string[] No Headers that MUST be present in _meta for group to forward; if any missing, group is skipped
validators Record<string, (value: string) => boolean> No Per-header validation functions; headers that fail validation are dropped

* headers is required for custom groups, optional when overriding predefined groups.

4.2 Predefined Group Defaults
Group Default Policy Required
trace-context clear-and-use-meta ["traceparent"]
baggage prefer-meta —

5. Transport Considerations

This specification applies to MCP servers making outbound HTTP/HTTPS requests. For other transports:

  • gRPC: Map to gRPC metadata
  • Message Queues: Map to message headers/properties
  • WebSocket: Not applicable (no standard header mechanism)

6. Conflict Resolution and Group Integrity

Conflict resolution is handled by the group's policy setting (see Section 1.1 and 1.2).

6.1 Group Integrity

By design, the SDK never mixes headers from different sources within a group. Groups stay intact:

  • clear-and-use-meta: ALL from _meta, or ALL from existing (if _meta has nothing or required headers are missing)
  • prefer-meta: Per-header replacement, but no partial mixing within a single header
  • ignore-meta: ALL from existing, _meta ignored entirely
6.2 Manual Mixing (Advanced)

To mix sources within a group (e.g., traceparent from _meta + tracestate from existing), the server must:

  1. Set policy: "ignore-meta" for that group
  2. Manually read from context.meta in the tool handler
  3. Construct headers explicitly

This is intentional—mixing coupled headers like traceparent + tracestate from different sources can break tracing. If you need to do it, you take explicit responsibility.

6.3 Logging

SDKs SHOULD log a debug-level message when overriding existing headers. No error should be raised—this is expected behavior when integrating with existing observability infrastructure.

Rationale

Why Group-Based Design?

The group-based approach was developed through community feedback to address several concerns:

  1. Separate W3C standards: traceparent/tracestate (W3C Trace Context) and baggage (W3C Baggage) are separate standards that can version independently. They should be configurable independently.

  2. Future-proof: New standards (Datadog, Jaeger, OpenTelemetry vendor extensions, internal conventions) don't require spec changes—users define their own groups.

  3. Semantic coupling: Headers like traceparent + tracestate are tightly coupled (tracestate is keyed to traceparent). The clear-and-use-meta policy ensures they stay together.

  4. Simple mental model: Single policy setting per group, 3 clear policies, no ambiguity.

Why Default-On for W3C Trace Context?

The key insight is that opt-in defeats the purpose. If every MCP server author must explicitly enable trace forwarding, adoption will be minimal and the distributed tracing problem remains unsolved.

W3C Trace Context fields are:

  1. Standardized - designed specifically for propagation across service boundaries
  2. Non-sensitive - contain only trace/span IDs, not business data
  3. Expected to propagate - this is their entire purpose

This matches how OpenTelemetry auto-instrumentation works: trace context propagates automatically without application code changes.

Why Opt-In for Custom Fields?

Custom fields like tenant_id, user_id, or correlation_id could contain:

  • Sensitive identifiers
  • Business context that shouldn't leak to third parties
  • PII or security-relevant data

These require explicit opt-in to prevent accidental data exposure.

Why Protocol-Level Specification?

PR #666 was rejected because it added transport-specific behavior without protocol backing. This SEP:

  1. Defines which _meta fields have semantic meaning for forwarding
  2. Specifies default behaviors via group policies
  3. Provides clear SDK implementation guidance
  4. Allows SDK implementations to follow a standard

Alignment with OpenTelemetry

This proposal aligns with OpenTelemetry's MCP context propagation guidance which recommends using params._meta. The approach of using standard header names (no X-MCP- prefix for W3C headers) also aligns with OpenTelemetry semantic conventions.

Alternative Designs Considered

  1. All opt-in (PR #666 approach): Rejected because it requires every server to enable forwarding, defeating the purpose.

  2. All opt-out: Too risky for custom fields that may contain sensitive data.

  3. New top-level request.trace field: Would require schema changes and break existing implementations.

  4. Hardcoded W3C headers: The original design treated all W3C headers as one unit. Rejected because trace-context and baggage are separate standards.

  5. Two-setting design (enabled + conflictPolicy): Rejected in favor of single policy setting for simplicity.

The chosen design balances automatic trace propagation with security for custom data.

Backward Compatibility

This SEP introduces a behavioral change for W3C Trace Context forwarding:

  • Before: No headers forwarded from _meta
  • After: traceparent, tracestate, baggage forwarded automatically

This is backward compatible in the sense that:

  • The _meta field format is unchanged
  • Servers not making HTTP calls are unaffected
  • Downstream APIs receiving additional headers will ignore unknown headers (per HTTP spec)
  • Servers can opt-out if the new behavior causes issues

Risk mitigation:

  • Servers can set policy: "ignore-meta" for any group to restore previous behavior
  • Only W3C standard headers are forwarded by default (not arbitrary data)
  • SDK release notes should clearly document the behavioral change

Security Implications

Header Injection

Risk: Malicious _meta values could inject arbitrary HTTP headers.

Mitigation: SDKs MUST validate all _meta values (ASCII-only, no control characters, size limits) and reject/sanitize invalid input.

Information Disclosure

Risk: Sensitive data in _meta could leak to third-party APIs.

Mitigation:

  • Only W3C Trace Context fields (non-sensitive) are forwarded by default
  • Custom fields require explicit opt-in with whitelist
  • Documentation MUST warn against including sensitive data in _meta

Amplification

Risk: Large _meta payloads could increase request sizes.

Mitigation: Size limits (256 chars per field, 8KB total) prevent abuse.

Reference Implementation

A reference implementation will be provided in the TypeScript SDK after acceptance. The implementation will include:

  1. Automatic header injection for HTTP clients (fetch, axios, etc.) based on group policies
  2. headerGroups configuration with predefined trace-context and baggage groups
  3. Support for custom group definitions
  4. context.meta accessor in tool handlers
  5. extractHttpHeaders() utility for manual use
  6. Validation and sanitization logic
  7. Unit tests covering all policy behaviors

Proof of Concept Testing

The problem was verified using:

  • MCP client sending _meta with traceparent and correlation_id
  • Unmodified @timlukahorstmann/mcp-weather MCP server
  • Mock HTTPS server intercepting AccuWeather API calls
  • Result: No trace headers received at mock server

Test code available at: [link to test repository or gist]

Open Questions

  1. Should there be a capability negotiation? Clients could discover if a server supports meta forwarding via initialize response.

  2. Should forwarding be configurable per-tool? Some tools may call sensitive APIs where even trace context should not be forwarded.

  3. What about non-HTTP tool implementations? Tools that don't make HTTP calls (e.g., local file operations) don't benefit from this.

  4. Should baggage default to ignore-meta? W3C Baggage can contain arbitrary key-value pairs. Currently proposing prefer-meta to match OpenTelemetry behavior, but this is open for discussion.

Acknowledgments

  • Contributors to Issue #246 who established the _meta convention
  • Author of PR #666 for the initial SDK implementation attempt
  • Participants in Discussion #801 for ongoing input on request identifiers

SEP-2085 Governance Succession and Amendment Procedures

Final · Process · Created 2025-12-05

Abstract

This SEP establishes formal procedures for Lead Maintainer succession and governance amendment within the Model Context Protocol project. It defines clear processes for leadership transitions when a Lead Maintainer leaves their role and establishes requirements for proposing and approving changes to the governance structure itself.

Motivation

The current MCP governance structure defines roles and responsibilities but lacks explicit procedures for two critical scenarios:

  1. Leadership Succession: The governance document identifies Justin Spahr-Summers and David Soria Parra as Lead Maintainers (BDFLs) but does not specify what happens if one or both leave their roles. Without a defined succession process, an unexpected departure could create uncertainty about project leadership and decision-making authority.

  2. Governance Evolution: As the MCP project grows and the community evolves, the governance structure may need to adapt. Currently, there is no defined process for how the governance document itself can be amended, which could lead to ad-hoc changes without proper community input or unclear authority for making such changes.

Establishing these procedures now, while the project leadership is stable, ensures continuity and provides clear guidance for future scenarios.

Specification

The following sections shall be added to the MCP Governance document.

Succession

If a Lead Maintainer leaves their role for any reason, the succession process begins upon their written notice or, if unable to provide notice, upon a determination by the remaining Lead Maintainer(s) or Core Maintainers that the Lead Maintainer is unable to continue serving.

If one or more Lead Maintainer(s) remain, they shall appoint a successor (by majority vote if multiple), and the remaining Lead Maintainer(s) will continue to govern until a successor is appointed.

If no Lead Maintainers remain, the Core Maintainers shall appoint a successor by majority vote within 30 days, and the project operates by two-thirds vote of Core Maintainers until a new Lead Maintainer is appointed.

Amendment

Amendments to this governance structure may only be proposed by Lead Maintainers. Any proposed amendment must be approved by a two-thirds (2/3) majority of all Core Maintainers to take effect.

Amendment proposals shall:

  1. Be submitted in writing with clear rationale for the proposed change
  2. Include specific language describing the modification to existing governance provisions
  3. Allow for a minimum comment period of five (5) days before voting
  4. Be decided by recorded vote of Core Maintainers

Rationale

Succession Process Design

The succession process is designed with several principles in mind:

  • Continuity: Remaining Lead Maintainers can continue operating and appoint successors without disruption to project governance.
  • Fallback Authority: If all Lead Maintainers depart, Core Maintainers have clear authority to select new leadership, preventing a governance vacuum.
  • Time-Bound Process: The 30-day requirement ensures succession happens promptly while allowing adequate time for deliberation.
  • Supermajority Interim Governance: Two-thirds voting during interregnum periods ensures major decisions have broad support during transitional periods.

Amendment Process Design

The amendment process balances stability with adaptability:

  • Lead Maintainer Proposal Authority: Limiting proposal authority to Lead Maintainers prevents governance churn from frequent amendment proposals while ensuring those with deepest project investment can drive necessary changes.
  • Core Maintainer Approval: Requiring two-thirds Core Maintainer approval ensures amendments have broad support from those actively governing the project.
  • Comment Period: The five-day minimum comment period allows affected parties to review and provide input before voting.
  • Recorded Votes: Transparency in voting ensures accountability and provides a historical record of governance decisions.

Alternatives Considered

Succession by Election: An open election process was considered but rejected as potentially disruptive and slow during critical transition periods. The current proposal allows for quick succession while maintaining checks through the existing maintainer structure.

Amendment by Any Maintainer: Allowing any maintainer to propose amendments was considered but could lead to governance instability. The current approach balances stability with the ability to evolve.

Longer Comment Periods: Longer comment periods (e.g., 30 days) were considered but deemed excessive for a project that already has regular bi-weekly Core Maintainer meetings. Five days allows for at least one meeting cycle while enabling timely decisions.

Backward Compatibility

This SEP adds new procedures without modifying existing governance structures. No backward compatibility concerns exist.

Security Implications

This SEP has no direct security implications. However, clear succession procedures indirectly support security by ensuring continuous responsible stewardship of the project, including security-related decisions.

Reference Implementation

Upon acceptance, this SEP will be implemented by adding the Succession and Amendment sections to docs/community/governance.mdx. The new sections will be inserted after the "Lead Maintainers (BDFL)" section and before the "Decision Process" section.

A draft pull request implementing these changes will be linked here once available.

SEP-2106 Tools `inputSchema` & `outputSchema` Conform to JSON Schema 2020-12

Final · Standards Track · Created 2026-01-06

Authorship note: The original proposal was authored by John McBride (@jpmcb) in PR #881, prior to the SEP-1850 PR-based workflow. This file converts that proposal to the current SEP format and is shepherded by Ola Hungerford (@olaservo), who has also revised the Backward Compatibility, Security Implications, and SDK Migration sections in response to review feedback. The original prose and design intent remain John's; substantive changes since the conversion are tracked in this PR's commit history.

Abstract

This SEP proposes loosening the restrictions on inputSchema, outputSchema, and structuredContent to better support JSON Schema 2020-12. Specifically:

  • inputSchema: Keeps type: "object" required (since tool arguments are objects), but allows any additional JSON Schema properties to support powerful validation compositions (anyOf, oneOf, allOf, etc.)
  • outputSchema: Fully supports JSON Schema 2020-12 since MCP servers may return any valid JSON
  • structuredContent: Accepts any JSON value validated by outputSchema

This proposal enables MCP servers to leverage the expressiveness of JSON Schema 2020-12 while maintaining backward compatibility with existing implementations.

Motivation

The current MCP specification restricts tool schemas in ways that conflict with full JSON Schema support:

  1. inputSchema restriction: Currently only allows type, properties, and required fields. This prevents use of composition keywords like anyOf, oneOf, and allOf for sophisticated object validation patterns.

  2. outputSchema restriction: Also restricted to type: "object" with only properties and required, despite the specification claiming to support "JSON Schema."

  3. structuredContent restriction: Defined as { [key: string]: unknown } (an object with string keys), which prevents returning arrays—a common API response pattern.

Real-World Impact

Consider a weather API tool that returns hourly forecasts:

[
  { "hour": "09:00", "temp": 68, "conditions": "sunny" },
  { "hour": "10:00", "temp": 72, "conditions": "partly cloudy" },
  { "hour": "11:00", "temp": 75, "conditions": "cloudy" }
]

Currently, this natural array response is impossible because structuredContent must be an object. Developers are forced to wrap arrays in unnecessary container objects:

{
  "forecasts": [
    { "hour": "09:00", "temp": 68, "conditions": "sunny" },
    ...
  ]
}

This artificial constraint:

  • Adds unnecessary nesting to responses
  • Conflicts with common REST API patterns
  • Prevents direct schema validation of array responses

Schema Composition Use Cases

The current inputSchema restriction prevents legitimate schema patterns. With this SEP, tools can use composition keywords alongside type: "object":

{
  "type": "object",
  "oneOf": [
    { "properties": { "id": { "type": "string" } }, "required": ["id"] },
    { "properties": { "name": { "type": "string" } }, "required": ["name"] }
  ]
}

This pattern allows a tool to accept either an ID-based or name-based lookup—a common API design that is currently unsupported because the schema only allows type, properties, and required fields.

Specification

1. Loosen inputSchema

Current definition:

inputSchema: {
  type: "object";
  properties?: { [key: string]: object };
  required?: string[];
};

Proposed definition:

inputSchema: {
  $schema?: string;
  type: "object";
  [key: string]: unknown;
};

The inputSchema field retains the type: "object" requirement (since tool arguments are always objects), but now accepts any additional JSON Schema properties. This enables:

  • Composition keywords: anyOf, oneOf, allOf, not
  • Conditional schemas: if/then/else
  • Reference schemas: $ref, $defs
  • Any other valid JSON Schema 2020-12 keywords

2. Loosen outputSchema

Current definition:

outputSchema?: {
  type: "object";
  properties?: { [key: string]: object };
  required?: string[];
};

Proposed definition:

outputSchema?: {
  $schema?: string;
  [key: string]: unknown;
};

The outputSchema field accepts any valid JSON Schema 2020-12 object, enabling schemas that validate arrays, primitives, or complex compositions. Unlike inputSchema, there is no type: "object" requirement since tool outputs can be any valid JSON.

3. Loosen structuredContent

Current definition:

structuredContent?: { [key: string]: unknown };

Proposed definition:

structuredContent?: unknown;

The structuredContent field accepts any valid JSON value that conforms to the tool's outputSchema. This includes:

  • Objects: { "key": "value" }
  • Arrays: [1, 2, 3] or [{ "id": "abc" }, { "id": "xyz" }]
  • Primitives: "string", 42, true, null

4. Documentation Updates

Update docs/specification/draft/server/tools.mdx:

  • Remove statement that structuredContent is "returned as a JSON object"
  • Clarify that structuredContent can be any JSON value conforming to outputSchema
  • Add examples demonstrating array responses

5. Examples

Tool returning an array of objects:
{
  "name": "list_users",
  "description": "List all users in the system",
  "inputSchema": {
    "type": "object",
    "properties": {
      "limit": { "type": "integer", "minimum": 1, "maximum": 100 }
    }
  },
  "outputSchema": {
    "type": "array",
    "items": {
      "type": "object",
      "properties": {
        "id": { "type": "string" },
        "name": { "type": "string" },
        "email": { "type": "string", "format": "email" }
      },
      "required": ["id", "name"]
    }
  }
}

Response:

{
  "content": [
    {
      "type": "text",
      "text": "Found 2 users: Alice (u1, alice@example.com) and Bob (u2, bob@example.com)."
    }
  ],
  "structuredContent": [
    { "id": "u1", "name": "Alice", "email": "alice@example.com" },
    { "id": "u2", "name": "Bob", "email": "bob@example.com" }
  ]
}
Tool with composition schema:
{
  "name": "find_resource",
  "description": "Find a resource by ID or name",
  "inputSchema": {
    "type": "object",
    "oneOf": [
      {
        "properties": { "id": { "type": "string", "format": "uuid" } },
        "required": ["id"]
      },
      {
        "properties": { "name": { "type": "string", "minLength": 1 } },
        "required": ["name"]
      }
    ]
  }
}

Rationale

Why not just allow arrays?

While we could simply extend structuredContent to allow arrays, this would be an incomplete solution. The root cause is that the schema types are artificially restricted to type: "object". By allowing any valid JSON Schema, we:

  1. Enable the full power of JSON Schema 2020-12
  2. Align with the specification's claim of JSON Schema support
  3. Provide a consistent, principled approach rather than piecemeal fixes

Why not require a wrapper object?

Requiring arrays to be wrapped in objects (e.g., { "items": [...] }) was considered but rejected because:

  1. It adds unnecessary complexity to responses
  2. It conflicts with common API design patterns
  3. It prevents direct schema validation of the actual response structure
  4. JSON Schema already handles array validation elegantly

Real-World API Patterns

Many production APIs return arrays directly:

  • GitHub Events API: Returns arrays of event objects
  • AccuWeather Search API: Returns arrays of location matches
  • REST collection endpoints: Standard GET /users returns [{...}, {...}]

Forcing wrapper objects creates friction for developers integrating existing APIs with MCP. Generic JSON Schema validation libraries should work without MCP-specific customization.

Alignment with JSON Schema 2020-12

JSON Schema 2020-12 provides powerful features for schema composition and validation. By removing artificial restrictions, MCP aligns with industry standards (OpenAPI 3.1 uses JSON Schema 2020-12) and enables developers to leverage existing JSON Schema knowledge and tooling.

SDK Ecosystem Evidence

The friction caused by current restrictions is not theoretical. FastMCP, one of the most popular Python SDKs for MCP, has implemented extensive workarounds:

  1. Explicit error messages acknowledge the limitation:

    raise ValueError(
        f"Output schemas must represent object types due to MCP spec limitations."
    )
    
  2. Auto-wrapping infrastructure adds complexity:

    • A _WrappedResult dataclass wraps non-object returns
    • A custom x-fastmcp-wrap-result extension enables client-side unwrapping
    • Both SDK and client need matching wrap/unwrap logic
  3. Real bugs have resulted from these workarounds:

    • Issue #2455: $ref schemas without type: object broke ALL tools on the server
    • Issue #2421: Unexpected {"result": ...} wrapping confused users

This demonstrates that the current restrictions create genuine ecosystem friction that SEP-2106 would eliminate.

OpenAPI Precedent

The OpenAPI specification went through a similar evolution. OpenAPI 3.0 used an "extended subset" of JSON Schema with custom restrictions (like requiring nullable: true instead of allowing "null" as a type).

OpenAPI 3.1 made the strategic decision to fully align with JSON Schema 2020-12, accepting breaking changes to eliminate the friction. The result: better tooling compatibility and less ecosystem confusion.

OpenAPI's Problem MCP's Parallel
type must be string, not array inputSchema only allows specific fields
Couldn't use standard null handling Can't use oneOf/anyOf in schemas
Custom nullable keyword Object-only structuredContent
Caused tooling confusion Causes SDK workarounds

MCP can learn from OpenAPI's experience rather than repeating the same evolution over several years.

Backward Compatibility

This change is wire-format backward compatible but has nuances depending on the direction of the version mismatch.

Compatibility Matrix

New client (post-SEP) Old client (pre-SEP)
New server (post-SEP) Fully compatible. Compatible only when the server returns object-typed structuredContent. Arrays/primitives in structuredContent may break.
Old server (pre-SEP) Fully compatible. Existing object-only schemas remain valid. Unchanged.

The asymmetry: a new server that takes advantage of array or primitive structuredContent (or composition keywords in inputSchema) cannot assume an old client will accept the response. Old clients written against the previous wire format may reject structuredContent that is not a JSON object, or fail to validate inputSchema containing keywords beyond type/properties/required.

To remain interoperable with older clients, servers using array or primitive structuredContent MUST also emit a TextContent block containing the serialized JSON (as already recommended in the tools specification). Clients that do not understand non-object structuredContent can fall back to the text content.

TypeScript / SDK Migration

Widening the structuredContent field type from { [key: string]: unknown } to unknown is a source-breaking change for typed consumers, even though the wire format is unchanged. Code such as:

const result = await client.callTool({ name: "get_weather", arguments: { ... } });
const temp = result.structuredContent?.temperature;        // previously compiled (type: unknown)
const city = result.structuredContent?.["city"] as string; // previously compiled

will no longer type-check after the change, because TypeScript forbids property access on unknown without a narrowing guard:

const sc = result.structuredContent;
if (sc && typeof sc === "object" && !Array.isArray(sc)) {
  const temp = (sc as Record<string, unknown>).temperature;
}

This break is intentional — the previous type was a lie whenever a tool returned a non-object — but SDK maintainers SHOULD:

  • Document the migration in SDK release notes.
  • Where ergonomic, provide typed helpers (e.g. generics over a tool's outputSchema) so consumers do not need to write narrowing guards by hand.

Migration Path

  • Servers: No migration is required to keep working as before. To use array or primitive structuredContent, also emit a serialized TextContent fallback.
  • Clients: Old clients continue to work against object-only servers. To consume the new flexibility, accept any JSON value in structuredContent and validate against outputSchema if present.
  • SDKs: Update generated types to mirror the new schema (unknown for structuredContent, open-ended inputSchema/outputSchema) and call out the source-breaking type change in release notes.

Security Implications

JSON Schema validation already handles type checking, value constraints, and required field validation, and implementations MUST continue to validate all inputs and outputs against declared schemas. Allowing the full JSON Schema 2020-12 vocabulary surfaces two areas that warrant explicit guidance.

$ref Dereferencing (SSRF and Fetch-DoS)

JSON Schema 2020-12 permits $ref to point at an absolute URI, not just a JSON Pointer into the same document. A naive implementation that resolves every $ref it encounters by issuing an HTTP request gives an attacker a server-side request forgery / fetch amplification primitive: a malicious tool definition can cause the host to fetch arbitrary URLs, including internal metadata endpoints or large payloads designed to exhaust resources.

To mitigate this:

  • Implementations MUST NOT automatically dereference $ref values that resolve to a network URI (i.e. anything that is not a same-document JSON Pointer such as #/$defs/Foo or an internal $anchor).
  • "Automatically" here means "as part of normal validation or schema processing, without explicit operator action." Implementations MAY offer an opt-in mode that fetches non-local $refs, but it MUST be disabled by default and SHOULD enforce an allowlist of hosts (or at minimum reject loopback, link-local, and private network addresses), apply timeouts and size limits, and log dereferenced URIs.
  • Schemas that fail to validate due to an unresolved external $ref SHOULD be rejected rather than silently treated as permissive.

Composition-Keyword Resource Use

Composition keywords (anyOf, oneOf, allOf, if/then/else) and $defs enable expressive schemas, but pathological combinations can be expensive to validate. Implementations SHOULD apply reasonable bounds — for example, a maximum schema depth, a cap on the total number of subschemas, or a per-validation time budget — to prevent a malicious tool definition from acting as a CPU DoS vector against the validator.

Reference Implementation

TypeScript SDK

A reference implementation demonstrating the loosened type restrictions:

  • Branch: olaservo/typescript-sdk@sep-834-v1x
  • npm: @olaservo/mcp-sdk@1.25.2-sep834.4
  • Key changes:
    • inputSchema: Retains type: "object" but allows any additional JSON Schema properties (compositions like oneOf/anyOf)
    • outputSchema: Any valid JSON Schema object (arrays, primitives, objects, compositions)
    • structuredContent: Any JSON value (objects, arrays, or primitives)
    • McpServer high-level API updated to support array and primitive outputSchema

Everything Server Demo Tools

Three demo tools added to the everything server demonstrating SEP-2106 capabilities:

  • Branch: olaservo/servers@sep-834-json-schema-2020-12
  • npm: @olaservo/mcp-server-everything-sep834@1.1.0-sep834.1
  • Tools:
    • get-weather-forecast: Returns raw array of hourly forecasts directly in structuredContent
      • Matches the exact example from SEP-2106's Motivation section
      • outputSchema: z.array(HourlyForecastSchema) - array type at root
      • structuredContent: [{hour, temp, conditions}, ...] - direct array
    • find-by-id-or-name: Demonstrates flexible input patterns (accepts id OR name)
    • get-count: Returns raw number directly in structuredContent (not wrapped in object)
      • outputSchema: z.number() - primitive type at root
      • structuredContent: 42 - direct primitive

Implementation Guidance

SDK implementations will need to:

  1. Update inputSchema types to retain type: "object" but allow any additional JSON Schema properties
  2. Update outputSchema types to allow any valid JSON Schema (remove type: "object" constraint)
  3. Update structuredContent types to accept any valid JSON value
  4. Update JSON Schema definitions accordingly

Acknowledgments

This proposal builds on discussions in GitHub issue #834 and incorporates feedback from the MCP community.

SEP-2127 MCP Server Cards - HTTP Server Discovery

Accepted · Extensions Track · Created 2026-01-21

  • Status: Accepted
  • Accepted: 2026-09-25
  • Type: Extensions Track
  • Created: 2026-01-21
  • Author(s): David Soria Parra (@dsp-ant), Sam Morrow (@SamMorrowDrums), Tadas Antanavicius (@tadasant); on behalf of the Server Card Working Group
  • Extension Identifier: io.modelcontextprotocol/server-card
  • PR: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127
The full extension specification is maintained in the [experimental-ext-server-card repository](https://github.com/modelcontextprotocol/experimental-ext-server-card) (graduating to an `ext-server-card` repository on acceptance). That repository holds the TypeScript source of truth (`schema.ts`), the generated JSON Schema (`schema.json`), the discovery mechanics (`docs/discovery.md`), and example documents. This SEP is the chartering document and intentionally does not duplicate that wire-format detail.

Abstract

This SEP proposes adding a standardized, self-contained format to describe MCP servers for pre-connection discovery. This enables clients to automatically discover available transports, protocol versions, and connection guidance before establishing a connection.

As an Extensions Track SEP (per SEP-2133), this document charters the Server Card extension and names its Working Group and maintainers. The detailed, normative wire format — the Server Card schema, field definitions, the discovery mechanics, and the full security analysis — is maintained in the experimental-ext-server-card repository rather than inline here, following the precedent set by SEP-1865 (MCP Apps).

Motivation

MCP clients need an out-of-band way to discover remote servers before choosing or opening a transport. Runtime mechanisms such as server/discover become available only after a client already knows where to connect. This creates friction for discovery, integration, and optimization scenarios.

Current Pain Points

  • Manual Endpoint Configuration: Users must manually configure transport URLs for each server, with no standardized discovery mechanism.
  • No Domain-Level Discovery: Clients cannot automatically discover available MCP servers on a domain. This prevents automated integration scenarios, such as registry crawling or service auto-detection.
  • No Out-of-Band Metadata: Runtime discovery still requires a configured transport and a live connection, so it cannot support domain crawling, indexing, or pre-connection configuration.

Imported from upstream. Proposed as modelcontextprotocol#2127 by @dsp-ant on 2026-01-21, 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 — accepted. The document carries every required section, and a prototype a reviewer can run is cited at https://github.com/modelcontextprotocol/experimental-ext-server-card. Conformance tests and the reference documentation chapter are still required before Final.

Proposed Solution

This SEP introduces MCP Server Cards – structured metadata documents that servers expose through standardized mechanisms. For domain-level discovery, an AI Catalog can link to or embed the relevant Server Cards. Cards themselves can be hosted at any unreserved URI, with <streamable-http-url>/server-card reserved as the recommended location. These documents provide static server information without requiring connection establishment.

Enabled Use Cases

  • Autoconfiguration: IDE extensions can automatically configure themselves when pointed at a domain, eliminating manual setup.
  • Automated Discovery: Clients and registries can crawl domains to discover available MCP servers, enabling ecosystem-wide server indexes.
  • Reduced Latency: Display server information and metadata without opening each advertised endpoint.

Relationship to server/discover

Server Cards provide out-of-band, HTTP-oriented discovery: public, cacheable, and indexable metadata that can tell a client where to connect. The server/discover RPC provides live, in-protocol discovery consistently across transports once an endpoint is known and reachable. The mechanisms overlap in identity, endpoint, and protocol-version metadata and SHOULD remain aligned. If they disagree, clients SHOULD prefer the live server/discover values.

Discovery

Relationship to AI Catalog

The AI Catalog provides a decentralized, protocol-agnostic mechanism for identifying AI artifacts. An AI Catalog entry can link to or embed an MCP Server Card, leaving the catalog responsible for domain-level discovery and the Server Card responsible for MCP-specific identity and connection details.

MCP Connection Details

MCP Server Cards provide a richer, MCP-specific definition that can be used by MCP clients to actually connect and start performing MCP operations. The canonical discovery path and the relationship to the broader AI Catalog are specified in the extension repository's docs/discovery.md.

Example:

  • "Restaurant A" works with platform "Restaurant Reservations SaaS" to provide MCP-powered bookings for their restaurant
  • Restaurant A also works with platform "Jobs SaaS" to provide MCP-powered job listings to prospective job seekers
  • Restaurant A would advertise the two relevant Server Cards in an AI Catalog at restaurant-a.com/.well-known/ai-catalog.json
  • Restaurant Reservations SaaS would expose a Server Card per restaurant it hosts (one for Restaurant A, one for Restaurant B, etc.)
  • Jobs SaaS would expose a Server Card per entity it hosts (one for Restaurant A, one for Coffee Shop B, etc.)

We can develop and iterate on MCP Server Cards independently from AI Catalog, as long as catalog entries can identify Server Cards hosted and maintained elsewhere.

Relationship to the MCP Registry's server.json

MCP Server Cards describe remote MCP connectivity only. The MCP Registry's server.json separately describes registry entries, including locally installable packages and their runtime configuration. The Registry owns that schema; the Server Card extension does not define a Server superset or package-installation types.

Where the formats cover the same concepts, such as identity, documentation, and remote endpoints, they use compatible metadata where practical. A Registry entry can reference or embed a card's remote information, while vendors that need to attach installation hints directly to a card can use namespaced _meta.

Specification

The full specification is maintained in the experimental-ext-server-card repository. The TypeScript file schema.ts is the single source of truth; the generated schema.json and the discovery mechanics in docs/discovery.md are derived from it.

For purposes of SEP review, extension repository snapshot 526201bb captures the contract considered by this proposal. After acceptance, implementers should follow the current, versioned extension specification rather than this historical snapshot. As defined by SEP-2133, extension maintainers may evolve the specification independently while preserving backward compatibility; breaking changes require a new extension identifier.

At a high level, the Server Card extension introduces:

  • Server Card document: A static metadata document describing a remote MCP server — its identity (name, version, description, optional title / icons / repository / websiteUrl), its remote transport endpoints (URLs, headers, variable templates, supported protocol versions), and optional namespaced _meta data. _meta is not used to advertise MCP capabilities or negotiated extension support. The precise field set is defined in the extension repository's schema.ts.
  • Discovery: An AI Catalog can link to or embed multiple cards. Cards can be hosted at any unreserved URI, with <streamable-http-url>/server-card reserved as the recommended location. The media types, catalog format, and browser-oriented CORS and caching guidance are specified in docs/discovery.md.

As an extension, MCP Server Cards are optional and additive: servers that do not publish a card continue to work normally through standard MCP connections and runtime discovery (see Backward Compatibility).

Rationale

Why Use .well-known for AI Catalog?

For automated domain-level discovery, an AI Catalog can be published at /.well-known/ai-catalog.json. The .well-known URI pattern is an established IETF standard (RFC 8615) used by many protocols for service discovery, including OAuth 2.0 Authorization Server Metadata (RFC 8414). This approach:

  • Provides a predictable, standardized location for discovery
  • Requires no prior knowledge of server configuration
  • Works with standard HTTP infrastructure (caches, CDNs, load balancers)
  • Is already familiar to developers working with web services

Why Align with Registry Metadata?

MCP Server Cards aim to provide a static representation of server identity and connection details so that clients can discover and connect to them without prior knowledge of their existence.

The MCP Registry and Server Cards serve different consumers but share concepts such as server identity and remote endpoints. Reusing compatible field names where those concepts overlap reduces needless translation while allowing each owner to evolve its schema independently.

Why Exclude Primitives?

This specification intentionally omits primitive definitions (tools, resources, and prompts) from server cards. MCP servers are inherently dynamic: the primitives a server exposes can vary by authenticated user, session, configuration, feature flags, deployment state, and more. A static document cannot reliably represent this surface, and there is currently no viable substitute for runtime listing via the protocol's standard operations (tools/list, resources/list, prompts/list) with the logged-in user's identity. Including primitives prematurely risks disadvantaging dynamic servers and encouraging clients to rely on information that may be incorrect. A follow-on SEP should address the prerequisites, such as variant enumeration and clear consumer contracts, before primitive advertisement is added.

For the same reason, the initial Server Card format does not advertise MCP capabilities or extension support. supportedProtocolVersions lets a client determine whether it can attempt a connection; capabilities and extensions describe negotiated runtime behavior and can depend on the client, identity, configuration, and deployment state. They remain available through live protocol negotiation. Namespaced _meta is for non-standard metadata and is not a substitute for standardized capability or extension fields. A future revision can add those fields once it defines how static claims relate to runtime variants.

Why Not Wait for Primitives?

The debate around primitives should not delay server card adoption. Discovery (knowing that a server exists, where to connect, and what transports and protocol versions it supports) is enormously valuable on its own. It is the information an end user or IDE needs to install and configure a server, and it is the information a registry needs to index one. None of this depends on knowing the server's tool list in advance.

Server cards without primitives already enable the core use cases that motivate this SEP: autoconfiguration, domain-level discovery, reduced-latency metadata retrieval, and registry integration. Primitives can be added in a future revision once the ecosystem has the right mechanisms to advertise them safely. Shipping discovery now, and shipping it correctly, is more important than shipping a larger surface that risks being wrong.

Other Considered Discovery Mechanisms

DNS-based discovery: We considered using DNS TXT records for discovery, similar to DKIM or SPF. However, this approach would be limited to domain-level discovery and wouldn't work for path-based or port-based MCP servers, making it too restrictive.

Header-based discovery: We considered using HTTP headers (similar to Link headers) to advertise server card locations. While this could work, it requires an HTTP request to the main endpoint first, eliminating many of the benefits of pre-connection discovery.

Backward Compatibility

This SEP is fully backward compatible with existing MCP implementations:

  • Server cards are optional. Servers that don't implement them continue to work normally through standard MCP connections and runtime discovery.
  • Clients that don't support server cards can ignore them and connect using configured endpoints.
  • No changes to core MCP protocol messages or connection flows are required.

Migration Path

  1. Phase 1 (Optional): Servers can begin exposing server cards without requiring client support
  2. Phase 2 (Recommended): Clients can implement server card fetching for enhanced discovery and pre-connection validation
  3. Phase 3 (Future): The ecosystem can develop tooling around server cards for registries, security scanning, and automated discovery

Security Implications

Server Cards are publicly accessible, read-only metadata documents served over HTTP, and the extension's security posture follows from that:

  • Information disclosure: Cards are public by design; servers MUST NOT include credentials, internal network topology, proprietary logic, or user/session-specific data.
  • Primitive exclusion as a safety property: Cards deliberately exclude primitive definitions (tools, resources, prompts) so clients cannot trust a static manifest for access-control or safety decisions; primitives are always validated at runtime via standard list operations.
  • CORS: Wide-open CORS (Access-Control-Allow-Origin: *) is acceptable because cards carry only public, read-only metadata.
  • Transport security and MITM: Cards SHOULD be served over HTTPS with certificate validation; because cards are advisory and clients verify their claims against live runtime metadata, a compromised card primarily affects discoverability rather than security.
  • Denial of service: Servers SHOULD rate-limit discovery endpoints and clients SHOULD respect cache headers.

The full threat model and the normative CORS/caching/transport requirements are maintained in the extension repository's docs/discovery.md.

Reference Implementation

The Server Card extension is incubated in the experimental-ext-server-card repository, which holds the TypeScript schema source of truth, the generated JSON Schema, discovery documentation, and valid/invalid example documents exercised by a validation script.

python-sdk#2951 is an experimental Server Cards implementation for the Python SDK by @dsp-ant: server-side card construction and serving, client-side fetch and validation, and Pydantic models for the ServerCard shape, with tests that round-trip the conformance examples.

go-sdk#1024 provides an experimental Go SDK server-side reference implementation for constructing and serving Server Cards.

tadasant/goose#1 demonstrates Server Card discovery and installation in the Goose CLI and Desktop client.

Working Group and Maintainers

References

SEP-2133 Extensions

Final · Standards Track · Created 2025-01-21

Abstract

This SEP establishes a lightweight framework for extending the Model Context Protocol through optional, composable extensions. This proposal defines a governance model and presentation structure for extensions that allows the MCP ecosystem to evolve while maintaining core protocol stability. Extensions enable experimentation with new capabilities without forcing adoption across all implementations, providing clear extension points for the community to propose, review, and adopt enhanced functionality.

This SEP defines both official extensions (maintained by MCP maintainers) and experimental extensions (an incubation pathway for Working Groups and Interest Groups to prototype and collaborate on extension ideas before formal acceptance). Externally maintained extensions will likely come at a later stage.

Motivation

MCP currently lacks any form of guidance on how extensions are to be proposed or adopted. Without a process, it is unclear how these extensions are governed, what expectations there are around implementation, how they should be referenced in the specification, etc.

Specification

Definition

An MCP extension is an optional addition to the specification that defines capabilities beyond the core protocol. Extensions enable functionality that may be modular (e.g., distinct features like authentication), specialized (e.g., industry-specific logic), or experimental (e.g., features being incubated for potential core inclusion).

Extensions are identified using a unique extension identifier with the format: {vendor-prefix}/{extension-name}, e.g. io.modelcontextprotocol/oauth-client-credentials or com.example/websocket-transport. The names follow the same rules as the _meta keys, except that the prefix is mandatory.

To prevent identifier collisions, the vendor prefix SHOULD be a reversed domain name that the extension author owns or controls (similar to Java package naming conventions). For example, a company owning example.com would use com.example/ as their prefix.

Breaking changes MUST use a new identifier, e.g. io.modelcontextprotocol/oauth-client-credentials-v2. A breaking change is any modification that would cause existing compliant implementations to fail or behave incorrectly, including: removing or renaming fields, changing field types, altering the semantics of existing behavior, or adding new required fields.

Extensions may have settings that are sent in client/server messages for fine-grained configuration.

This SEP defines Official Extensions and Experimental Extensions. Experimental extensions are maintained within the MCP organization as an incubation pathway but are not yet officially accepted. Unofficial extensions are not recognized by MCP governance and may be introduced and governed by developers outside the MCP organization.

Official Extensions

Official extensions live inside the MCP github org at https://github.com/modelcontextprotocol/ and are officially developed and recommended by MCP maintainers. Official extensions use the io.modelcontextprotocol vendor prefix in their extension identifiers.

An extension repository is a repository within the official modelcontextprotocol github org with the ext- prefix, e.g. https://github.com/modelcontextprotocol/ext-auth.

  • Extension repositories are created at the core maintainers discretion with the purpose of grouping extensions in a specific area (e.g. auth, transport, financial services).
  • A repository has a set of maintainers (identified by MAINTAINERS.md) appointed by the core maintainers that are responsible for the repository and extensions within it (e.g. ext-auth MAINTAINERS.md, ext-apps MAINTAINERS.md).
  • Extensions SHOULD have an associated working group or interest group to guide their development and gather community input.

An extension is a versioned specification document within an extension repository, e.g. https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/oauth-client-credentials.mdx

  • Extension specifications MUST use the same language as the core specification (i.e. [BCP 14] [RFC2119] [RFC8174]) and SHOULD be worded as if they were part of the core specification.

While day-to-day governance is delegated to extension repository maintainers, the core maintainers retain ultimate authority over official extensions, including the ability to modify, deprecate, or remove any extension.

Experimental Extensions

Experimental extensions provide an incubation pathway for Working Groups (WGs) and Interest Groups (IGs) to facilitate discovery, prototype ideas, and collaborate on extension concepts before formal SEP submission. Experimental extensions allow cross-company collaboration under neutral governance with clear anti-trust protection and IP clarity.

An experimental extension repository is a repository within the official modelcontextprotocol github org with the experimental-ext- prefix, e.g. https://github.com/modelcontextprotocol/experimental-ext-interceptors.

  • Any maintainer MAY create an experimental extension repository while the associated SEP is still in draft state (or before a SEP has been submitted).
  • Experimental extensions MUST be associated with a Working Group or Interest Group, whose maintainers are responsible for day-to-day governance of the repository.
  • Experimental extension repositories MUST clearly indicate their experimental/non-official status (e.g., in the README) to avoid confusion with official extensions.
  • Any published packages from experimental extensions MUST use naming that clearly indicates their experimental status.
  • Core maintainers retain oversight of experimental extension repositories, including the ability to archive or remove them.

To graduate an experimental extension to official status, the standard SEP process (Extensions Track) applies. The experimental repository and any reference implementations developed during incubation MAY be referenced in the SEP to demonstrate the extension's practicality.

Lifecycle

Creation

Extensions MAY optionally begin as experimental extensions (see Experimental Extensions section) to facilitate prototyping and collaboration before formal submission. This incubation period is encouraged but not required.

To become an official extension, extensions are created via a SEP in the main MCP repository using the standard SEP guidelines but with a new type: Extensions Track. This type follows the same review and acceptance process as Standards Track SEPs, but clearly indicates that the proposal is for an extension rather than a core protocol addition. The SEP must identify the Working Group and Extension Maintainers that will be responsible for the extension. See SEP-2148 for how maintainers are appointed.

Extension SEPs:

  • SHOULD be discussed and iterated on in a relevant working group prior to submission.
  • MUST have at least one reference implementation in an official SDK prior to review to ensure the extension is practical and implementable.
  • MAY reference an existing experimental extension repository and implementations developed during incubation.
  • Will be reviewed by the Core Maintainers, who have the final authority over its inclusion as an Official Extension.

Once approved, the author SHOULD produce a PR that introduces the extension to the extension repository and reference in the main spec (see Spec Recommendation section). Approved extensions MAY be implemented in additional clients / servers / SDKs (see SDK Implementation).

Iteration

Once accepted, extensions may be iterated on without further review from the Core Maintainers. The extension repository maintainers are responsible for the review and acceptance of changes to an extension and SHOULD coordinate change via the relevant working group(s). As extensions are independent of the core protocol, extensions may be updated and deployed at any time, but changes MUST ensure they account for backwards compatibility in their design.

Promotion to Core Protocol (Optional)

Eventually, some extensions MAY transition to being core protocol features. This SHOULD be treated as a Standards Track SEP with separate core maintainer review. Note that not all extensions are suitable for inclusion in the core protocol (e.g. those specific to an industry) and may remain as extensions indefinitely.

Spec Recommendation

Extensions will be referenced from a new page on the MCP website at omcp.tech/extensions (to be created) with links to their specification.

Links to relevant extensions MAY also be added to the core specification as appropriate (e.g. https://omcp.tech/specification/draft/basic/authorization may link to ext-auth extensions), but they MUST be clearly advertised as optional extensions and SHOULD be links only (not copies of specification text).

SDK Implementation

SDKs MAY implement extensions. Where implemented, extensions MUST be disabled by default and require explicit opt-in. SDK documentation SHOULD list supported extensions.

SDK maintainers have full autonomy over extension support in their SDKs:

  • Maintainers are solely responsible for the implementation and maintenance of any extensions they choose to support.
  • Maintainers are under no obligation to implement any extension or accept contributed implementations. Extension support is not required for 100% protocol conformance or the upcoming SDK conformance tiers.
  • This SEP does not prescribe how SDKs should structure or package extensions. Maintainers may provide extension points, plugin systems, or any other mechanism they see fit.

Evolution

All extensions evolve independently of the core protocol, i.e. a new version of an extension MAY be published without review by the core maintainers. Minor updates, bug fixes, and non-breaking enhancements to an extension do not require a new SEP; these changes are managed by the extension repository maintainers.

Extensions SHOULD be versioned, but exact versioning approach is not specified here.

Negotiation

Clients and servers advertise their support for extensions in the ClientCapabilities and ServerCapabilities fields respectively, and in the Server Card (currently in progress).

A new "extensions" field will be introduced to each that is a map of extension identifiers to per-extension settings objects. Each extension specifies the schema of its settings object; an empty object indicates no settings.

Client Capabilities

Clients advertise extension support in the initialize request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "roots": {
        "listChanged": true
      },
      "extensions": {
        "io.modelcontextprotocol/ui": {
          "mimeTypes": ["text/html;profile=mcp-app"]
        }
      }
    },
    "clientInfo": {
      "name": "ExampleClient",
      "version": "1.0.0"
    }
  }
}
Server Capabilities

Servers advertise extension support in the initialize response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {},
      "extensions": {
        "io.modelcontextprotocol/ui": {}
      }
    },
    "serverInfo": {
      "name": "ExampleServer",
      "version": "1.0.0"
    }
  }
}
Server-Side Capability Checking

Servers SHOULD check client capabilities before offering extension-specific features:

const hasUISupport = clientCapabilities?.extensions?.[
  "io.modelcontextprotocol/ui"
]?.mimeTypes?.includes("text/html;profile=mcp-app");

if (hasUISupport) {
  // Register tools with UI features
} else {
  // Register text-only fallback
}
Graceful Degradation

If one party supports an extension but the other does not, the supporting party MUST either revert to core protocol behavior or reject the request with an appropriate error if the extension is mandatory. Extensions SHOULD document their expected fallback behavior. For example, a server offering UI-enhanced tools should still return meaningful text content for clients that do not support the UI extension, while a server requiring a specific authentication extension MAY reject connections from clients that do not support it.

Trademark Policy
  • Use of MCP trademarks in extension identifiers does not grant trademark rights. Third parties may not use 'MCP', 'Model Context Protocol', or confusingly similar marks in ways that imply endorsement or affiliation.
  • MCP makes no judgment about trademark validity of terms used in extensions.
Antitrust
  • Extension developers acknowledge that they may compete with other participants, have no obligation to implement any extension, are free to develop competing extensions and protocols, and may license their technology to third parties including for competing solutions.
  • Status as an official extension does not create an exclusive relationship.
  • Extension repository maintainers act in individual capacity using best technical judgment.
Licensing

Official extensions MUST be available under the Apache 2.0 license.

Contributor License Grant

By submitting a contribution to an official MCP extension repository, you represent that:

  1. You have the legal authority to grant the rights in this agreement
  2. Your contribution is your original work, or you have sufficient rights to submit it
  3. You grant to Linux Foundation and recipients of the specification a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license to:
    • Reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute the contribution
    • Make, have made, use, offer to sell, sell, import, and otherwise transfer implementations
No Other Rights

Except as explicitly set forth in this section, no other patent, trademark, copyright, or other intellectual property rights are granted under this agreement, including by implication, waiver, or estoppel.

Not Specified

This SEP does not specify all aspects of an extension system. The following is an incomplete list of what this SEP does not address:

  • Schema: we do not specify a mechanism for extensions to advertise how they modify the schema.
  • Dependencies: we do not specify if/how extensions may have dependencies on specific core protocol versions, or interdependencies with other extensions (or versions of extensions).
  • Profiles: we do not specify a way of grouping extensions.

These are omitted not because they are unimportant, but because they may be added later and the goal of this SEP is simply to get some initial extension structure off the ground and defers detailed technical discussion around more complex/debatable aspects of extensions.

Rationale

This design for extensions uses the following principles:

  • Start simple: the intention is to have a relatively simple mechanism that allows people to start building and proposing extensions in a structured way.
  • Clear governance: For now, the focus is on clear governance and less on implementation details.
  • Refine later: Over time, once we have more experience with extensions, we can adjust the approach appropriately.

Some specific design choices:

  • Why extension repositories instead of individual/independent extensions? Repositories provide a natural group and governance structure that allows for the repository maintainers to enforce structure and conformity to extensions. It avoids a failure case of different extensions in an area working in incompatible ways. Also provides a way to delegate much of the governance work.
  • Why not require core maintainer review for official extensions? Delegated reviews allows for extensions to evolve autonomously without being bottlenecked on core maintainer review, which is already a (often months) long process.
  • Why separate versioning? Extensions are additions to the spec and optional so there is no need to tie versions together. Separate versions allow for more rapid iteration.

Backward Compatibility

The extension framework itself is purely additive to the core protocol, so there are no backwards compatibility concerns with the core specification.

The design described in this SEP is consistent with existing official extensions (ext-apps and ext-auth), which already use the patterns specified here for capability negotiation and extension identifiers.

However, individual extensions may have their own backwards compatibility concerns. Extensions MUST consider and account for backwards compatibility in their design, both across core protocol versions and extension versions. Breaking changes within an extension MUST use a new extension identifier (see Definition section). Extensions SHOULD also document their approach to backwards compatibility and stability (e.g. an extension MAY advertise itself as "experimental" indicating that it may break without notice).

Security Implications

Extensions MUST implement all related security best practices in the area that they extend.

Clients and servers SHOULD treat any new fields or data introduced as part of an extension as untrusted and SHOULD comprehensively validate them.

Reference Implementation

To be provided.

SEP-2145 Standardize `tools/call` Failure Reporting

Accepted · Standards Track · Created 2026-01-23

Abstract

This SEP proposes clarifying and extending tools/call error handling so that more tool-invocation failure modes are reported as Tool Execution Errors (i.e., a successful JSON-RPC response containing a CallToolResult with isError: true), rather than as JSON-RPC Protocol Errors.

Building on SEP-1303 (input validation errors), this SEP focuses on: (1) tool resolution failures (unknown tool names, tools that are present but not callable due to server policy/disablement), and (2) output validation failures for tools that declare an outputSchema but return missing or non-conforming structuredContent.

The goal is to make these failures consistently visible to language models (and other clients), improving self-correction and recovery behavior.

Motivation

SEP-1303 established that tool input validation failures are most useful when returned as tool execution errors, because models can read the message and self-correct.

In practice, additional tools/call failure modes are similarly actionable (or at least strategically useful) to surface to the model:

  • Unknown tool: When the model calls a tool name that does not exist (e.g., stale tool list, wrong name, tool name produced by another tool), returning a tool execution error allows the model to select an alternative tool or re-list tools.
  • Tool not callable: Servers may have tools that are temporarily unavailable, restricted, or disabled by policy. Returning a tool execution error allows the model to pick an alternative path or avoid repeated failing attempts.
  • Output schema failures: If a tool declares outputSchema but fails to produce valid structuredContent, returning a tool execution error allows the model/client to fall back to unstructured output, retry with different parameters, or report a clear diagnostic.

Today, the draft schema documentation states that "errors in finding the tool" should be protocol errors, and SEP-1303 retained that categorization when it went Final. This SEP proposes reversing that decision so that resolution failures are model-visible. The current split causes inconsistent implementations and pushes "out-of-the-box" behavior into client-specific policy, which many hosts will not implement.

Specification

Terminology

  • Protocol Error: A JSON-RPC error response (error object) indicating a failure to process the request at the JSON-RPC / MCP method layer (e.g., invalid JSON-RPC, invalid request shape, method not found, server internal failure before a method result can be constructed).
  • Tool Execution Error: A successful JSON-RPC response (result object) that contains a CallToolResult with isError: true, where the content describes the failure.
  • Tool Invocation: The conceptual operation of attempting to run the named tool with provided arguments after the server has accepted a well-formed tools/call request.

Imported from upstream. Proposed as modelcontextprotocol#2145 by @KKonstantinov on 2026-01-23, 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 — accepted. The document carries every required section, and a prototype a reviewer can run is cited at https://github.com/modelcontextprotocol/typescript-sdk/pull/1389. Conformance tests and the reference documentation chapter are still required before Final.

Normative requirements (server behavior)

Given a tools/call request that satisfies the CallToolRequest schema (i.e., it is not malformed at the protocol level), servers:

  1. MUST report tool input validation failures as Tool Execution Errors (extending the categorization established by SEP-1303).

    • This includes failures of JSON Schema validation against the tool's inputSchema as well as additional programmatic validation (ranges, invariants, cross-field checks, etc.).
  2. MUST report tool resolution failures as Tool Execution Errors, including:

    • Unknown tool: params.name does not match any callable tool.
    • Tool not callable: the tool exists conceptually but cannot be executed (e.g., disabled, unavailable, policy-restricted).
  3. MUST report tool execution failures as Tool Execution Errors, including:

    • Exceptions thrown by the tool implementation.
    • Timeouts or server-internal aborts that occur during tool execution (if the server can still return a CallToolResult). Client-initiated cancellation continues to follow the cancellation utility.
  4. SHOULD report output validation failures as Tool Execution Errors when a tool declares an outputSchema, including:

    • structuredContent is missing when the server/tool intended to provide structured output.
    • structuredContent is present but does not conform to outputSchema.
    • structuredContent is present but is not an object (root must be type: "object").

    Note: tools.mdx already requires "Servers MUST provide structured results that conform to outputSchema" and "Clients SHOULD validate." This requirement is defense-in-depth: when a server fails to conform, it SHOULD also report the failure as a Tool Execution Error rather than returning silently invalid output.

  5. MUST use Protocol Errors only for protocol-level failures, such as:

    • Malformed requests that fail to satisfy the CallToolRequest schema (e.g., missing params, wrong types).
    • JSON-RPC method-level failures (e.g., tools/call method not supported / not implemented).
    • Server failures that prevent constructing a CallToolResult at all (e.g., unrecoverable internal errors before tool invocation can be represented as a result).

Error message guidance (non-normative)

When returning Tool Execution Errors, servers SHOULD include a short, actionable message in the first content item (typically a text content block). Recommended patterns:

  • Unknown tool: Unknown tool: <name>. Re-run tools/list and retry.
  • Tool not callable: Tool unavailable: <name>. Reason: <reason>.
  • Input validation: Invalid input: <tool_name>. <details>.
  • Output validation: Tool produced invalid structured output for <tool_name>: <details>.

Servers SHOULD avoid leaking sensitive policy details in messages.

Examples

Protocol error (malformed request)

If the request is invalid at the CallToolRequest schema level (e.g., missing params.name, invalid params type, etc.), the server returns a JSON-RPC error response.

{
  "jsonrpc": "2.0",
  "id": 3,
  "error": {
    "code": -32602,
    "message": "Invalid params"
  }
}
Tool execution error (unknown tool)
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Unknown tool: invalid_tool_name. Re-run tools/list and retry."
      }
    ],
    "isError": true
  }
}
Tool execution error (tool not callable / disabled / policy-restricted)
{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Tool unavailable: update_flight_details. This tool is disabled or policy-restricted."
      }
    ],
    "isError": true
  }
}
Tool execution error (output schema validation failure)
{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Tool produced invalid structured output for get_weather: structuredContent did not conform to outputSchema."
      }
    ],
    "isError": true
  }
}

Specification text changes (draft targets)

This SEP intends to update the draft specification text in:

  • docs/specification/draft/server/tools.mdx (Error Handling section) to:
    • Treat "Unknown tools" (tool name resolution) as a Tool Execution Error for tools/call.
    • Add explicit categories for "tool not callable" and "output schema validation failures" as Tool Execution Errors.
    • Keep Protocol Errors focused on malformed CallToolRequest and server-level failures.
  • schema/draft/schema.ts documentation for CallToolResult.isError to remove/adjust language suggesting that "errors in finding the tool" should be protocol errors, aligning it with the above requirements.
  • schema/draft/schema.ts documentation for InvalidParamsError (Tools bullet) and the associated example files (examples/InvalidParamsError/unknown-tool.json, examples/InvalidParamsError/invalid-tool-arguments.json) so the generated Schema Reference matches.
Proposed draft wording for docs/specification/draft/server/tools.mdx (Error Handling)
## Error Handling

Tools use two error reporting mechanisms:

1. **Protocol Errors**: Standard JSON-RPC errors for protocol-level issues like:
   - Malformed requests (requests that fail to satisfy [CallToolRequest schema](/specification/draft/schema#calltoolrequest))
   - Server errors (when the server cannot return a tool result)
   - Unsupported methods (e.g., server does not implement `tools/call`)

2. **Tool Execution Errors**: Reported in tool results with `isError: true`:
   - Tool resolution failures (unknown tool name)
   - Tool availability failures (disabled, unavailable, policy-restricted)
   - Input validation errors (e.g., date in wrong format, value out of range; arguments do not match tool `inputSchema`)
   - Output validation errors (e.g., tool declared `outputSchema` but returned missing or non-conforming `structuredContent`)
   - API failures
   - Business logic errors

**Tool Execution Errors** contain actionable feedback that language models can use to self-correct and retry with adjusted parameters or alternative strategies.
**Protocol Errors** indicate issues with the request structure or method support itself.
Clients **SHOULD** provide tool execution errors to language models to enable self-correction.
Clients **MAY** provide protocol errors to language models, though these are less likely to result in successful recovery.
Proposed adjustment to CallToolResult.isError documentation (draft schema)

This SEP proposes changing the CallToolResult.isError documentation to treat "errors in finding the tool" as a Tool Execution Error for tools/call (i.e., return CallToolResult with isError: true) when the request itself is well-formed.

Rationale

  • Proposes a clearer direction for SDK behavior: As noted in the Nov 7 discussion, the Python SDK returns CallToolResult with isError: true for unknown tools, disabled tools, and various output schema failures. The TypeScript SDK previously did the same, but typescript-sdk#1389 (merged March 2026) reverted it to protocol errors to conform with the current spec text. This SEP would reverse that direction, making tool execution errors the normative behavior and reducing host-specific divergence.
  • Improves model recovery: Even when the model cannot "fix" the underlying cause (e.g., tool disabled), it can adapt (use a different tool, ask the user, or take a different path). Hiding these failures behind protocol errors often prevents that.
  • Preserves protocol integrity: Protocol errors remain appropriate for malformed requests and method-level failures; this SEP does not attempt to redefine JSON-RPC semantics.

Backward Compatibility

This SEP does not change JSON-RPC framing or introduce new required fields.

However, it is a behavioral change for servers that currently return Protocol Errors for tool resolution and output validation failures. Clients that rely on a JSON-RPC error response for these cases will need to handle CallToolResult.isError: true equivalently.

Notably, typescript-sdk#1389 (merged March 2026) recently changed the TypeScript SDK to return protocol errors for unknown tools, conforming to the current spec. Accepting this SEP would require reverting that change. python-sdk#1872 proposes a similar alignment for the Python SDK and would also need to be reconsidered.

Mitigations:

  • Clients already need to handle isError: true for tool input validation errors per SEP-1303; extending that handling to the additional scenarios should be straightforward.

Security Implications

  • Returning tool execution errors for "tool not callable" can leak policy details if the server includes overly specific messages. Servers SHOULD provide user-appropriate, least-privilege error text.
  • Returning tool execution errors for unknown tools does not introduce a new capability leak beyond what tools/list already provides, but implementations should avoid enumerating available tool names in error text unless appropriate.

Reference Implementation

The Python SDK currently wraps exceptions and returns CallToolResult with isError: true for unknown tools and other failures. The TypeScript SDK previously did the same, but typescript-sdk#1389 (merged March 2026) reverted it to protocol errors for unknown tools to conform with the current spec text. Accepting this SEP would require re-reverting the TypeScript SDK.

This SEP's reference implementation would be a documentation + SDK conformance update:

  • Update the draft tools error handling documentation and schema documentation as described above.
  • Add/adjust conformance tests (where available) to assert:
    • unknown tool → CallToolResult.isError: true
    • tool not callable (policy/disabled) → CallToolResult.isError: true
    • output schema mismatch → CallToolResult.isError: true

Alternatives Considered

  • Keep unknown tools as Protocol Errors: Rejected because "unknown tool" is not a JSON-RPC method-level failure; it is a tool-invocation outcome that is often recoverable by the model.
  • Let clients decide what to pass to the model: Rejected as insufficient for interoperability; many hosts will not implement bespoke forwarding/translation logic.

Open Questions

  • Should output schema failures be MUST (instead of SHOULD) be surfaced as Tool Execution Errors when outputSchema is declared?
  • Should the spec standardize a machine-readable error code/type inside CallToolResult (beyond isError) to distinguish unknown tool vs validation vs execution failure?

Acknowledgments

All participants in https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1303

SEP-2148 MCP Contributor Ladder

Final · Process · Created 2026-01-15

Abstract

This SEP establishes a formal contributor ladder for the Model Context Protocol project, defining clear roles, responsibilities, and advancement criteria from first-time contributor through Core Maintainer. The ladder provides transparent pathways for community members to understand how they can grow their involvement and influence within the project.

This SEP is a companion to SEP-2149: MCP Group Governance and Charter Template, which defines how Working Groups and Interest Groups operate. The two SEPs intersect: WG/IG leadership requires Member status on this ladder, and group participation is a recognized pathway to ladder advancement.

Motivation

As MCP adoption grows, the project needs a clear framework for:

  1. Contributor Development: Community members lack visibility into how to grow their involvement and influence within the MCP project. A defined ladder shows the path from first contribution to project leadership.

  2. Trust Building: Merge rights and other high-privilege responsibilities are earned through demonstrated commitment and good judgment over time. A graduated system ensures contributors are set up for success and are trusted by existing maintainers and broader community before taking on greater ownership of the project.

  3. Organizational Diversity: With multiple organizations contributing to MCP, the project needs mechanisms to prevent organizational capture while welcoming participation from outside Anthropic.

  4. Scalability: Core Maintainer bandwidth is limited. Delegating authority to Maintainers and Working/Interest Group Leads through clear scope definitions enables the project to scale.

  5. Recognition: Contributors invest significant effort in MCP. Formal recognition through defined roles acknowledges their contributions and encourages sustained engagement.

Without a contributor ladder, advancement decisions become ad-hoc, potentially inconsistent, and opaque to the community.

Specification

Guiding Principles

The contributor ladder operates under these principles:

  • Earned Trust: Advancement based on demonstrated meaningful contributions that align with the project goals, good judgment, and sustained engagement, not tenure alone
  • Multiple Growth Pathways: Code, specification work, documentation, and community building all lead to advancement
  • Transparency: Criteria for advancement are explicit and consistently applied
  • Alignment With MCP Goals: Individual contributors must demonstrate commitment to advance and evolve MCP project components beyond one's employer's interests

Role Definitions

Role Summary Key Privileges Minimum Timeline
Contributor Anyone who contributes to MCP Submit issues, PRs, participate in discussions Immediate
Member Established, active contributor GitHub org membership, triage rights, eligible for WG/IG leadership 2-3 months of meaningful contributions
Maintainer Area steward with operational responsibility Merge rights, release participation 6+ months as Member
Core Maintainer Technical leadership and protocol stewardship Final decision authority, governance participation By invitation after sustained Maintainer contribution
Lead Maintainer Ultimate project authority (founders) All Core Maintainer privileges, veto authority, appoints Core Maintainers Reserved for project founders — succession only
Community Moderator CoC enforcement and community health Moderation rights on community platforms, incident handling Parallel track — Member status + appointment

Timelines listed are minimum contribution periods, not guarantees of advancement. They exist to protect the project from rapid privilege escalation and to ensure a high bar of demonstrated commitment. Actual advancement is discretionary and may take longer in practice; the only guarantee is that advancement will not happen on a shorter timescale than documented. Exceptions require explicit Core Maintainer approval with documented rationale.

Contributor

Anyone who has contributed to MCP in any form is a contributor. This includes:

  • Opening issues or discussions
  • Submitting pull requests
  • Participating in working group discussions
  • Improving documentation
  • Helping other community members

No formal requirements, we welcome all contributions.

How to get started:

  • Review the Contributing Guide
  • Join community channels (Discord, GitHub Discussions)
  • Look for issues tagged good-first-issue or help-wanted
  • Attend working group meetings

Member

Members are established contributors who have demonstrated ongoing commitment to the success and growth of MCP.

Requirements:

  • Multiple contributions to MCP (code, documentation, and/or community)
  • At least one merged PR or accepted contribution
  • Ongoing engagement with the MCP community and not just one-off contributions
  • Enabled two-factor authentication on GitHub
  • No objections from existing Members within 7 days

Sponsorship:

  • Sponsored by two existing Members or Maintainers from different organizations
  • or sponsored by one Core Maintainer or Lead Maintainer

Minimum timeline: 2-3 months of active participation

Responsibilities:

  • Continue contributing in good faith
  • Be responsive to assigned issues and PRs
  • Follow community guidelines and code of conduct
  • Help onboard new contributors when possible

Privileges:

  • GitHub organization membership with triage rights
  • Can be assigned to issues and PRs
  • Can use shortcut approval or review commands on PRs, such as /lgtm
  • Listed in community membership roster
  • Can create PRs in restricted repositories
  • Eligible for Working Group Lead or Interest Group Facilitator roles

Inactivity: Members with no contributions for 3 months may be moved to emeritus status. Re-engagement follows a simplified re-familiarization process.

Maintainer

Maintainers are trusted stewards who take operational responsibility for specific areas.

Requirements:

  • Member for at least 6 months with sustained, high-quality contributions
  • Demonstrated leadership in working groups or significant initiatives
  • Shown ability to represent MCP's interests above an individual employer's or organization's interests
  • Deep understanding of the MCP vision, roadmap, and design principles
  • Understands how their area impacts real-world AI integration and model interaction patterns
  • Completed security and governance onboarding

Sponsorship & Approval:

  • Sponsored by an existing Maintainer or Core Maintainer
  • Approved by Core Maintainers

Responsibilities:

  • Operational ownership of area health (test stability, documentation currency)
  • Responsible for the release processes and milestone planning of their respective scope
  • Provide timely review of escalated decisions
  • Active participation in governance discussions
  • Mentor Members and develop future Maintainers
  • Represent MCP in external contexts when appropriate
  • Engage with the area ecosystem and stakeholders, understanding real-world usage, and representing community needs
  • Ensure proposals reaching Core Maintainers are refined, well-considered, and account for ecosystem-wide impact
  • Active participation in discussions on communication channels (GitHub issues, Discord)

Privileges:

  • Merge privileges for owned areas
  • Can sponsor new Maintainers
  • Participate in roadmap and prioritization discussions
  • Listed in MAINTAINERS.md
  • Release participation

All pathways can lead to Maintainer, though the specific scope will align with the contribution type.

Inactivity: Maintainers with no contributions for 6 months may be moved to emeritus status following review by Core Maintainers. Merge rights are revoked upon emeritus transition. Re-engagement requires re-completing security and governance onboarding.

Core Maintainer

Core Maintainers hold final decision-making authority for the MCP technical direction. This is the highest level of trust in the community.

Note: The Core Maintainer role is intentionally limited to ensure coherent technical vision while the project scales. Core Maintainer bandwidth concerns are addressed through clearer delegation to Maintainers, Working Group Leads, and Interest Group Facilitators, not expansion of Core Maintainer numbers.

Requirements:

  • Sustained contribution as Maintainer or similar roles over at least 6 months
  • Demonstrated judgment on complex, project-wide decisions
  • Trust and respect across organizational boundaries
  • Deep commitment to MCP's long-term success

Appointment:

  • Nominated by majority of Core Maintainers, approved by Lead Maintainers
  • Or direct appointment by Lead Maintainers

When evaluating candidates, Core Maintainers should consider whether the current composition adequately represents the breadth of the MCP ecosystem, including enterprise adopters deploying MCP in production domains.

Responsibilities:

  • Final technical decision authority for contested or cross-cutting issues
  • Stewardship of project vision and design principles
  • Governance and policy decisions
  • External representation of MCP
  • Succession planning and community health
  • Ensure restraint and sustainability in protocol evolution
  • Participation in Core Maintainer meetings and Core Maintainer meetups

Privileges:

  • Final approval on breaking changes and major spec revisions
  • Voting rights on SEPs (Specification Enhancement Proposals)
  • Approval of Maintainers
  • Governance voting rights / expectation of governance participation
  • Administrative rights to all MCP GitHub repositories
  • Listed in MAINTAINERS.md as Core Maintainer

Inactivity: Core Maintainers with no participation in governance or technical decisions for 6 months may be moved to emeritus status following review by Lead Maintainers. Given the trust and visibility of this role, Core Maintainers are expected to proactively communicate reduced availability.

Lead Maintainer

Lead Maintainers hold ultimate authority over MCP's direction and governance. This is a lifetime appointment reserved for project founders. There is no defined advancement path to this role; it is only assumed through succession when necessary (see Succession).

Responsibilities:

  • All Core Maintainer responsibilities
  • Appointment and removal of Core Maintainers
  • Final authority on contested governance decisions
  • Project-wide strategic direction

Privileges:

  • Can act alone where Core Maintainers require multiple approvals
  • Veto authority over any decision
  • Appointment of successor

Succession

If a Lead Maintainer leaves their role for any reason, the succession process begins upon their written notice or, if unable to provide notice, upon a determination by the remaining Lead Maintainer(s) or Core Maintainers that the Lead Maintainer is unable to continue serving.

If one or more Lead Maintainer(s) remain, they shall appoint a successor (by majority vote if multiple), and the remaining Lead Maintainer(s) will continue to govern until a successor is appointed.

If no Lead Maintainers remain, the Core Maintainers shall appoint a successor by majority vote within 30 days, and the project operates by two-thirds vote of Core Maintainers until a new Lead Maintainer is appointed.

Advancement Process

Self-Nomination vs. Recognition

Contributors may either:

  1. Self-nominate when they believe they meet the requirements
  2. Be nominated by a sponsor who has observed their contributions

Both paths are equally valid. Self-nomination is encouraged and preferred, as it demonstrates initiative and self-awareness of the contribution scope.

Process Steps
  1. Nomination: Nominee or sponsor opens an issue using the nomination template, including links to contributions demonstrating requirements and sponsor confirmations
  2. Community Review: 7-day period for community input
  3. Decision: Approving authority reviews and decides
  4. Onboarding: New role-holder receives appropriate access and onboarding
Advancement To Approved By
Member 2 existing Members+ from different organizations, or 1 Core/Lead Maintainer
Maintainer 1 Maintainer or Core Maintainer sponsor + Core Maintainer approval
Core Maintainer Lead Maintainers
Community Moderator 1 Core Maintainer or Lead Maintainer

Self-nomination is encouraged, but nominees must still secure the required sponsorship. Sponsors confirm support in the nomination issue.

Decision-Making & Escalation

Delegation as Default

MCP operates on a principle of delegation: decisions should be made at the lowest appropriate level. This enables the project to move quickly while preserving Core Maintainer bandwidth for cross-cutting concerns.

  • Maintainers, WG Leads, and IG Facilitators handle day-to-day decisions within scope
  • Core Maintainers intervene on escalation, cross-cutting issues, or when required (spec changes, Maintainer approval)
  • Lead Maintainer intervenes only on contested governance decisions or when Core Maintainers cannot reach consensus

When in doubt, make the decision at your level and document it. Escalate only when blocked, when the decision has project-wide implications, or when explicitly required by process.

The detailed escalation procedure for Working Group and Interest Group disputes — including the designation of a Core Maintainer without shared organizational affiliation to resolve the issue — is defined in SEP-2149 §1.5.

Escalation Matrix
Issue Type First Escalation Second Escalation Timeline
Technical disagreement in PR Maintainer in scope Core Maintainer 5 business days
Technical disagreement in WG WG Lead Core Maintainer 5 business days
Technical disagreement in IG IG Facilitator Core Maintainer 5 business days
Disagreement with WG Lead / IG Facilitator Core Maintainer Lead Maintainer 7 business days
Disagreement with Maintainer decision Core Maintainer Lead Maintainer 7 business days
Core Maintainer disagreement Lead Maintainer N/A 10 business days
Code of Conduct violation Community Moderator Core Maintainer Immediate
Security issue Core Maintainer Lead Maintainer Immediate

Escalation process:

  1. Document the decision, options considered, and points of disagreement
  2. Present to the escalation authority with a clear ask
  3. Escalation authority either: (a) provides binding guidance, (b) requests more information, or (c) escalates further if needed

Contribution Pathways

MCP values diverse contributions. Here are recognized pathways to advancement:

Code Contributions
  • SDK development (TypeScript, Python, etc.)
  • Testing infrastructure
  • Tooling and developer experience
Specification Work
  • Drafting or refining spec text
  • SEP authorship or co-authorship
  • Protocol design participation
  • Compatibility analysis
Documentation
  • User guides and tutorials
  • API documentation
  • Architecture documentation
  • Maintaining content currency
Community Building
  • Onboarding new contributors
  • Working group facilitation
  • Community support (Discord, GitHub discussions)
  • Event organization or representation
Quality & Security
  • Bug triage and reproduction
  • Security review and analysis
  • Test coverage improvement
  • Release validation

Working Group and Interest Group Leadership

Working Group (WG) Leads and Interest Group (IG) Facilitators are a special form of community leadership that doesn't require Maintainer status. WG/IG leadership focuses on facilitation and coordination rather than merge authority. The full governance rules for WGs and IGs — including participation tiers, decision-making process, meeting requirements, and lifecycle — are defined in SEP-2149: MCP Group Governance and Charter Template.

Requirements:

  • Member status minimum
  • Demonstrated sustained engagement with the WG/IG's scope
  • Good facilitation and communication skills
  • Ability to represent multiple perspectives fairly
  • Group and its leadership are sponsored by at least two Core Maintainers or one Lead Maintainer

Relationship to Contributor Ladder:

  • WG Lead and IG Facilitator experience is valuable for advancement to Maintainer
  • WG Leads and IG Facilitators without Maintainer status work with Maintainers for merge decisions
  • WG Leads and IG Facilitators have authority over group operations but not spec approval
  • WG Leads and Maintainers may sponsor SEPs
  • WG Leads may triage SEPs in their scope area, including closing SEPs that do not fit the WG's roadmap (with documented rationale; authors may appeal to Core Maintainers)

Community Moderators

Community Moderators are trusted individuals who help keep the MCP community healthy, safe, and welcoming. This is a dedicated community role focused on moderation and Code of Conduct enforcement rather than technical contribution.

Requirements:

  • Member status minimum
  • Demonstrated good judgment and composure in community interactions
  • Understanding of the MCP Code of Conduct and community guidelines
  • Ability to handle sensitive situations with discretion and fairness

Sponsorship:

  • Sponsored by a Core Maintainer or Lead Maintainer

Responsibilities:

  • Monitor community channels (Discord, GitHub Discussions, etc.) for adherence to the Code of Conduct
  • Handle Code of Conduct incident reports, including initial triage and response
  • Escalate serious or complex incidents to Core Maintainers
  • Help maintain a welcoming and inclusive environment for all community members
  • Coordinate with other moderators to ensure consistent enforcement
  • Document moderation actions and maintain confidentiality of incident details
  • Recuse from any incident in which they are personally involved; such incidents are handled directly by Core Maintainers

Privileges:

  • Moderation rights on community platforms (Discord, GitHub Discussions)
  • Access to moderation tools and private moderation channels
  • Authority to issue warnings, mute, or temporarily ban users for Code of Conduct violations
  • Listed in community moderator roster

Relationship to Contributor Ladder:

  • Community Moderator is a parallel track, not a prerequisite for technical advancement
  • Moderator experience is valued for advancement to any role, particularly where community judgment is important
  • Moderators may simultaneously hold other roles (Member, Maintainer, etc.)

Removal: Community Moderators may be removed by Core Maintainers for failure to uphold moderation standards or Code of Conduct violations. Moderators may step down voluntarily at any time.

Recognition and Visibility

The community recognizes contributors through:

  • Contributor lists such as MAINTAINERS.md
  • GitHub teams for appropriate access
  • Public acknowledgment in release notes
  • Speaking opportunities at community events
  • Badges (if implemented) on community platforms

Stepping Down and Emeritus Status

Contributors may step down from roles for any reason. This is normal and healthy.

Process:

  1. Notify relevant leadership (WG Lead, IG Facilitator, Maintainer, or Core Maintainer as appropriate)
  2. Help transition any ongoing work
  3. Move to emeritus status

Emeritus:

  • Recognized for past contributions
  • May return to active status with abbreviated re-onboarding
  • No ongoing responsibilities or privileges

Involuntary Removal: In cases of code of conduct violations or sustained non-participation, roles may be revoked following appropriate review processes.

Rationale

Why a Formal Ladder?

Informal advancement creates inconsistency and opacity. A formal ladder:

  • Sets clear expectations for all parties
  • Provides a common vocabulary for discussing advancement
  • Creates accountability in advancement decisions
  • Enables self-nomination, reducing gatekeeping

Why Minimum Timelines?

Timelines are floors, not targets. They exist for security and trust-building:

  • Trust is built through demonstrated behavior over time
  • Security risks increase with rapid privilege escalation
  • Deep project understanding requires sustained engagement
  • Behavior patterns only become visible over longer periods

Meeting a minimum timeline does not create an entitlement to advancement; it establishes eligibility for consideration. Exceptions to minimums require explicit Core Maintainer approval with documented rationale.

Why Two-Organization Sponsorship?

Requiring sponsors from different organizations:

  • Prevents organizational capture of the contributor base
  • Ensures contributors are recognized beyond their employer
  • Maintains diverse perspectives in advancement decisions

Model Inspiration

This ladder is modeled on Kubernetes community membership structures and adapted for MCP's needs and stage of development.

Backward Compatibility

This SEP establishes new processes without modifying existing structures. Current contributors retain their existing access and standing.

Security Implications

This SEP directly addresses security through:

  • Graduated privilege escalation with timeline requirements
  • Two-factor authentication requirement for Members
  • Multi-organization sponsorship to prevent capture
  • Security onboarding requirement for Maintainers

Reference Implementation

Upon acceptance, this SEP will be implemented by:

  1. Adding the contributor ladder to docs/community/contributor-ladder.mdx
  2. Creating nomination issue templates in .github/ISSUE_TEMPLATE/ (see Appendix for checklist templates)
  3. Updating MAINTAINERS.md format to reflect role distinctions

Appendix: Checklist Templates

Member Nomination Checklist

**Nominee:** [GitHub handle]
**Sponsors:** [GitHub handles]
  - **Organizations represented:** [Must be 2+ different orgs among sponsors]

**Contributions:**
- [ ] Link to merged PR(s)
- [ ] Link to issues filed/triaged
- [ ] Link to discussions participated in
- [ ] Duration of participation: [X months]

**Sponsor Attestations:**
Sponsors confirm
- [ ] Sponsors confirm nominee demonstrates community values
- [ ] Sponsors confirm nominee demonstrates sustained engagement

Maintainer Nomination Checklist

**Nominee:** [GitHub handle]
**Scope:** [Specific area]
**Sponsor:** [GitHub handle, must be Maintainer or Core Maintainer]

**Requirements:**
- [ ] Member for 6+ months with sustained, high-quality contributions
- [ ] Links to demonstrated leadership in WG, IG, or significant initiatives
- [ ] Evidence of representing MCP's interests above employer/organization interests
- [ ] Deep understanding of MCP vision, roadmap, and design principles
- [ ] Security and governance onboarding completed (or scheduled)

**Core Maintainer Approval:**
- [ ] Approved by Core Maintainers

Community Moderator Nomination Checklist

**Nominee:** [GitHub handle]
**Sponsor:** [GitHub handle, must be Core Maintainer or Lead Maintainer]

**Requirements:**
- [ ] Member status
- [ ] Links to demonstrated good judgment and composure in community interactions
- [ ] Confirmed understanding of the MCP Code of Conduct and community guidelines

**Sponsor Attestation:**
- [ ] Sponsor confirms nominee can handle sensitive situations with discretion and fairness

SEP-2149 MCP Group Governance and Charter Template

Final · Process · Created 2025-01-15

Abstract

This SEP establishes governance rules and a standardized charter template for MCP's two collaborative group types: Working Groups (WGs) and Interest Groups (IGs). Working Groups produce concrete deliverables — SEPs, implementations, and code. Interest Groups facilitate discussion and knowledge-sharing to identify problems and gather requirements. The governance rules define the requirements that all groups must follow, with lighter expectations for IGs where appropriate. The charter template defines the structure each group uses to document its specific mission, scope, leadership, and work. Together they address community feedback about unclear authority delegation and inconsistent processes across groups.

This SEP is a companion to SEP-2148: MCP Contributor Ladder, which defines the org-wide contributor roles (Member, Maintainer, Core Maintainer, Lead Maintainer) referenced throughout this document. Group leadership roles intersect with the contributor ladder: WG Leads and IG Facilitators must hold at least Member status on the ladder, and group participation is a recognized pathway to ladder advancement.

Motivation

Community interviews and feedback identified several challenges with the current group structure:

  1. Unclear Authority: It's not always clear what decisions a working group can make autonomously versus what requires Core Maintainer approval. This leads to hesitation and bottlenecks.

  2. Inconsistent Decision-Making: Different groups operate with different norms. Decisions made in one meeting may be contradicted in another, with no clear process for resolution.

  3. Participation Confusion: Community members are uncertain about who should participate in groups, what levels of involvement exist, and how to become more involved.

  4. Scope Creep: Without explicit boundaries, groups may gradually expand into areas owned by other groups or outside their mandate.

  5. Missing Escalation Paths: When groups get stuck, there's no clear path to resolution, leading to prolonged disagreements or abandoned initiatives.

  6. WG/IG Distinction: The difference between Working Groups and Interest Groups is not always clear to participants, leading to mismatched expectations about outputs and commitment.

A standardized charter template and shared governance rules address these issues by establishing consistent processes across all groups while requiring each group to explicitly define its specific scope and boundaries.

Specification

MCP maintains two types of collaborative groups:

  • Working Groups (WGs) produce concrete deliverables — SEPs, reference implementations, and code. Active contribution is expected.
  • Interest Groups (IGs) facilitate discussion and knowledge-sharing around a topic area. They produce problem statements, use cases, and recommendations. Active contribution is expected.

This specification has two parts:

  1. Group Governance — rules that apply to all MCP groups (WGs and IGs), with differences noted where applicable
  2. Charter Template — the structure each group fills in to define its specific mission, scope, and operations

Part 1: Group Governance

The following rules apply to all MCP Working Groups and Interest Groups. Individual charters cannot override these requirements. Where rules differ between WGs and IGs, this is noted explicitly.

1.1 Leadership

Each group has one or more Leads (referred to as Facilitators for Interest Groups).

Requirements for all Leads and Facilitators:

  • Hold at least Member status on the MCP Contributor Ladder
  • Demonstrated sustained engagement with the group's scope area
  • Ability to facilitate across organizational boundaries
  • Commitment to running the group's operations
  • Group and its leadership sponsored by at least two Core Maintainers or one Lead Maintainer

Additional requirements for WG Leads:

  • Commitment to 2-3 hours/week for WG activities
1.2 Leadership Responsibilities

All Leads are responsible for:

  • Schedule and facilitate regular meetings
  • Set agendas in collaboration with participants and publish them in advance
  • Ensure meeting notes are published within 48 hours
  • Maintain the group's documentation
  • Maintain a members list and respective access list in https://github.com/modelcontextprotocol/access
  • Proactively recruit and retain broad, representative membership across organizations and perspectives

WG Leads are additionally responsible for:

  • Drive proposals through the SEP (Specification Enhancement Proposal) process to resolution
  • Triage SEPs in the WG's scope area, including closing SEPs that do not fit the roadmap (with documented rationale; authors may appeal to Core Maintainers)
  • Escalate blocked decisions to Core Maintainers with clear context
  • Maintain the working group's roadmap
  • Solicit feedback from one or more Core Maintainers on the general direction of the group on a continuous basis
  • Provide quarterly status updates to the Community and Core Maintainer Group
1.3 Participation Levels

All groups use the following participation tiers. Note that WG Member is a group-specific participation level distinct from the org-wide Member role defined in the Contributor Ladder — an individual may be a WG Member in a specific group without holding org-wide Member status, and vice versa.

Level Description Privileges
Observer Anyone interested in following the group's work Read access, may attend meetings, limited discussion participation
Participant Active contributor to group discussions Can propose agenda items, participate in async votes
WG Member Sustained contributor with demonstrated expertise Counted for quorum (WGs only)
Lead/Facilitator Operational leadership of the group Sets agenda, facilitates, escalates

Interest Groups primarily operate with Observers, Participants, and Facilitators. IGs may adopt the WG Member tier if their work warrants formal decision-making, but are not required to.

Becoming a WG Member (WGs, and IGs that adopt the WG Member tier):

  • Sustained participation over 3 months
  • Meaningful contributions (code, spec text, reviews, or documentation)
  • Nomination by existing WG Member or Lead
  • No objections from Leads, Core Maintainers, or Lead Maintainers within 7 days

WG Member Responsibilities:

  • Continue contributing in good faith
  • Maintain name, organization, and Discord name in the respective group's member list

Active vs. Emeritus: WG Members who do not participate for 3 consecutive months are moved to emeritus status and may return by demonstrating renewed participation.

1.4 Decision-Making Process

This section applies primarily to Working Groups, which make binding decisions (consensus on technical designs, spec changes, etc.). Interest Groups typically operate by rough consensus in discussions and do not make binding decisions — their output is recommendations, problem statements, and use cases. IGs that adopt the WG Member tier may use this process for internal decisions.

WG Consensus is achieved through the following progression. Each step is attempted before moving to the next:

Step 1: Lazy Consensus (default)

  • Proposals announced with clear deadline (5 days minimum for minor items, 10 days for significant items)
  • Silence is consent
  • Any WG Member may block with documented objection
  • Blocks must propose alternatives or clear criteria for resolution
  • If no blocks are raised by the deadline, the proposal is accepted

Step 2: Formal Vote (when lazy consensus is blocked)

A formal vote is triggered when:

  • A WG Member blocks during the lazy consensus period
  • A Lead or three or more WG Members request a formal vote

Voting rules:

  • Quorum: 50% of active WG Members
  • Passage: Simple majority for routine matters; 2/3 majority for scope changes
  • Core Maintainer feedback is advisory unless explicitly stated as binding
  • All votes documented with rationale

Step 3: Escalation (when voting does not resolve)

If a vote fails to resolve the matter (no quorum, does not pass, or the result is contested), the Lead escalates to Core Maintainers following the escalation path defined below.

1.5 Escalation Path

For technical and design disagreements within a group's scope, groups should resolve disagreements locally before involving Core Maintainers. For WGs, this means using the decision-making progression (lazy consensus → vote → escalation). For IGs, the Facilitator should attempt to find rough consensus before escalating.

Some disagreements are not appropriate for group-level resolution and should be escalated directly to Core Maintainers:

  • Scope disputes (whether a topic falls within the group's charter)
  • Authority disputes (whether the group has the right to decide a matter)
  • Cross-group conflicts (disagreements spanning multiple WGs or IGs)
  • Code of conduct or behavioral concerns
  • Membership or participation disputes

When escalation is necessary:

  1. Lead documents the decision, options considered, and points of disagreement
  2. Lead presents the escalation to the Core Maintainer group with a clear ask
  3. The Core Maintainer group designates a CM—who should not share organizational affiliation with the parties involved—to resolve the issue and report back to the group
  4. The designated CM either: (a) provides binding guidance, (b) requests more information, or (c) recommends the full Core Maintainer group deliberate
  5. Timeline: Escalations should receive initial response within 5 business days
1.6 Meeting Requirements

Leads determine meeting frequency, format, and duration based on the group's current needs and lifecycle stage. There is no fixed cadence requirement — a WG near a specification release may meet weekly, while an IG in early exploration may meet monthly or work primarily asynchronously.

Regardless of format or frequency, all group meetings must:

Leads should actively involve WG Members and Participants in operational duties such as preparing agendas, taking meeting notes, and facilitating discussions.

1.7 Communication Channels

All groups use the following channels:

Channel Purpose Response Expectation
Discord #{name}-wg or #{name}-ig Quick questions, coordination Best effort
GitHub Discussions Long-form technical discussion Weekly triage

In addition to Discord, groups can establish a discussion category in the GitHub Discussions. Leads will be granted the appropriate roles to manage and moderate discussions.

1.8 Reporting

Working Groups provide quarterly updates (end of January, April, July, October) including:

  • Progress against deliverables
  • Blocked items and escalations
  • Membership changes
  • Upcoming priorities
  • Resource needs

The quarterly updates are provided as a document posted in the GitHub Discussions category of the Working Group. They are optionally discussed with the Core Maintainers in a core maintainer meeting.

Interest Groups do not have formal reporting requirements but should keep their charter and member list current.

1.9 Lifecycle

Working Group Formation:

  • There must be a widely acknowledged concern requiring coordination
  • PR for creation of WG into docs/community/<name>/overview.mdx, gated by CODEOWNERS requiring approval by Maintainers
  • PR for charter into docs/community/<name>/charter.mdx, gated by CODEOWNERS requiring approval from a single Core Maintainer (who should notify all Core Maintainers)
  • Initial member list approved by WG Lead

Interest Group Formation:

  • Fill out the creation template in the #wg-ig-group-creation channel on Discord
  • A Core Maintainer reviews the proposal; the IG and its Facilitator(s) must be sponsored by at least two Core Maintainers or one Lead Maintainer
  • Once sponsored, the Facilitator(s) organize the IG and create a charter

Retirement:

  • WGs: WG Lead or Core Maintainer proposes retirement with rationale; Core Maintainer or Lead Maintainer approval required. WGs are also retired when they have no active work for a sustained period or have completed all planned deliverables.
  • IGs: Core Maintainers or Lead Maintainers may retire an IG that is no longer active or needed.
  • In both cases, documentation is archived and channels are marked inactive.
1.10 Charter Amendments

Changes to a group's charter (WG or IG) require:

  • Proposal by Lead/Facilitator or Core Maintainer
  • Approval by Core Maintainers

Part 2: Charter Template

Every MCP Working Group and Interest Group must maintain a charter document following this template structure. Charters are stored as MDX files at docs/community/<group-name>/charter.mdx in the modelcontextprotocol repository and added to the docs/docs.json file. A copyable version of this template was previously published in the community documentation.

The charter captures information specific to each group. Governance rules from Part 1 apply automatically and do not need to be repeated in the charter. Sections marked (WG only) are required for Working Groups but optional for Interest Groups.

1. Group Type

State whether this is a Working Group or an Interest Group.

2. Mission Statement

A 2-3 sentence summary of the group's purpose, articulating:

  • The problem space being addressed
  • Why cross-cutting collaboration is needed
  • For WGs: what concrete deliverables the group will produce
  • For IGs: what discussions and knowledge-sharing the group will facilitate

WG Example:

The Transport Working Group exists to evolve MCP's transport mechanisms to support diverse deployment scenarios—from local subprocess communication to horizontally-scaled cloud deployments—while maintaining protocol coherence and backward compatibility.

IG Example:

The Enterprise IG explores the challenges of deploying MCP in enterprise environments, gathering use cases and requirements to inform future specification work.

3. Scope

In Scope: Enumerated responsibilities.

For WGs, this includes:

  • Specification Work: Specific spec sections or SEPs owned
  • Reference Implementations: SDK components or reference implementations
  • Cross-Cutting Concerns: Areas requiring coordination with other groups
  • Documentation: Documentation responsibilities

For IGs, this includes:

  • Topic areas for discussion
  • Types of output (problem statements, use cases, recommendations)

Out of Scope: Explicit statements of what is NOT within the group's purview to prevent mission creep.

Related Groups: List of other WGs or IGs with intersecting work and nature of overlap.

4. Leadership

Leads/Facilitators table with:

  • Role, Name, Organization, GitHub handle, Term

Leadership requirements and responsibilities are defined in the governance rules (Sections 1.1 and 1.2).

5. Authority & Decision Rights (WG only)

Each WG must explicitly define its decision authority. The decision-making process and escalation path are defined in the governance rules (Sections 1.4 and 1.5). This section documents which decisions the WG can make at which authority level.

Example:

Decision Type Authority Level
Meeting logistics & scheduling WG Leads (autonomous)
Proposal prioritization within WG WG Leads (autonomous)
SEP triage & closure (in scope) WG Leads (autonomous, with documented rationale)
Technical design within scope WG consensus
Spec changes (additive) WG consensus → Core Maintainer approval
Spec changes (breaking/fundamental) WG consensus → Core Maintainer approval + wider review
Scope expansion Core Maintainer approval required
WG Member approval WG Member sponsors

IGs do not make binding decisions and do not need this section.

6. Membership

List current group members and their participation levels, if any. Leave out if no members exist yet. Participation tiers and membership criteria are defined in the governance rules (Section 1.3).

7. Operations

Document the group's current meeting approach. Meeting requirements and communication channels are defined in the governance rules (Sections 1.6 and 1.7).

Example:

Meeting Frequency Duration Purpose
Working Session Weekly/Biweekly 60 min Technical discussion, proposal review
Office Hours Monthly 30 min Open Q&A for newcomers and observers
8. Deliverables & Success Metrics (WG only)

Active Work Items:

Item Status Target Date Champion
SEP-XXX: Name Draft/Review/Approved Date Name

Success Criteria: Measurable outcomes for WG success.

Quarterly reporting requirements are defined in the governance rules (Section 1.8).

IGs do not track formal deliverables but may list current discussion topics or planned outputs (problem statements, recommendations, etc.) in their charter.

9. Changelog

Track charter versions with date and changes.

Rationale

Why Separate Governance from Charter Template?

Separating fixed governance rules from the per-group charter template makes it clear what is consistent across all groups (decision-making, membership tiers, escalation) versus what each group defines for itself (scope, leadership roster, deliverables). This prevents groups from accidentally diverging on process while preserving flexibility where it matters.

Why Cover Both WGs and IGs?

Working Groups and Interest Groups serve different purposes but share common operational needs — leadership, meeting requirements, communication channels, and escalation paths. A unified governance framework ensures consistency while clearly articulating where IGs have lighter requirements (no formal decision authority, no deliverables tracking, no quarterly reporting).

Why a Standardized Template?

Standardization:

  • Ensures all groups address critical governance questions
  • Makes it easier for community members to understand any group's operations
  • Reduces overhead for forming new groups
  • Creates accountability through explicit documentation

Why Explicit Authority Tables?

The authority table directly addresses the "unclear authority" feedback. By enumerating decision types and required approvals, WGs and community members know exactly what can be decided autonomously versus what needs escalation. IGs are exempt from this because they produce recommendations, not binding decisions.

Why Tiered Participation?

Different engagement levels serve different community needs:

  • Observers can learn without commitment
  • Participants can contribute without full WG Member responsibilities
  • WG Members take on accountability and get decision rights (primarily WGs)
  • Leads/Facilitators provide operational continuity

Why Lazy Consensus as Default?

Lazy consensus:

  • Enables efficient decision-making for routine matters
  • Reduces meeting burden
  • Documents decisions through announcement/deadline structure
  • Preserves blocking rights for substantive concerns

Voting is reserved for contested or high-impact decisions.

Model Inspiration

This template is adapted from Kubernetes governance structures and tailored for MCP's specific needs identified through community interviews.

Backward Compatibility

Transition for Existing Groups

Working Groups and Interest Groups that exist at the time this SEP is accepted are grandfathered in — they are recognized as valid groups and do not need to re-apply through the formation process defined in Section 1.9.

However, existing groups must create a charter conforming to the template in Part 2 within 8 weeks of this SEP's acceptance. During this transition period:

  • Existing groups continue to operate under their current processes
  • Leads/Facilitators are responsible for drafting the charter
  • Core Maintainers will review and approve both WG and IG charters
  • Groups that do not produce a charter within 8 weeks will be considered inactive and subject to retirement

Security Implications

No direct security implications. However, clear authority delegation and decision processes indirectly support security by ensuring decisions are made at appropriate levels with proper accountability.

Reference Implementation

This SEP is implemented by:

  1. docs/community/working-interest-groups.mdx — governance rules (Part 1) published as the Working and Interest Groups page on omcp.tech
  2. docs/community/charter-template.mdx — copyable charter template (Part 2) linked from the above
  3. Both pages added to docs/docs.json under the Community → Governance navigation group
  4. Existing WGs and IGs must create conforming charters at docs/community/<name>/charter.mdx within 8 weeks of acceptance

SEP-2164 Standardize Resource Not Found Error Code

Final · Standards Track · Created 2026-01-28

Abstract

The current MCP specification recommends -32002 as the error code for resource not found. However, -32002 falls within the JSON-RPC "server error" range (-32000 to -32099) which is reserved for implementation-defined errors, not protocol-level semantics. Additionally, SDK implementations are inconsistent — only 4 of 6 official SDKs use -32002, while the TypeScript SDK uses -32602 and the Python SDK uses 0.

This SEP standardizes on -32602 (Invalid Params), the correct JSON-RPC error code for this case, and aligns the specification with the JSON-RPC standard.

Motivation

Current SDK implementations vary in their error handling for resource not found:

SDK Current Error Code Source
TypeScript -32602 (InvalidParams) mcp.ts#L561
Python 0 (generic) server.py#L790
C# -32002 (custom RESOURCE_NOT_FOUND) McpServerImpl.cs#L289
Rust -32002 (custom RESOURCE_NOT_FOUND) model.rs#L450
Java -32002 (custom RESOURCE_NOT_FOUND) McpAsyncServer.java#L732
Go -32002 (custom RESOURCE_NOT_FOUND) server.go#L786
Kotlin -32603 (INTERNAL_ERROR) Server.kt#L618-L621
PHP -32002 (custom RESOURCE_NOT_FOUND) Error.php#L37
Ruby N/A (left to implementor) server.rb#L375-L379
Swift N/A (no built-in handler) N/A

This inconsistency means clients cannot reliably detect resource-not-found conditions across implementations. Of the 8 SDKs with built-in resource handling, four different error codes are used: -32002 (C#, Rust, Java, Go, PHP), -32602 (TypeScript), -32603 (Kotlin), and 0 (Python). Ruby and Swift leave error handling to the server implementor. Clients that need to distinguish "resource not found" from other errors must handle all variants.

Specification

If the requested resource does not exist, servers MUST return a JSON-RPC error with code -32602 (Invalid Params):

{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32602,
    "message": "Resource not found",
    "data": {
      "uri": "file:///nonexistent.txt"
    }
  }
}

The data field SHOULD include the uri that was not found.

Servers MUST NOT return an empty contents array for a non-existent resource. An empty array is ambiguous — it could mean the resource exists but has no content, or that it doesn't exist at all.

Rationale

Why -32602 (Invalid Params)?

-32602 is the standard JSON-RPC error code for invalid parameters. A non-existent URI is semantically an invalid parameter — the client provided a URI that doesn't correspond to any resource. This aligns with the TypeScript SDK's existing behavior and avoids introducing custom error codes outside the JSON-RPC reserved range.

Why Not a Custom Error Code?

Several SDKs use -32002 (RESOURCE_NOT_FOUND), but:

  • Custom codes in the -32000 to -32099 range are "reserved for implementation-defined server errors" per JSON-RPC spec, not for protocol-level semantics
  • Adding a protocol-defined custom code requires all clients to be updated to recognize it
  • -32602 already has the correct meaning and is universally understood by JSON-RPC libraries

Backward Compatibility

This changes what is specified — the current spec recommends -32002, and this SEP changes it to -32602. However, since the current recommendation is not consistently followed across SDKs (only 5 of 10 use -32002), clients cannot rely on any single error code today. This means the practical impact on clients is minimal — any client robust enough to work across existing SDKs already handles multiple error codes or treats all errors generically.

Migration Path

  1. SDKs should update their resource-not-found error code to -32602
  2. During the transition, clients SHOULD handle both -32602 and -32002 as resource-not-found
  3. The specification should document -32602 as the canonical error code

Security Implications

None. This change only affects error code values, not access control or data exposure.

SEP-2207 OIDC-Flavored Refresh Token Guidance

Final · Standards Track · Created 2026-02-04

  • Status: Final
  • Type: Standards Track
  • Created: 2026-02-04
  • Author(s): Wils Dawson (@wdawson)
  • Sponsor: Paul Carleton (@pcarleton)
  • PR: #2207

Abstract

This proposal provides guidance for MCP implementations regarding refresh token issuance and requests, particularly when Authorization Servers support the offline_access scope. The offline_access scope originated in OIDC but can be adopted by any OAuth 2.1 Authorization Server as a mechanism to let clients explicitly request refresh tokens. This SEP clarifies the expected behavior for both Authorization Servers and MCP Clients when working with this pattern.

Motivation

MCP's authorization mechanism is based on OAuth 2.1, but many real-world deployments use Authorization Servers that also implement OpenID Connect (OIDC). A key difference between pure OAuth and OIDC is how refresh tokens are handled:

  • In pure OAuth 2.1, there is no standard mechanism for a client to explicitly request a refresh token. The Authorization Server determines whether to issue one based on the client's capabilities (e.g., the refresh_token grant type in client metadata) and its own policies.
  • In OIDC (and Authorization Servers that adopt this convention), the offline_access scope exists to allow clients to explicitly request refresh tokens, in addition to the OAuth logic.

This creates several problems in the MCP ecosystem:

  1. Clients aren't requesting refresh tokens: Major MCP clients (Cursor, Claude, VS Code, etc.) aren't explicitly asking for refresh tokens via the offline_access scope because they don't know whether the Authorization Server supports, expects, or requires it.

  2. Resource servers shouldn't specify offline_access: The offline_access scope is not a resource-specific scope—it's a concern between the client and Authorization Server. Including it in the WWW-Authenticate header's scope parameter or in the Protected Resource Metadata's scopes_supported would be semantically incorrect since it implies the resource requires refresh tokens, which it never would.

  3. Authorization Servers can be inconsistent: When processing an authorization code grant, different Authorization Servers may have different behavior when issuing refresh tokens to different clients, especially when the client doesn't specify refresh_token as a grant type or request the offline_access scope.

  4. Interoperability gap: Without this guidance, implementations may behave inconsistently, leading to poor user experience (frequent re-authentication) or security issues (issuing refresh tokens to clients that can't securely store them).

Specification

MCP Client Requirements

MCP Clients that intend to use refresh tokens and are capable of storing them securely SHOULD follow these guidelines:

  1. Advertise capability: Clients SHOULD include refresh_token in their grant_types client metadata to indicate they support refresh tokens.

  2. Scope augmentation: When the client desires a refresh token and the Authorization Server metadata contains offline_access in its scopes_supported field, the client MAY add the offline_access scope to the list of scopes from the resource server before making authorization requests to the Authorization Server.

  3. No guarantee: Clients MUST NOT assume that advertising support or requesting offline_access guarantees they will receive a refresh token. The Authorization Server retains discretion based on its policies.

MCP Server (Resource Server) Requirements

MCP Servers (acting as OAuth 2.0 Protected Resources):

  1. SHOULD NOT include offline_access in the scope parameter of WWW-Authenticate headers, as refresh tokens are not a resource requirement.

  2. SHOULD NOT include offline_access in scopes_supported in Protected Resource Metadata, as it is not a resource-specific scope.

Rationale

Why not require offline_access in the 401 response?

The offline_access scope is fundamentally different from resource-specific scopes. It represents a client's desire for long-lived access, not a requirement of the resource. Per OAuth 2.1 Section 5.3.1, the scope attribute in WWW-Authenticate indicates "the required scope of the access token for accessing the requested resource." Since the resource doesn't require offline_access, including it would be semantically incorrect.

Why check client metadata for grant types? Why not always issue refresh tokens?

OAuth 2.1 requires clients to register their supported grant types. A client that doesn't support the refresh_token grant either:

  • Cannot securely store refresh tokens
  • Has no mechanism to use them

Issuing refresh tokens to such clients wastes Authorization Server resources (tracking tokens that will never be used) and may pose security risks if the tokens are leaked.

Why allow offline_access as an alternative signal?

Some Authorization Servers—whether fully OIDC-compliant or simply adopting this convention—only issue refresh tokens when offline_access is explicitly requested. Supporting this pattern provides a compatible path for such deployments. Clients can detect Authorization Servers that support this convention by checking for offline_access in scopes_supported in the Authorization Server Metadata and adapt their behavior accordingly.

Alternative approaches considered

  1. Mandate offline_access in resource responses: Rejected because it misrepresents the resource's requirements and creates an anti-pattern.

  2. Always issue refresh tokens: Rejected because it ignores client capabilities and Authorization Server security policies.

  3. Separate OIDC-specific specification: Rejected in favor of a unified approach that works for both pure OAuth and OIDC deployments.

  4. Provide guidance for Authorization Servers: Rejected in favor of relying on OAuth and OIDC specs for this guidance as it can vary.

Backward Compatibility

This proposal is fully backward-compatible:

  • Clients that already request offline_access continue to work
  • Authorization Servers that already check client capabilities continue to work
  • MCP Servers are not required to make any changes
  • The guidance is additive and does not change existing required behavior

Implementations that don't follow this guidance may experience suboptimal behavior (missing refresh tokens or unnecessary token issuance) but will remain functional.

Security Implications

Positive security implications

  1. Reduced token leakage risk: By not issuing refresh tokens to clients that don't advertise support, we reduce the risk of long-lived tokens being stored insecurely.

  2. Defense in depth: The risk-based assessment step gives Authorization Servers flexibility to implement additional security controls.

Considerations

  1. Client metadata may not be sufficient: Since client metadata is self-reported, a malicious actor could register a client claiming refresh_token grant support to obtain long-lived tokens. Authorization Servers MAY use the risk-based assessment step (see Specification) to apply additional restrictions—such as domain allowlists, reputation checks, or verification requirements—rather than solely relying on client metadata claims when deciding whether to issue refresh tokens.

  2. Scope injection: Clients adding offline_access should ensure this doesn't interfere with other scope-related logic or create unexpected authorization prompts.

Reference Implementation

Reference implementations demonstrating this guidance will be provided in the official MCP SDKs:

  • TypeScript SDK: Client-side offline_access scope handling
  • Python SDK: Client-side offline_access scope handling
  • Authorization Server example: Demonstration of client capability checking
  • Client conformance test: Allowing for easy validation of SDK implementations

Links to implementations will be added once the SEP is accepted.

Acknowledgments

This proposal was developed through discussion in the MCP Discord's authorization channel, with input from:

  • Aaron Parecki (OAuth/OIDC expertise)
  • Paul Carleton (MCP authorization guidance)
  • Simon Russell (OIDC deployment experience)

SEP-2243 HTTP Header Standardization for Streamable HTTP Transport

Final · Standards Track · Created 2026-02-04

Abstract

This SEP proposes exposing critical routing and context information in standard HTTP header locations for the Streamable HTTP transport. By mirroring key fields from the JSON-RPC payload into HTTP headers, network intermediaries such as load balancers, proxies, and observability tools can route and process MCP traffic without deep packet inspection, reducing latency and computational overhead.

Motivation

Current MCP implementations over HTTP bury all routing information within the JSON-RPC payload. This creates friction for network infrastructure:

  • Load balancers must terminate TLS and parse the entire JSON body to extract routing information (e.g., region, tool name)
  • Proxies and gateways cannot make routing decisions without deep packet inspection
  • Observability tools have limited visibility into MCP traffic patterns
  • Rate limiters and WAFs cannot apply policies based on MCP-specific fields

By exposing key fields in HTTP headers, we enable standard network infrastructure to work with MCP traffic using existing, well-supported mechanisms.

Specification

Standard Headers

The Streamable HTTP transport will require POST requests to include the following headers mirrored from the request body:

Header Name Source Field Required For
Mcp-Method method All requests and notifications
Mcp-Name params.name or params.uri tools/call, resources/read, prompts/get requests

These headers are required for compliance with the MCP version in which they are introduced.

Server Behavior: Servers that process the request body MUST reject requests where the values specified in the headers do not match the values in the request body.

Rationale: This requirement prevents potential security vulnerabilities and error conditions that could arise when different components in the network rely on different sources of truth. For example, a load balancer or gateway might use the header values to make routing decisions, while the MCP server uses the body values for execution. This requirement applies to any network intermediary that processes the message body, as well as the MCP server itself.

Implementation Note: When validating integer parameter values, servers SHOULD compare the header value and the body value numerically rather than as strings (e.g., 42.0 and 42 are considered equal).

Case Sensitivity: Header names (called "field names" in RFC 9110) are case-insensitive. Clients and servers MUST use case-insensitive comparisons for header names.

Example: tools/call Request
POST /mcp HTTP/1.1
Content-Type: application/json
Mcp-Session-Id: 1f3a4b5c-6d7e-8f9a-0b1c-2d3e4f5a6b7c
Mcp-Method: tools/call
Mcp-Name: get_weather

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "location": "Seattle, WA"
    }
  }
}
Example: resources/read Request
POST /mcp HTTP/1.1
Content-Type: application/json
Mcp-Session-Id: 1f3a4b5c-6d7e-8f9a-0b1c-2d3e4f5a6b7c
Mcp-Method: resources/read
Mcp-Name: file:///projects/myapp/config.json

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "resources/read",
  "params": {
    "uri": "file:///projects/myapp/config.json"
  }
}
Example: prompts/get Request
POST /mcp HTTP/1.1
Content-Type: application/json
Mcp-Session-Id: 1f3a4b5c-6d7e-8f9a-0b1c-2d3e4f5a6b7c
Mcp-Method: prompts/get
Mcp-Name: code_review

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "prompts/get",
  "params": {
    "name": "code_review",
    "arguments": {
      "language": "python"
    }
  }
}
Example: Other Request Methods

For requests that don't involve tools, resources, or prompts, only the Mcp-Method header is required:

POST /mcp HTTP/1.1
Content-Type: application/json
Mcp-Method: initialize

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": {
      "name": "ExampleClient",
      "version": "1.0.0"
    }
  }
}
Example: Notification

Notifications also require the Mcp-Method header:

POST /mcp HTTP/1.1
Content-Type: application/json
Mcp-Session-Id: 1f3a4b5c-6d7e-8f9a-0b1c-2d3e4f5a6b7c
Mcp-Method: notifications/initialized

{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}

Custom Headers from Tool Parameters

MCP servers MAY designate specific tool parameters to be mirrored into HTTP headers using an x-mcp-header extension property in the parameter's schema within the tool's inputSchema.

Client Requirement: While the use of x-mcp-header is optional for servers, clients MUST support this feature. When a server's tool definition includes x-mcp-header annotations, conforming clients MUST mirror the designated parameter values into HTTP headers as specified in this document.

Schema Extension

The x-mcp-header property specifies the name portion used to construct the header name Mcp-Param-{name}.

Constraints on x-mcp-header values:

  • MUST NOT be empty
  • MUST match HTTP field-name token syntax (1*tchar, RFC 9110 Section 5.1)
  • MUST NOT contain control characters, including carriage return (CR, \r) or line feed (LF, \n)
  • MUST be case-insensitively unique among all x-mcp-header values in the inputSchema
  • MUST only be applied to parameters with primitive types (integer, string, boolean). Parameters with type number are not permitted. Integer values MUST be within the safe range for JavaScript (−2^53+1 to 2^53−1)
  • MAY be applied to properties at any nesting depth within the inputSchema, not only top-level properties

Clients using the Streamable HTTP transport MUST reject tool definitions where any x-mcp-header value violates these constraints. Rejection means the client MUST exclude the invalid tool from the result of tools/list. Clients SHOULD log a warning when rejecting a tool definition, including the tool name and the reason for rejection. This behavior ensures that a single malformed tool definition does not prevent other valid tools from being used. Clients using other transports (e.g., stdio) MAY ignore x-mcp-header annotations entirely.

Example Tool Definition:

{
  "name": "execute_sql",
  "description": "Execute SQL on Google Cloud Spanner",
  "inputSchema": {
    "type": "object",
    "properties": {
      "region": {
        "type": "string",
        "description": "The region to execute the query in",
        "x-mcp-header": "Region"
      },
      "query": {
        "type": "string",
        "description": "The SQL query to execute"
      }
    },
    "required": ["region", "query"]
  }
}
Example: Geo-Distributed Database

Consider a server exposing an execute_sql tool for Google Cloud Spanner, which requires a region parameter.

Tool Definition:

{
  "name": "execute_sql",
  "description": "Execute SQL on Google Cloud Spanner",
  "inputSchema": {
    "type": "object",
    "properties": {
      "region": {
        "type": "string",
        "description": "The region to execute the query in",
        "x-mcp-header": "Region"
      },
      "query": {
        "type": "string",
        "description": "The SQL query to execute"
      }
    },
    "required": ["region", "query"]
  }
}

Scenario: A client requests to execute SQL in us-west1.

Current Friction: The global load balancer receives the request but must terminate TLS and parse the entire JSON body to find "region": "us-west1" before it knows whether to route the packet to the Oregon or Belgium cluster.

With This Proposal: The client detects the x-mcp-header annotation and automatically adds the header Mcp-Param-Region: us-west1 to the HTTP request. The load balancer can now route based on the header without parsing the body.

Request:

POST /mcp HTTP/1.1
Content-Type: application/json
Mcp-Session-Id: 1f3a4b5c-6d7e-8f9a-0b1c-2d3e4f5a6b7c
Mcp-Method: tools/call
Mcp-Name: execute_sql
Mcp-Param-Region: us-west1

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "execute_sql",
    "arguments": {
      "region": "us-west1",
      "query": "SELECT * FROM users"
    }
  }
}
Example: Multi-Tenant SaaS Application

A SaaS platform exposes tools that operate on different customer tenants. By exposing the tenant ID in a header, the platform can route requests to tenant-specific infrastructure.

Tool Definition:

{
  "name": "query_analytics",
  "description": "Query analytics data for a tenant",
  "inputSchema": {
    "type": "object",
    "properties": {
      "tenant_id": {
        "type": "string",
        "description": "The tenant identifier",
        "x-mcp-header": "TenantId"
      },
      "metric": {
        "type": "string",
        "description": "The metric to query"
      },
      "start_date": {
        "type": "string",
        "description": "Start date for the query range"
      },
      "end_date": {
        "type": "string",
        "description": "End date for the query range"
      }
    },
    "required": ["tenant_id", "metric", "start_date", "end_date"]
  }
}

Request:

POST /mcp HTTP/1.1
Content-Type: application/json
Mcp-Session-Id: 1f3a4b5c-6d7e-8f9a-0b1c-2d3e4f5a6b7c
Mcp-Method: tools/call
Mcp-Name: query_analytics
Mcp-Param-TenantId: acme-corp

{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tools/call",
  "params": {
    "name": "query_analytics",
    "arguments": {
      "tenant_id": "acme-corp",
      "metric": "page_views",
      "start_date": "2026-01-01",
      "end_date": "2026-01-31"
    }
  }
}
Example: Priority-Based Request Handling

A server can expose a priority parameter to allow infrastructure to prioritize certain requests.

Tool Definition:

{
  "name": "generate_report",
  "description": "Generate a complex report",
  "inputSchema": {
    "type": "object",
    "properties": {
      "report_type": {
        "type": "string",
        "description": "Type of report to generate"
      },
      "priority": {
        "type": "string",
        "description": "Request priority: low, normal, or high",
        "x-mcp-header": "Priority"
      }
    },
    "required": ["report_type"]
  }
}

Request:

POST /mcp HTTP/1.1
Content-Type: application/json
Mcp-Session-Id: 1f3a4b5c-6d7e-8f9a-0b1c-2d3e4f5a6b7c
Mcp-Method: tools/call
Mcp-Name: generate_report
Mcp-Param-Priority: high

{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "tools/call",
  "params": {
    "name": "generate_report",
    "arguments": {
      "report_type": "quarterly_summary",
      "priority": "high"
    }
  }
}

Header Processing

Value Encoding

Clients MUST encode parameter values before including them in HTTP headers to ensure safe transmission and prevent injection attacks.

Character Restrictions

Per RFC 9110, HTTP header field values must consist of visible ASCII characters (0x21-0x7E), space (0x20), and horizontal tab (0x09). The following characters are explicitly prohibited:

  • Carriage return (\r, 0x0D)
  • Line feed (\n, 0x0A)
  • Null character (\0, 0x00)
  • Any character outside the ASCII range (> 0x7F)

Whitespace Handling

HTTP parsers typically trim leading and trailing whitespace from header values. To preserve leading and trailing spaces in parameter values, clients MUST use Base64 encoding when the value:

  • Starts with a space (0x20) or horizontal tab (0x09)
  • Ends with a space (0x20) or horizontal tab (0x09)

Encoding Rules

Clients MUST apply the following encoding rules in order:

  1. Type conversion: Convert the parameter value to its string representation:

    • string: Use the value as-is
    • integer: Convert to decimal string representation (e.g., 42, -7)
    • boolean: Convert to lowercase "true" or "false"
  2. Whitespace check: If the string starts or ends with whitespace (space or tab):

    • Apply Base64 encoding (see below)
  3. ASCII validation: Check if the string contains only valid ASCII characters (0x20-0x7E):

    • If valid, proceed to step 4
    • If invalid (contains non-ASCII characters), apply Base64 encoding (see below)
  4. Control character check: If the string contains any control characters (0x00-0x1F or 0x7F):

    • Apply Base64 encoding (see below)

Base64 Encoding for Unsafe Values

When a value cannot be safely represented as a plain ASCII header value, clients MUST use Base64 encoding of the UTF-8 representation of the value with the following format:

Mcp-Param-{Name}: =?base64?{Base64EncodedValue}?=

The prefix =?base64? and suffix ?= indicate that the value is Base64-encoded. These markers are case-sensitive and MUST appear exactly as shown (lowercase). Servers and intermediaries that need to inspect these values MUST decode them accordingly.

To avoid ambiguity, clients MUST also Base64-encode any plain-ASCII value that matches the sentinel pattern (i.e., starts with =?base64? and ends with ?=).

Examples:

Original Value Reason Encoded Header Value
"us-west1" Plain ASCII Mcp-Param-Region: us-west1
"Hello, 世界" Contains non-ASCII Mcp-Param-Greeting: =?base64?SGVsbG8sIOS4lueVjA==?=
" padded " Leading/trailing spaces Mcp-Param-Text: =?base64?IHBhZGRlZCA=?=
"line1\nline2" Contains newline Mcp-Param-Text: =?base64?bGluZTEKbGluZTI=?=
"=?base64?literal?=" Matches sentinel pattern Mcp-Param-Val: =?base64?PT9iYXNlNjQ/bGl0ZXJhbD89?=
Client Behavior

When constructing a tools/call request via HTTP transport, the client MUST:

  1. Extract the values for any standard headers from the request body (e.g., method, params.name, params.uri)
  2. Append the Mcp-Method header and, if applicable, Mcp-Name header to the request
  3. Inspect the tool's inputSchema for properties marked with x-mcp-header and extract the value for each parameter
  4. Encode the values according to the rules in Value Encoding
  5. Append a Mcp-Param-{Name}: {Value} header to the request:

Implementation Note: Clients MUST construct Mcp-Param-* headers using the most recently obtained inputSchema for the tool. A client that has never obtained the tool's inputSchema SHOULD send the request without Mcp-Param-* headers. If the server rejects the request because required Mcp-Param-* headers are missing or do not match the body, the client SHOULD call tools/list to obtain the current inputSchema, then retry the original request with the appropriate headers. Clients MAY pre-load tool definitions via other means (e.g., from a previous session or configuration) to enable header emission without a prior tools/list call.

Server Behavior

When receiving a request, the server MUST reject requests with Mcp-Param-{Name} headers that contain invalid characters (see "Character Restrictions" in the Value Encoding section).

Any server that processes the message body (not simply forwarding it) MUST validate that encoded header values, after decoding if Base64-encoded, match the corresponding values in the request body. Servers MUST reject requests with a 400 Bad Request HTTP status if any validation fails.

Error Code

When rejecting a request due to header validation failure, servers MUST return a JSON-RPC error response with the following error code:

Code Name Description
-32001 HeaderMismatch The HTTP headers do not match the corresponding values in the request body, or required headers are missing/malformed.

This error code is in the JSON-RPC implementation-defined server error range (-32000 to -32099).

Error Response Format:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32001,
    "message": "Header mismatch: Mcp-Name header value 'foo' does not match body value 'bar'"
  }
}

Validation Failure Conditions:

  • A required standard header (Mcp-Method, Mcp-Name, etc.) is missing
  • A header value does not match the request body value
  • A Base64-encoded value cannot be decoded
  • A header value contains invalid characters

Note: Intermediaries MUST return an appropriate HTTP error status (e.g., 400 Bad Request) for validation failures but are not required to return a JSON-RPC error response.

Note: Intermediaries that enforce policy based on mirrored headers (e.g., routing or rate-limiting by tenant) SHOULD verify that the MCP-Protocol-Version header indicates a version that requires header–body validation. If the version is older or the header is absent, the intermediary SHOULD reject the request rather than trusting unvalidated header values.

Custom Header Handling:

Custom headers (those defined via x-mcp-header) follow the same validation rules as standard headers:

Scenario Client Behavior Server Behavior
Parameter value provided Client MUST include the header Server MUST validate header matches body
Parameter value is null Client MUST omit the header Server MUST NOT expect the header
Parameter not in arguments Client MUST omit the header Server MUST NOT expect the header
Client omits header but value is in body Non-conforming client Server MUST reject the request

When rejecting requests due to missing or invalid custom headers, the server MUST return HTTP status 400 Bad Request with JSON-RPC error code -32001 (HeaderMismatch).

Rationale

Headers vs Path

This proposal mirrors request data into headers rather than encoding it in the URL path.

Advantages of Headers:

  1. Simplicity: All widely-used network load balancers support routing based on HTTP headers
  2. Multi-version support: Easier to support multiple MCP versions in clients and servers
  3. Compatibility: Headers work with the existing Streamable HTTP transport design without changing the endpoint structure
  4. Unlimited values: Header values can contain characters that would require encoding in URLs (e.g., /, ?, #)
  5. No URL length limits: Very long values can be transmitted without hitting URL length restrictions

Advantages of Path-based Routing:

  1. Framework simplicity: Many web frameworks (Flask, Express, Django, Rails) have built-in support for path-based routing with minimal configuration
  2. Logging: URL paths are typically logged by default, making debugging easier

Trade-offs and Framework Considerations:

Framework Header-based Routing Path-based Routing
Flask (Python) Requires middleware or decorators to extract headers before routing Native support via @app.route('/mcp/<method>')
Express (Node.js) Easy via req.headers but requires custom routing logic Native support via app.post('/mcp/:method')
Django (Python) Requires custom middleware Native URL patterns
Go (net/http) Easy via r.Header.Get() Native via path patterns
ASP.NET Core Easy via [FromHeader] attribute Native via route templates

For frameworks like Flask that strongly favor path-based routing, implementing header-based routing requires additional code:

# Flask example: Header-based routing requires manual dispatch
@app.route('/mcp', methods=['POST'])
def mcp_handler():
    method = request.headers.get('Mcp-Method')
    if method == 'tools/call':
        return handle_tools_call(request)
    elif method == 'resources/read':
        return handle_resources_read(request)
    # ... etc

Despite this additional complexity in some frameworks, header-based routing was chosen because:

  1. Backwards Compatibility introducing path based routing would require all existing MCP Servers to take a major update, and potentially support two sets of endpoints to support multiple versions. Even if the SDKs can paper over this additional operational concerns like testing, metrics, etc would need to happen. Header based routing requires minimal client side changes. And clients which don't opt in will still function correctly.

  2. Infrastructure benefits outweigh framework complexity: The primary goal is enabling network infrastructure (load balancers, proxies, WAFs) to route and process requests without body parsing. This benefit applies regardless of the server framework.

Infrastructure Support

HTTP header-based routing and processing is supported by:

  • Load Balancers: All major load balancers (HAProxy, NGINX, Cloudflare, F5, Envoy/Istio)
  • Rate Limiting: 9 of 11 popular rate-limiting solutions
  • Authorization: Kong, Tyk, AWS API Gateway, Google Cloud Apigee, Azure API Gateway, NGINX, Apache APISIX, Istio/Envoy
  • Web Application Firewalls: Cloudflare WAF, AWS WAF, Azure WAF, F5 Advanced WAF, FortiWeb, Imperva WAF, Barracuda WAF, ModSecurity, Akamai, Wallarm
  • Observability: Most observability solutions can extract data from HTTP headers

Explicit Header Names in x-mcp-header

The design uses an explicit name value in x-mcp-header rather than deriving the header name from the parameter name because:

  1. Case sensitivity mismatch: Header names are case-insensitive, but JSON Schema property names are case-sensitive
  2. Character set constraints: Header names are limited to ASCII characters, but tool parameter names may contain arbitrary Unicode
  3. Simplicity: No complex scheme needed for constructing header names from nested properties

Placement Within JSON Schema

The x-mcp-header extension is placed directly within the JSON Schema of the property to be mirrored, rather than in a separate metadata field outside the schema. This design choice offers several advantages:

  1. Co-location: The header mapping is defined alongside the property it affects, making it immediately clear which parameter will be mirrored. Developers don't need to cross-reference between the schema and a separate metadata structure.

  2. Established pattern: JSON Schema explicitly supports extension keywords (properties starting with x-), and this pattern is widely used in ecosystems like OpenAPI. Tool authors and SDK developers are already familiar with this approach.

  3. Schema composability: When schemas are composed, extended, or referenced using $ref, the x-mcp-header annotation travels with the property definition. A separate metadata structure would require complex synchronization logic to maintain consistency.

  4. Tooling compatibility: Existing JSON Schema validators ignore unknown keywords by default, so adding x-mcp-header doesn't break existing schema validation. Tools that don't understand this extension simply skip it.

  5. Reduced complexity: A separate metadata structure would require defining a mapping mechanism (e.g., JSON Pointer or property paths) to associate headers with properties, adding implementation complexity and potential for errors.

Scope: Tools Only

The x-mcp-header mechanism currently applies only to tools/call requests because tools are the only MCP primitive with an inputSchema that supports JSON Schema extension keywords. Resources and prompts lack an equivalent schema structure: resources/read takes only a uri (already exposed via Mcp-Name), and prompts/get defines arguments as a simple {name, description, required} array without JSON Schema extensibility. Generalizing custom header mapping to these primitives would require adding inputSchema-style definitions to resources and prompts, which is a larger specification change. This is noted as a potential future extension.

No Specification-Level Header Size Limit

This specification intentionally does not define limits on individual header value length, total MCP header size, or number of custom headers. Headers are solely an HTTP concept, and HTTP itself (RFC 9110) does not specify header size limits. Common HTTP infrastructure imposes its own limits — ranging from 4–8 KB on some servers (e.g., Apache at ~8190 bytes) to 128 KB on others (e.g., Cloudflare) — but the appropriate limit depends on the deployment environment, which only the service operator can determine.

Defining a specification-level limit (such as "omit headers exceeding 8192 bytes") would introduce problems:

  1. Arbitrary threshold: Any chosen value would be too low for some deployments and irrelevant for others. The "right" limit varies by infrastructure.
  2. Counterproductive omission: If a client omits a header because it exceeds a spec-defined limit, servers and intermediaries that rely on that header for routing must either parse the body or reject the request — undermining the core purpose of exposing values in headers.
  3. Unnecessary SDK burden: SDK maintainers would need to implement and test limit-checking logic for a constraint that rarely applies in practice.
  4. Redundant with HTTP: Servers and intermediaries already reject oversized headers using standard HTTP status codes (413 Request Entity Too Large, 431 Request Header Fields Too Large), which clients must handle regardless.

Note to implementers: Servers, intermediaries, and clients MAY independently impose limits on individual header size, total MCP header size, or number of custom headers as appropriate for their deployment environment. Servers SHOULD document any limits they impose. Clients SHOULD gracefully handle 413 Request Entity Too Large or 431 Request Header Fields Too Large responses. Tool authors SHOULD limit x-mcp-header annotations to parameters that provide clear infrastructure benefits.

Encoding Approach for Unsafe Values

Four approaches were considered for encoding parameter values that cannot be safely represented as plain ASCII header values (non-ASCII characters, leading/trailing whitespace, control characters):

  1. Sentinel wrapping (chosen approach): Use the =?base64?{value}?= prefix/suffix within the same Mcp-Param-{Name} header to signal Base64-encoded values.

  2. Separate header name: Use a distinct header name for encoded values, e.g. Mcp-ParamEncoded-{Name}, so the encoding is indicated by the header name rather than the value format.

  3. Implicit encoding: Let the parser infer encoding from the tool schema, e.g. via a "x-mcp-header-encoding": "base64" annotation in the tool definition.

  4. Always encode: Base64-encode every Mcp-Param-{Name} value unconditionally.

Approach Pros Cons
Sentinel wrapping Single header name per parameter; common case (plain ASCII) is human-readable; intermediaries can route on plain values without decoding In-band signaling can theoretically collide with literal values; every reader must check for the prefix
Separate header name No in-band ambiguity; encoding is self-documenting from the header name Doubles the header namespace; every intermediary must check two header names per parameter; needs a conflict rule if both are present
Implicit encoding Simplest wire format; no sentinels or extra headers Intermediaries need access to the tool schema to know whether to decode — defeats the purpose of exposing values in headers; static per-parameter decision doesn't handle the mixed case well
Always encode Simplest rules; no conditional logic or ambiguity Plain ASCII values become unreadable; intermediaries must decode Base64 to inspect any value, significantly undermining the core motivation of this SEP

Conclusion: The sentinel wrapping approach provides the best trade-off. The primary use case for custom headers is enabling intermediaries to route and filter on simple, readable values like region names and tenant IDs — these are invariably plain ASCII and never trigger Base64 encoding. Option 4 makes all values opaque to intermediaries. Option 3 leaves intermediaries unable to distinguish encoded from literal values without access to the tool schema. Option 2 eliminates in-band ambiguity but doubles the header namespace, requiring intermediaries to check two possible header names per parameter and adding a conflict rule when both are present. The theoretical collision risk of the sentinel in Option 1 is negligible since =?base64?...?= is an unlikely literal parameter value in practice.

Backward Compatibility

Standard Headers

Existing clients and SDKs will be required to include the standard headers when using the new MCP version. This is a minor addition since clients already include headers like Mcp-Protocol-Version, adding only one or two new headers per message.

Servers implementing the new version MUST reject requests missing required headers. Servers MAY support older clients by accepting requests without headers when negotiating an older protocol version.

Custom Headers from Tool Parameters

The x-mcp-header extension is optional for servers. Existing tools without this property continue to work unchanged. However, clients implementing the MCP version that includes this specification MUST support the feature. Older clients that do not support x-mcp-header will still function but will not provide the header-based routing benefits that servers may depend on.

Security Implications

Header Injection

Header injection attacks occur when malicious values containing control characters (especially \r\n) are included in headers, potentially allowing attackers to inject additional headers or terminate the header section early.

Clients MUST follow the Value Encoding rules defined in this specification. These rules ensure that:

  • Control characters are never included in header values
  • Non-ASCII values are safely encoded using Base64
  • Values exceeding safe length limits are omitted

Header Spoofing

Servers MUST validate that header values match the corresponding values in the request body. This prevents clients from sending mismatched headers to manipulate routing while executing different operations.

For example, a malicious client could attempt to:

  • Route a request to a less-secured region while executing operations intended for a high-security region
  • Bypass rate limits by spoofing tenant identifiers
  • Evade security policies by misrepresenting the operation being performed

Information Disclosure

Tool parameter values designated for headers will be visible to network intermediaries (load balancers, proxies, logging systems). Server developers:

  • SHOULD NOT mark sensitive parameters (passwords, API keys, tokens, PII) with x-mcp-header
  • SHOULD document which parameters are exposed as headers
  • SHOULD consider that Base64 encoding provides no confidentiality—it is merely an encoding, not encryption

Trusting Header Values

Header values originate from tool call arguments, which may be influenced by an LLM or a malicious client. Intermediaries and servers MUST NOT treat these values as trusted input for security-sensitive decisions. In particular:

  • Header values that imply access to specific resources (e.g., tenant IDs, region names) MUST be independently verified against the authenticated user's permissions before granting access to those resources.
  • Header values MUST NOT be used as the sole basis for granting elevated privileges without server-side enforcement of rate limits and quotas.
  • Deployments SHOULD reject requests with oversized or excessive headers early in the pipeline — before performing Base64 decoding or body parsing — to mitigate denial-of-service risks from crafted payloads.

Conformance Test Cases

This section defines edge cases that conformance tests MUST cover to ensure interoperability between implementations.

Standard Header Edge Cases

Case Sensitivity
Test Case Input Expected Behavior
Header name case variation mcp-method: tools/call Server MUST accept (header names are case-insensitive)
Header name mixed case MCP-METHOD: tools/call Server MUST accept
Method value case Mcp-Method: TOOLS/CALL Server MUST reject (method values are case-sensitive)
Header/Body Mismatch
Test Case Header Value Body Value Expected Behavior
Method mismatch Mcp-Method: tools/call "method": "prompts/get" Server MUST reject with 400 and error code -32001
Tool name mismatch Mcp-Name: foo "params": {"name": "bar"} Server MUST reject with 400 and error code -32001
Missing required header (no Mcp-Method) Valid body Server MUST reject with 400 and error code -32001
Extra whitespace in header Mcp-Name: foo "params": {"name": "foo"} Server MUST accept (trim whitespace per HTTP spec)
Special Characters in Values
Test Case Value Expected Behavior
Tool name with hyphen my-tool-name Client sends as-is; server accepts
Tool name with underscore my_tool_name Client sends as-is; server accepts
Resource URI with special chars file:///path/to/file%20name.txt Client sends as-is; server accepts
Resource URI with query string https://example.com/resource?id=123 Client sends as-is; server accepts

Custom Header Edge Cases

x-mcp-header Name Conflicts
Test Case Schema Expected Behavior
Duplicate header names (same case) Two properties with "x-mcp-header": "Region" Client MUST reject tool definition
Duplicate header names (different case) "x-mcp-header": "Region" and "x-mcp-header": "REGION" Client MUST reject tool definition (case-insensitive uniqueness)
Header name matches standard header "x-mcp-header": "Method" Allowed (produces Mcp-Param-Method, not Mcp-Method)
Empty header name "x-mcp-header": "" Client MUST reject tool definition
Invalid x-mcp-header Values
Test Case x-mcp-header Value Expected Behavior
Contains space "x-mcp-header": "My Region" Client MUST reject tool definition
Contains colon "x-mcp-header": "Region:Primary" Client MUST reject tool definition
Contains non-ASCII "x-mcp-header": "Région" Client MUST reject tool definition
Contains control character "x-mcp-header": "Region\t1" Client MUST reject tool definition
Value Encoding Edge Cases
Test Case Parameter Value Expected Header Value
Plain ASCII string "us-west1" Mcp-Param-Region: us-west1
String with leading space " us-west1" Mcp-Param-Region: =?base64?IHVzLXdlc3Qx?=
String with trailing space "us-west1 " Mcp-Param-Region: =?base64?dXMtd2VzdDEg?=
String with leading/trailing spaces " us-west1 " Mcp-Param-Region: =?base64?IHVzLXdlc3QxIA==?=
String with internal spaces only "us west 1" Mcp-Param-Region: us west 1
Boolean true true Mcp-Param-Flag: true
Boolean false false Mcp-Param-Flag: false
Integer 42 Mcp-Param-Count: 42
Floating point 3.14159 Mcp-Param-Value: 3.14159
Non-ASCII characters "日本語" Mcp-Param-Text: =?base64?5pel5pys6Kqe?=
String with newline "line1\nline2" Mcp-Param-Text: =?base64?bGluZTEKbGluZTI=?=
String with carriage return "line1\r\nline2" Mcp-Param-Text: =?base64?bGluZTENCmxpbmUy?=
String with leading tab "\tindented" Mcp-Param-Text: =?base64?CWluZGVudGVk?=
Empty string "" Mcp-Param-Name: (empty value)
Type Restriction Violations
Test Case Property Type x-mcp-header Present Expected Behavior
Array type "type": "array" Yes Server MUST reject tool definition
Object type "type": "object" Yes Server MUST reject tool definition
Null type "type": "null" Yes Server MUST reject tool definition
Nested property Property inside object Yes Server MUST reject tool definition

Server Validation Edge Cases

Base64 Decoding
Test Case Header Value Expected Behavior
Valid Base64 =?base64?SGVsbG8=?= Server decodes to "Hello" and validates
Invalid Base64 padding =?base64?SGVsbG8?= Server MUST reject with 400 and error code -32001; Intermediary MAY reject with 400 status code
Invalid Base64 characters =?base64?SGVs!!!bG8=?= Server MUST reject with 400 and error code -32001; Intermediary MAY reject with 400 status code
Missing prefix SGVsbG8= Server treats as literal value, not Base64
Missing suffix =?base64?SGVsbG8= Server treats as literal value, not Base64
Non-lowercase prefix =?BASE64?SGVsbG8=?= Server treats as literal value, not Base64
Null and Missing Values
Test Case Scenario Expected Behavior
Parameter with x-mcp-header is null "region": null Client MUST omit header
Parameter with x-mcp-header is missing Parameter not in arguments Client MUST omit header
Optional parameter present Optional parameter provided Client MUST include header
Missing Custom Header with Value in Body
Test Case Header Present Body Value Expected Behavior
Standard header omitted, value in body No Mcp-Name "params": {"name": "foo"} Server MUST reject with 400 and error code -32001; Intermediary MAY reject with 400 status code
Custom header omitted, value in body No Mcp-Param-Region "region": "us-west1" Server MUST reject with 400 and error code -32001; Intermediary MAY reject with 400 status code

Reference Implementation

To be provided before this SEP reaches Final status.

Implementation requirements:

  • Server SDKs: Provide a mechanism (attribute/decorator) for marking parameters with x-mcp-header
  • Client SDKs: Implement the client behavior for extracting and encoding header values
  • Validation: Both sides must validate header/body consistency

Changes since SEP became Final

This SEP is preserved as a historical record of what was accepted. The list below tracks changes made to the specification after this SEP reached Final status. Refer to the current specification for the authoritative, up-to-date requirements.

  • HeaderMismatch error code reassigned from -32001 to -32020. This SEP originally assigned HeaderMismatch to -32001. The error-code allocation update in #2907 reassigned HeaderMismatch to -32020. All references to -32001 above should be read as -32020 when implementing against the current specification.

SEP-2260 Require Server requests to be associated with a Client request.

Final · Standards Track · Created 2026-02-16

Abstract

This SEP clarifies that roots/list, sampling/createMessage, and elicitation/create requests MUST be associated with an originating client-to-server request (e.g., during tools/call, resources/read, or prompts/get processing). Standalone server-initiated requests of these types outside notifications MUST NOT be implemented.

Although not enforced in the current MCP Data Layer, logically these requests MUST be associated with a valid client-to-server JSON-RPC Request Id.

The operational server-to-client Ping is excepted from this restriction.

Motivation

Current Specification

The current specification uses SHOULD language in the transport layer:

In context of responding to a POST Request in the Streamable HTTP transport (2025-11-25/basic/transports.mdx:121-L123):

  • "The server MAY send JSON-RPC requests and notifications before sending the JSON-RPC response. These messages SHOULD relate to the originating client request."

For the optional GET SSE Stream (2025-11-25/basic/transports.mdx:146-L148):

  • "The server MAY send JSON-RPC requests and notifications on the stream."
  • "These messages SHOULD be unrelated to any concurrently-running JSON-RPC request from the client."

Although the GET stream allows "unsolicited" requests, its use is entirely optional and cannot be relied upon by MCP Server authors.

Design Intent

The design intent of MCP Server Requests is to operate reactively nested within other MCP operations:

  • Sampling enables servers to request LLM assistance while processing a tool call, resource request, or prompt
  • Elicitation enables servers to gather additional user input needed to complete an operation
  • List Roots enables servers to identify shared storage locations

Ping has a special status as it is primarily intended as a keep-alive/health-check mechanism.

For Streamable HTTP Servers this enables SSE Streams to be maintained for extended periods if no Notifications or Requests are available to be sent. For client-to-server Requests they are associable. Future transport implementations will remove the need for dissociated Pings.

The current specification already describes this pattern:

"Sampling in MCP allows servers to implement agentic behaviors, by enabling LLM calls to occur nested inside other MCP server features."

However, the normative requirements don't enforce this constraint.

Simplification Benefits

Making this constraint explicit:

  1. Simplifies transport implementations - Transports don't need to support arbitrary server-initiated request/response flows, which require a persistent connection from Server to Client; they only need request-scoped bidirectional communication
  2. Clarifies user experience - Users understand that sampling/elicitation happens because they initiated an action, not spontaneously
  3. Reduces security surface - Ensures client has context for what scope the additional requested information will be used for. This allows clients to make better informed decisions on whether to provide the requested info.
  4. Aligns with practice - Based on a scan of GitHub all existing implementations already follow this pattern, except one repo owned by the SEP author with a contrived scenario.

Specification Changes

1. Add Warning Blocks to Feature Documentation

In client/sampling.mdx (after existing security warning):

<Warning>

**Request Association Requirement**

Servers **MUST** send `sampling/createMessage` requests only in association with an originating client request (e.g., during `tools/call`, `resources/read`, or `prompts/get` processing).

Standalone server-initiated sampling on independent communication streams (unrelated to any client request) is not supported and **MUST NOT** be implemented. Future transport implementations are not required to support this pattern.

</Warning>

In client/elicitation.mdx (after existing security warning):

<Warning>

**Request Association Requirement**

Servers **MUST** send server-to-client requests (such as `roots/list`,
`sampling/createMessage`, or `elicitation/create`) only in association with an
originating client request (e.g., during `tools/call`, `resources/read`, or
`prompts/get` processing).

Standalone server-initiated requests of these types on independent
communication streams (unrelated to any client request) are not supported and
**MUST NOT** be implemented. Future transport implementations are not required
to support this pattern.

</Warning>

In client/roots.mdx (in User Interaction Model section):

<Warning>

Servers **MUST** send server-to-client requests (such as `roots/list`,
`sampling/createMessage`, or `elicitation/create`) only in association with an
originating client request (e.g., during `tools/call`, `resources/read`, or
`prompts/get` processing).

Standalone server-initiated requests of these types on independent
communication streams (unrelated to any client request) are not supported and
**MUST NOT** be implemented. Future transport implementations are not required
to support this pattern.

</Warning>

In basic/utilities/ping.mdx (In Overview section):

<Warning>

`ping` is an MCP-level liveness check and **MAY** be sent by either party at
any time on an established session/connection.

In Streamable HTTP, implementations **SHOULD** prefer transport-level SSE
keepalive mechanisms for idle-connection maintenance; `ping` remains available
for protocol-level responsiveness checks.

Request-association requirements for `roots/list`, `sampling/createMessage`,
and `elicitation/create` do not apply to `ping`.

</Warning>

2. Clarify Transport Layer Constraints

In basic/transports.mdx, POST-initiated SSE streams (line ~121):

- The server **MAY** send JSON-RPC _requests_ and _notifications_ before sending the
- JSON-RPC _response_. These messages **SHOULD** relate to the originating client
- _request_.
+ The server **MAY** send JSON-RPC _requests_ and _notifications_ before sending the
+ JSON-RPC _response_. These messages **MUST** relate to the originating client
+ _request_.

In basic/transports.mdx, GET-initiated standalone SSE streams (line ~147):

- The server **MAY** send JSON-RPC _requests_ and _notifications_ on the stream.
- These messages **SHOULD** be unrelated to any concurrently-running JSON-RPC
- _request_ from the client.
+ The server **MAY** send JSON-RPC _notifications_ and _pings_ on the stream.
+ These messages **SHOULD** be unrelated to any concurrently-running JSON-RPC
+ _request_ from the client, **except** that `roots/list`,
+ `sampling/createMessage`, and `elicitation/create` requests **MUST NOT** be
+ sent on standalone streams.

Backward Compatibility

Impact Assessment

This change is expected to have minimal to no impact on existing implementations:

  1. Common usage patterns are preserved - Sampling/elicitation within tool execution, resource reading, and prompt handling remain fully supported
  2. No known implementations affected - Research conducted on GitHub has shown only one implementation of this pattern. This singular implementation is owned by the SEP author.

What's Disallowed

The following pattern, which was never explicitly documented or recommended, is now explicitly prohibited:

# ❌ PROHIBITED: Standalone server push
async def background_task():
    while True:
        await asyncio.sleep(60)
        # Try to initiate sampling without any client request context
        await session.create_message(...)  # NOT ALLOWED

What Remains Supported

The canonical pattern remains fully supported:

# ✅ SUPPORTED: Sampling during tool execution
@mcp.tool()
async def analyze_data(data: str, ctx: Context) -> str:
    # Request LLM analysis while processing the tool call
    result = await ctx.session.create_message(
        messages=[SamplingMessage(role="user", content=...)]
    )
    return result.content.text

Implementation Guidance

For Server Implementers

No changes required if your server:

  • Only uses server-to-client requests within tool handlers
  • Only uses server-to-client requests within resource/prompt handlers
  • Uses server-to-client requests synchronously as part of processing a client request

Changes required if your server:

  • Attempts to initiate server-to-client requests on standalone HTTP GET streams
  • Attempts to send server-to-client requests requests independent of client operations
  • Has background tasks that try to invoke server-to-client requests

Alternative designs will need to be implemented for the "Changes Required" case.

Implementors performing unsolicited server-to-client requests (typically URL Elicitation) immediately following initialization are encouraged to lazily perform these requests within the scope of a client-to-server request that requires that information from the client.

Timeout Considerations

When an MCP Server initiates a "nested" request inside a client request, the duration of the parent request extends to include the user's response time.

Implementers MUST ensure that:

  1. Transport timeouts (e.g. HTTP Request Timeout) are sufficient to accommodate "Human-in-the-loop" delays, which may be unbounded.
  2. Short timeouts enforced by infrastructure (e.g. Load Balancers) may result in connection termination before the user responds. For Streamable HTTP, transport-level SSE keepalive mechanisms SHOULD be used to keep connections alive and reset timers; ping requests MAY additionally be used for protocol-level responsiveness checks.

For Client Implementers

No changes required - Clients should already handle sampling/elicitation requests in the context of their own outbound requests. Potential to simplify implementations if out-of-band is currently supported.

Clients receiving server-to-client requests with no associated outbound request SHOULD respond with a -32602 (Invalid Params) error.

For Transport Implementers

Future transport implementations can rely on the guarantee that:

  • Sampling/elicitation requests only occur within the scope of a client-initiated request
  • Transports don't need to support arbitrary server-initiated request/response flows on standalone channels
  • Request correlation and lifecycle management is simplified

Timeline

(This SEP intends to serve as a public notice of the change prior to future protocol versions that will not be compatible with this usage)

Alternatives Considered

1. Soft Deprecation

Use SHOULD NOT language to discourage but not prohibit the pattern.

Rejected because: The behavior was never intentionally supported, and leaving it ambiguous prevents transport simplification.

2. Keep Current Ambiguity

Leave the existing SHOULD language unchanged.

Rejected because: This blocks future transport implementations and leaves implementers uncertain about whether the pattern is supported.

3. Create a Capability Flag

Add a sampling.standalone or similar capability for servers that want this behavior.

Rejected because: This adds complexity for a use case with no known demand, and contradicts the "nested" design principle.

References

  • Current sampling documentation: /specification/draft/client/sampling.mdx
  • Current elicitation documentation: /specification/draft/client/elicitation.mdx
  • Transport specification: /specification/draft/basic/transports.mdx
  • User interaction model discussion in client concepts documentation

SEP-2293 Enhanced Completion Values with Rich Metadata Support

Draft · Standards Track · Created 2025-09-08

Imported from upstream. Proposed as modelcontextprotocol#2293 by @evalstate on 2026-02-22, 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 extending the MCP completion system to support rich metadata in completion values. Currently, CompleteResult.completion.values is limited to string[], which prevents servers from providing contextual display information alongside completion options. This proposal introduces a CompletionValue object type and changes values to Array<string | CompletionValue>, where CompletionValue carries a required value string (the actual argument value), a required title string (the human-readable display label), and an optional description string (tooltip or help text). The union type preserves full backward compatibility: existing servers returning plain strings require no changes, while new servers can adopt rich objects incrementally. Clients that understand CompletionValue SHOULD display title to users and submit value in subsequent requests; clients that do not understand CompletionValue can degrade gracefully by using the value field directly.

Motivation

The current completion system only allows servers to return plain strings in CompleteResult.completion.values. This creates several concrete problems:

  1. Opaque identifiers: When completion values are internal identifiers (e.g., database IDs, version slugs like py-3.11), users see cryptic strings with no context about what they represent.

  2. No display/value separation: There is no way to show a human-readable label (e.g., "Python 3.11") to the user while submitting a different technical value (e.g., "py-3.11") to the server. Servers are forced to either expose IDs directly or encode metadata into the string itself.

  3. No inline documentation: Rich IDE-style completion interfaces support descriptions and tooltips. These cannot currently be expressed in MCP responses.

  4. Inconsistent client UX: Without metadata, each client must independently decide how to render completions, leading to inconsistent experiences across MCP clients.

Real-world cases demonstrating the need:

  • A journaling server returns numeric entry IDs as completions; users must know which ID maps to which entry.
  • A runtime environment server returns version slugs; "py-3.11" is less clear to users than "Python 3.11".
  • VS Code's completion UI can display a label and description alongside a value; MCP cannot currently supply these (demonstrated by @connor4312).

Specification

New Type: CompletionValue

A CompletionValue object provides display metadata alongside a completion value:

interface CompletionValue {
  /** The actual value submitted in argument context for subsequent requests. */
  value: string;
  /** Human-readable display label shown to the user in completion UIs. */
  title: string;
  /** Optional additional context, shown as a tooltip or help text. */
  description?: string;
}

All fields are strings. The value field is what gets placed into context.arguments in subsequent completion/complete requests.

Change to CompleteResult

CompleteResult.completion.values changes from:

values: string[]

to:

values: Array<string | CompletionValue>;

The maximum of 100 items per response is unchanged.

Protocol Examples

All plain strings (unchanged, backward compatible)
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "completion": {
      "values": ["python", "pytorch", "pyside"],
      "total": 10,
      "hasMore": true
    }
  }
}
All rich objects
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "completion": {
      "values": [
        {
          "value": "py-3.11",
          "title": "Python 3.11",
          "description": "Python programming language, version 3.11"
        },
        {
          "value": "torch-2.0",
          "title": "PyTorch 2.0",
          "description": "Open source machine learning framework"
        },
        {
          "value": "pyside-6",
          "title": "PySide 6"
        }
      ],
      "total": 3,
      "hasMore": false
    }
  }
}
Mixed (plain strings and rich objects in the same response)
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "completion": {
      "values": [
        "python",
        {
          "value": "torch-2.0",
          "title": "PyTorch 2.0",
          "description": "Open source machine learning framework"
        },
        "pyside"
      ],
      "total": 3,
      "hasMore": false
    }
  }
}

Client Behaviour

Clients that receive CompletionValue objects:

  1. MUST use the value field (not title) when constructing subsequent completion/complete requests (i.e., in context.arguments or as the argument value).
  2. SHOULD display the title field to users in place of the raw value when rendering completion suggestions.
  3. SHOULD display the description field as supplemental information (e.g., tooltip, help text) when it is present.
  4. SHOULD search or filter completion suggestions against both the value and title fields when performing client-side filtering, so that users typing either the display name or the technical value can find matches.

Clients that do not yet recognise CompletionValue objects MAY fall back to displaying the value field directly.

Server Behaviour

Servers:

  1. MAY return only plain strings (existing behaviour, no changes required).
  2. MAY return only CompletionValue objects.
  3. MAY mix plain strings and CompletionValue objects in the same response.
  4. SHOULD be aware that clients MAY filter on title as well as value; servers performing server-side prefix matching SHOULD match against both fields, or document which field they match against.
  5. MUST ensure that value fields in CompletionValue objects are strings. They MUST NOT be numbers or other non-string types.

Schema Changes

schema.ts (draft)

Add the new interface:

/**
 * A completion value with optional display metadata.
 *
 * @category `completion/complete`
 */
export interface CompletionValue {
  /** The actual completion value submitted in subsequent requests. */
  value: string;
  /** Human-readable display label for the completion option. */
  title: string;
  /** Optional additional context shown as tooltip or help text. */
  description?: string;
}

Change CompleteResult.completion.values:

// Before
values: string[];

// After
values: Array<string | CompletionValue>;
schema.json (draft)

Add CompletionValue definition:

"CompletionValue": {
  "description": "A completion value with optional display metadata.",
  "type": "object",
  "properties": {
    "value": {
      "type": "string",
      "description": "The actual completion value submitted in subsequent requests."
    },
    "title": {
      "type": "string",
      "description": "Human-readable display label for the completion option."
    },
    "description": {
      "type": "string",
      "description": "Optional additional context shown as tooltip or help text."
    }
  },
  "required": ["value", "title"],
  "additionalProperties": false
}

Change the values items definition in CompleteResult:

"values": {
  "description": "An array of completion values. Must not exceed 100 items.",
  "type": "array",
  "items": {
    "anyOf": [
      { "$ref": "#/$defs/CompletionValue" },
      { "type": "string" }
    ]
  }
}

Rationale

Union type rather than always-object

Requiring all items to be CompletionValue objects would be a breaking change for existing servers. The string | CompletionValue union allows both old and new servers to work without modification, and allows mixed arrays so servers can provide rich metadata only where useful.

value + title rather than a single display field

A single optional label field alongside a plain string was considered, but the union approach makes field semantics clearer: value is always what gets submitted to the server; title is always what gets displayed. This avoids ambiguity when a value happens to look like a human-readable label.

Three fields only (value, title, description)

Additional fields (e.g., icons, categories, sort keys) were considered and rejected to keep the initial scope minimal and reviewable. They can be proposed in a follow-on SEP if real demand emerges.

title required, description optional

Requiring title ensures that when a server opts into the rich format, it always provides a display label. Making description optional avoids overhead for cases where a label alone is sufficient context.

additionalProperties: false on CompletionValue

This prevents future accidental extension outside the SEP process and keeps clients free to make assumptions about the structure.

Backward Compatibility

This change is fully backward compatible:

  • Existing servers returning string[] require zero changes.
  • Existing clients that expect string[] will encounter object items only if a new-style server is deployed. Such clients SHOULD handle unknown item shapes gracefully (e.g., by extracting a value property if present or skipping unrecognised items).
  • No protocol version bump is required; this is an additive, optional enrichment.

Recommended migration path:

  1. Clients update to handle string | CompletionValue (additive, no breaking change).
  2. Servers optionally migrate completions to rich objects where they add value.

Security Implications

No new attack surface is introduced. The title and description fields are display-only strings subject to the same input validation requirements as existing completion values. Servers MUST:

  • Validate all completion metadata before returning it.
  • Ensure title and description fields do not leak sensitive information that the plain value would not have exposed.
  • Apply the same access control and rate limiting to CompletionValue responses as to plain string responses.

Reference Implementation

The original implementation PR is #589 by @kentcdodds, which introduced draft schema and documentation changes for enhanced completion values. This SEP formalizes and corrects that work.

Note: The examples in PR #589 contain a bug where value fields are shown as integers (e.g., "value": 45). The correct type is string; this SEP supersedes those examples and the PR should be updated accordingly.

SDK prototype implementations are in progress. The reference implementation requirement will be satisfied before this SEP moves to final.

Open Questions

  1. Client-side filtering: Should the spec give stronger guidance (MUST vs SHOULD) on clients filtering against both value and title? Current wording is SHOULD to allow clients that delegate all filtering to the server.

  2. additionalProperties: false: Some schemas in the spec allow extension via additional properties. We have chosen to restrict CompletionValue — is this the right call, or should it be open for future extension?

  3. Ordering: Should the spec say anything about how title affects sort order of suggestions? Current proposal is silent on this.

SEP-2322 Multi Round-Trip Requests

Final · Standards Track · Created 2026-02-03

Abstract

This proposal specifies a simple way to handle server-initiated requests in the context of a client-initiated request (e.g., an elicitation request in the context of a tool call) without requiring a shared storage layer shared across server instances or statefulness in load balancing, which will significantly reduce the cost of operating MCP servers at scale in the common case. It also reduces the HTTP transport's dependence on SSE streams, which cause problems in a lot of environments that cannot support long-lived connections.

This proposed way of handling server-initiated requests will replace the current approach of sending server-initiated requests. This is a breaking change.

This SEP also specifies the subset of client requests that a server can send a server-initiated request on. This is a reduced scope compared to the current spec and is also a breaking change.

Making a breaking change here is necessary since adoption of server-initiated request features like Elicitation, Sampling and ListRoots is very low or blocked for many Remote MCP servers or Server Hosted Clients due to the operational complextity of supporting the SSE streams and server-side state.

Motivation

Note: This SEP is intended to provide a generic mechanism for handling any server-initiated request in the context of any client-initiated request. For clarity, throughout this document, we will specifically discuss tool calls as a proxy for any client-initiated request, but it should be read as applying equally to (e.g.) resource or prompt requests; similarly, we will discuss elicitation requests as a proxy for any server-initiated request, but it should be read as applying equally to (e.g.) sampling requests.

We start with the observation that there are two types of MCP tools:

  1. Ephemeral: No state is accumulated on the server side.
    • If server needs more info to process the tool call, it can start from scratch when it gets that additional info.
    • Examples: weather app, accessing email
  2. Persistent: State is accumulated on the server side.
    • Server may generate a large amount of state before requesting more info from the client, and it may need to pick up that state to continue processing after it receives the info from the client.
    • Server may need to continue processing in the background while waiting for more info from the client, in which case server-side state is needed to track that ongoing processing.
    • Examples: accessing an agent, spinning up a VM and needing user interaction to manipulate the VM

The vast majority of MCP tools will be ephemeral, and it is extremely common for tools to be deployed in a horizontally scaled, load balanced service, so we need to optimize for this case.

Today, if a tool needs to send an elicitation request in order to make progress, the workflow works like this:

  1. Client sends tool call request. For this example, let's assume that the load balancers happen to send this request to server instance A.
  2. Server A opens an SSE stream and sends the elicitation request on that stream.
  3. Client sends the elicitation response as a separate request, for which the load balancers will choose a server instance completely independently of the one they chose in step 1. In this example, let's assume that the load balancers happen to send this request to server instance B.
  4. Server A must somehow discover the elicitation response delivered to server B.
  5. Server A then sends the tool call result on the SSE stream opened in step 2.
Server Instance BServer Instance ALoad BalancerClientServer Instance BServer Instance ALoad BalancerClient1. Initial RequestServer A holds memory,waiting for response...2. User RespondsA is waiting, but B has the data.Requires Shared Storage to bridge this gap.Tool Call (Start)Route to Instance AElicitation Request (SSE Stream Open)Elicitation ResultRoute to Instance B (Stateless LB)

The difficult part here is step 4, which requires some sort of statefulness on the server side. The main way to solve this problem today is to have a storage layer shared across all server instances, so that multiple server instances can match up the elicitation response on one server instance with the original ongoing tool call on a different server instance.

There are two main approaches that can be used to solve this problem today:

  • Persistent Storage Layer Shared Across Server Instances: Servers can deploy and manage a persistent storage layer (e.g., PostgreSQL, Redis, DynamoDB), which allow multiple server instances to match up the elicitation response on one server instance with the original ongoing tool call on a different server instance. This approach has a number of drawbacks:
    • The persistent storage layer is extremely expensive, especially for ephemeral tools that may not already have such a layer (e.g., a weather tool).
    • The persistent storage layer imposes significant reliability concerns: it becomes a critical dependency and therefore a potential single point of failure. To avoid that, it must provide high availability, replication, and backup mechanisms.
    • The persistent storage layer becomes a bottleneck, limiting horizontal scalability. Geographic distribution requires either expensive global replication or sticky routing.
    • The persistent storage layer also imposes significant operational complexity. In horizontally scaled deployments, it requires distributed locking or consensus protocols. It also requires special garbage collection logic to determine when shared can be cleaned up, which requires careful trade-offs: cleaning up state too aggressively can reduce storage costs but limit how long users have to respond, whereas cleaning up less aggressively accommodates slow users but increases storage costs.
    • This approach requires special behavior in the tool implementation to integrate with the persistent storage layer. The MCP SDKs today do not have any special hooks for this sort of storage layer integration, which means that it's very hard to write in-line code via the SDKs.
  • Statefulness in Load Balancing: With the use of cookies, it is possible for the load balancing layer to ensure that the elicitation request in step 3 is delivered to the same server instance that the original request was delivered to in step 1. This approach, while often cheaper than a persistent storage layer, has the following drawbacks:
    • It requires special configuration and behavior in the load balancers, which is often difficult to manage.
    • It breaks normal load balancing models, resulting in uneven load distribution, thus increasing the cost of running the service.
    • It requires special behavior in clients to propagate the cookies used for statefulness.
    • It requires the tool implementation to match up the elicitation request with the ongoing tool call. (The MCP SDKs have some code to handle this, but it's still a very strange pattern in the HTTP world.)
    • It is not fault tolerant. If the server instance goes down, all state is lost, and the tool call would need to start over from scratch. (This doesn't necessarily matter for ephemeral tools, but it is an issue for persistent tools.)

Also, both of these approaches rely on the use of an SSE stream, which causes problems in environments that cannot support long-lived connections. They also require an instance of the tool to stay in memory in a particular server instance indefinitely. This is particularly problematic for elicitation requests specifically, since the result may not come from the user for an unbounded amount of time (e.g., it could be days or months, or maybe even never).

The goal of this SEP is to propose a simpler way to handle the pattern of server-initiated requests within the context of a client-initiated request. Specifically, we need to make it cheaper to support this pattern in the common case of an ephemeral tool in a horizontally scaled, load balanced deployment. This means that we need a solution that does not depend on an SSE stream and does not require either a persistent storage layer or stateful load balancing, which in turn means that we need to avoid dependencies between requests: servers must be able to process each individual request using no information other than what is present in that individual request.

Note that while the goal here is to optimize the common case of ephemeral tools, we do want to continue to support persistent tools, which generally already require a persistent storage layer.

Specification

This SEP proposes a new mechanism for handling server requests in the context of a client request. This new mechanism will have a slightly different workflow for ephemeral tools and persistent tools, the latter of which will leverage Tasks. However, both workflows will use the same data structures.

Schema Changes

First, we introduce the notion of InputRequests, which represents a set of one or more server-initiated request to be sent to the client, and InputResponses, which represents the client's responses to those requests. Both requests and responses are stored in a map with string keys. For InputRequests, the map values are server-initiated requests (e.g., elicitation or sampling requests), whereas for InputResponses, the map values are the responses to those requests. Here's how that would look in the typescript MCP schema:

export type InputRequest =
  CreateMessageRequest | ElicitRequest | ListRootsRequest;

export interface InputRequests {
  [key: string]: InputRequest;
}

export type InputResponse =
  CreateMessageResult | ElicitResult | ListRootsResult;

export interface InputResponses {
  [key: string]: InputResponse;
}

The keys are assigned by the server when issuing the requests. The client will send the response for each request using the corresponding key. For example, a server might send the following input requests:

"inputRequests": {
  // Elicitation request.
  "github_login": {
    "method": "elicitation/create",
    "params": {
      "mode": "form",
      "message": "Please provide your GitHub username",
      "requestedSchema": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          }
        },
        "required": ["name"]
      }
    }
  },
  // Sampling request.
  "capital_of_france" : {
    "method": "sampling/createMessage",
    "params": {
      "messages": [
        {
          "role": "user",
          "content": {
            "type": "text",
            "text": "What is the capital of France?"
          }
        }
      ],
      "modelPreferences": {
        "hints": [
          {
            "name": "claude-3-sonnet"
          }
        ],
        "intelligencePriority": 0.8,
        "speedPriority": 0.5
      },
      "systemPrompt": "You are a helpful assistant.",
      "maxTokens": 100
    }
  }
}

The client would then send the responses in the following form:

"inputResponses": {
  // Elicitation response (ElicitResult).
  "github_login": {
    "action": "accept",
    "content": {
      "name": "octocat"
    }
  },
  // Sampling response (CreateMessageResult).
  "capital_of_france": {
    "role": "assistant",
    "content": {
      "type": "text",
      "text": "The capital of France is Paris."
    },
    "model": "claude-3-sonnet-20240307",
    "stopReason": "endTurn"
  }
}

The schema looks like this:

export interface InputRequiredResult extends Result {
  // Requests issued by the server that must be complete before the
  // client can retry the original request.
  inputRequests?: InputRequests;
  // Request state to be passed back to the server when the client
  // retries the original request.
  // Note: The client must treat this as an opaque blob; it must not
  // interpret it in any way.
  requestState?: string;
}

// RequestParams type that includes input responses and request state.
// These parameters may be included in any client-initiated request.
export interface InputResponseRequestParams extends RequestParams {
  // New field to carry the responses for the server's requests from the
  // InputRequiredResult message. For each key in the response's inputRequests
  // field, the same key must appear here with the associated response.
  inputResponses?: InputResponses;
  // Request state passed back to the server from the client.
  requestState?: string;
}

Since this change creates a polymorphic response for method calls like tools/call, we are introducing a new field to Result which indicate the ResultType. The client should parse this field to determine the type of the Result contained in the message. If this field is not provided, the Client should assume a ResultType of "complete" for backwards compatibility.

Extensions MAY add additional ResultType values. The set of supported ResultType values MUST be created from the set defined in the core protocol and include any additional values of supported extensions that are advertised via capabilities.

The Client SHOULD treat unrecognized values as invalid protocol responses.

The schema change will look like this:

/**
 * Common result fields.
 *
 * @category Common Types
 */
export interface Result {
  _meta?: MetaObject;
  // New field to indicate the type of the result, which allows the client to determine how to parse the result object. If no resultType is specified "complete" should be assumed.
  resultType: ResultType;
  [key: string]: unknown;
}

export type ResultType =
  | "complete" // the request completed successfully and the result contains the final content.
  | "input_required" // the request is incomplete and the result contains an {@link InputRequiredResult} object
  | string; // open to extensions

We anticipate this field will be useful for future extensibility, as it allows us to introduce new types of results and can also apply to tasks as well.

These types will be used in two different workflows, one for ephemeral tools and another for persistent tools.

Server-Initiated Request Support for Client Requests

Many ClientRequest don't have clear use cases where a Server would need to request more information from the Client. This SEP builds upon SEP-2260 and further restricts when a Server can send a Server-Initiated Request to the Client.

Servers MAY send InputRequiredResult responses on the following Client Requests:

ClientRequest ServerResult InputRequiredResult Supported
GetPromptRequest GetPromptResult Yes
ReadResourceRequest ReadResourceResult Yes
CallToolRequest CallToolResult Yes
GetTaskPayloadRequest GetTaskPayloadResult Yes

Servers MUST NOT send InputRequiredResult responses on any other Client Requests. The below table represents what ClientRequests this excludes at the writing of this SEP.

ClientRequest InputRequiredResult Supported
PingRequest No
InitializeRequest No
CompleteRequest No
SetLevelRequest No
ListPromptsRequest No
ListResourcesRequest No
ListResourceTemplatesRequest No
SubscribeRequest No
UnsubscribeRequest No
ListToolsRequest No
GetTaskRequest No
ListTasksRequest No
CancelTaskRequest No
TaskInputResponseRequest No

Ephemeral Tool Workflow

For the ephemeral use case, in addition to input requests, we introduce the concept of request state. In cases where the server needs more information, the request state is sent to the client which echoes back the state to the server, allowing the server to remain stateless.

We will adopt the following workflow for ephemeral tools:

  1. Client sends tool call request.
  2. Server sends back a single response indicating that the request is incomplete. The response may include input requests that the client must complete. It may also include some request state that the client must return back to the server. This response terminates the original request. It will normally be sent as a single response, not on an SSE stream, although for now (this may change in a future SEP) it is also legal to send this response on an SSE stream following (e.g.) progress notifications. If this incomplete response is sent on an SSE stream, it must be the last message on the SSE stream, just as if it were a normal response.
  3. Client sends a new tool call request, completely independent of the original one. This new tool call includes responses to the input requests from step 2. It also includes the request state specified by the server in step 2.
  4. Server sends back a CallToolResponse.
Server (Stateless)ClientUserServer (Stateless)ClientUserServer needs more infoGenerates requestStateInitial Request TerminatedServer reconstitutes stateCompletes executiontools/call (id: 1)InputRequiredResult (id: 1, inputRequests, requestState)Prompts user for inputProvides responsestools/call (id: 2, inputResponses, requestState)Result (id: 2, final content)

Note that the requests in steps 1 and 3 are completely independent: the server that processes the request in step 3 does not need any information that is not directly present in the request. To support this decoupling the JsonRPC Id MUST be different between the requests sent in step 1 and step 3.

Note that both the "inputRequests" and "requestState" fields affect only the client's next retry of the original request. They will not be used for any other request that the client may be sending in parallel (e.g., a tool list or even another tool call).

Click to expand Example Flow for Ephemeral Tools Example Flow for Ephemeral Tools

Note: This is a contrived example, just to illustrate the flow.

  1. The client sends the initial call tool request:
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "location": "New York"
    }
  }
}
  1. The server responds with an incomplete response, indicating that the client needs to respond to an elicitation request in order for the tool call to complete, and including request state to be passed back:
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "github_login": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "Please provide your GitHub username",
          "requestedSchema": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              }
            },
            "required": ["name"]
          }
        }
      }
    },
    "requestState": "foo"
  }
}
  1. The client then retries the original tool call, this time including the responses to the input server request and the request state:
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "location": "New York"
    },
    "inputResponses": {
      "github_login": {
        "action": "accept",
        "content": {
          "name": "octocat"
        }
      }
    },
    "requestState": "foo"
  }
}
  1. Finally, the server completes the tool call:
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "resultType": "complete",
    "content": [
      {
        "type": "text",
        "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"
      }
    ],
    "isError": false
  }
}
Real-World Example for Ephemeral Workflow

This example demonstrates how requestState enables a multi-round-trip elicitation flow driven by Azure DevOps custom rules. The scenario involves an update_work_item tool that transitions a Bug work item to "Resolved." ADO custom rules require specific fields when certain state transitions occur, and the server uses iterative elicitation to gather them — accumulating context in requestState across rounds so that the final update can be executed without any server-side storage.

Click to expand ADO Custom Rules Example

Background — ADO Custom Rules in effect:

  • Rule 1: When State changes to "Resolved" → require the "Resolution" field (e.g., Fixed, Won't Fix, Duplicate, By Design).
  • Rule 2: When Resolution is "Duplicate" → require the "Duplicate Of" field (a link to the original work item).
Round 1 — Tool call triggers state change, server elicits Resolution
  1. The client invokes the update_work_item tool to resolve Bug #4522:
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "update_work_item",
    "arguments": {
      "workItemId": 4522,
      "fields": { "System.State": "Resolved" }
    }
  }
}
  1. The server recognizes that setting State to "Resolved" triggers Rule 1, which requires a Resolution value. Rather than failing the call, the server returns an incomplete response with an elicitation request. No requestState is needed yet, since the original tool call arguments will be re-sent on retry:
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "resolution": {
        "method": "elicitation/create",
        "params": {
          "message": "Resolving Bug #4522 requires a resolution. How was this bug resolved?",
          "requestedSchema": {
            "type": "object",
            "properties": {
              "resolution": {
                "type": "string",
                "enum": ["Fixed", "Won't Fix", "Duplicate", "By Design"],
                "description": "Resolution type for this bug"
              }
            },
            "required": ["resolution"]
          }
        }
      }
    }
  }
}
  1. The user selects "Duplicate". The client retries the original tool call with the elicitation response:
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "update_work_item",
    "arguments": {
      "workItemId": 4522,
      "fields": { "System.State": "Resolved" }
    },
    "inputResponses": {
      "resolution": {
        "action": "accept",
        "content": { "resolution": "Duplicate" }
      }
    }
  }
}
Round 2 — Resolution triggers another rule, server elicits Duplicate Of
  1. The server merges the user's response and sees that Resolution = "Duplicate" triggers Rule 2, requiring a "Duplicate Of" link. It returns another incomplete response, this time encoding the already-gathered resolution in requestState so it is available regardless of which server instance handles the next retry:
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "duplicate_of": {
        "method": "elicitation/create",
        "params": {
          "message": "Since this is a duplicate, which work item is the original?",
          "requestedSchema": {
            "type": "object",
            "properties": {
              "duplicateOfId": {
                "type": "number",
                "description": "Work item ID of the original bug"
              }
            },
            "required": ["duplicateOfId"]
          }
        }
      }
    },
    "requestState": "eyJyZXNvbHV0aW9uIjoiRHVwbGljYXRlIn0..."
  }
}
  1. The user provides the original work item ID. The client retries the tool call, echoing back the requestState and including the new elicitation response:
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "update_work_item",
    "arguments": {
      "workItemId": 4522,
      "fields": { "System.State": "Resolved" }
    },
    "inputResponses": {
      "duplicate_of": {
        "action": "accept",
        "content": { "duplicateOfId": 4301 }
      }
    },
    "requestState": "eyJyZXNvbHV0aW9uIjoiRHVwbGljYXRlIn0..."
  }
}
Final — Server completes the update
  1. The server decodes the requestState (which contains the resolution), reads the inputResponses (which contains the duplicate ID), and now has all required fields. It completes the tool call:
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "resultType": "complete",
    "content": [
      {
        "type": "text",
        "text": "Bug #4522 resolved as Duplicate of Bug #4301. State set to Resolved and duplicate link created."
      }
    ],
    "isError": false
  }
}

Key takeaway: Across both elicitation rounds, the server held no in-memory or persisted state. The requestState field carried the accumulated context through the client, and any server instance could have handled any individual round.

Use Cases for Request State

The "requestState" mechanism provides a mechanism for doing multiple round trips on the same logical request. There are two main use-cases for this.

Use Case 1: Rolling Upgrades

Let's say that you are doing a rolling upgrade of your horizontally scaled server instances to deploy a new version of a tool implementation. The old version had two input requests with keys "github_login" and "google_login". However, in the new version of the tool implementation, it still uses the "github_login" input request, but it replaces the "google_login" input request with a new "microsoft_login" input request.

If the first request goes to an old version of the server but the second attempt (that includes the input responses) goes to a new version of the server, then the server will see the result for "github_login", which it needs, but it won't see the result for "microsoft_login". (It will also see the result for "google_login", but it no longer needs that, so it doesn't matter.) At this point, the server needs to send a new input request for "microsoft_login", but it also doesn't want to lose the answer that it's already gotten for "github_login", so it would use the kind of state proposed in 1685 to retain that information without having to store the state on the server side.

The workflow here would look like this:

  1. Client sends tool call request that hits a server instance running the old version.
  2. Server sends back an incomplete response indicating the input requests for "github_login" and "google_login".
  3. Client sends a new tool call request that includes the responses to the input requests for "github_login" and "google_login". This time it hits a server instance running the new version.
  4. Server sends back another incomplete response indicating the input request for "microsoft_login", which the client has not already provided. However, the response also includes request state containing the already-provided "github_login" response, so that the client does not need to prompt the user for the same information a second time.
  5. Client sends a third tool call request that includes the response to the "microsoft_login" input request as well as echoing back the request state provided by the server in step 4.
  6. Server now sees the "github_login" info in the request state and the "microsoft_login" state in the input responses, so the request now contains everything the server needs to perform the tool call and send back a complete response.
Use Case 2: Load Shedding

Let's say that you have an MCP server instance that is processing a bunch of tool calls and notices that it's too heavily loaded, so it wants to move one of the ongoing tool calls to a different server instance. However, it has already done a significant amount of processing on that tool call, so it does not want to simply fail the call and have the client start over from scratch on another server instance; instead, it wants to preserve the state it has already accumulated, so that whichever server instance resumes processing can pick up from where the original server instance left off. This can be accomplished by sending an incomplete request that contains request state but does not contain any input requests.

The workflow here would look like this:

  1. Client sends the original request, which the load balancers route to server instance A.
  2. Server instance A does a bunch of computation before deciding that it needs to shed load. It sends an incomplete response with its accumulated state in the requestState field but without the inputRequests field.
  3. Client retries the request with the requestState field attached. The load balancers route this request to server instance B.
  4. Server instance B starts from the state it sees in the requestState field, thus picking up the computation from where server instance A left off, and eventually returning a complete response.
Protocol Requirements for Ephemeral Workflow
  1. Server Behavior:

    • Servers MAY respond to any client-initiated request with a InputRequiredResult. This message MAY be sent either as a standalone response or as the final message on an SSE stream, although implementations are encouraged to prefer the former. If using an SSE stream, servers MUST NOT send any message on the stream after the incomplete response message.
    • The InputRequiredResult MAY include an inputRequests field.
    • The InputRequiredResult MAY include a requestState field. If specified, this field is an opaque string that is meaningful only to the server. Servers are free to encode the state in any format (e.g., plain JSON, base64-encoded JSON, encrypted JWT, serialized binary, etc.).
    • If a request contains a requestState field, servers MUST always validate that state, as the client is an untrusted intermediary. If tampering is a concern, servers SHOULD encrypt the requestState field using an encryption algorithm of their choice (e.g., they can use AES-GCM or a signed JWT) to ensure both confidentiality and integrity. Note that there is also a risk of replaying/hijacking attacks, where an authenticated attacker resends state that was originally sent to a different user. Therefore, if the request state contains any data that is specific to the original user, the server MUST use some mechanism to cryptographically bind the data to the original user and MUST verify that the requestState data sent by the client is associated with the currently authenticated user. Servers using plaintext state MUST treat the decoded values as untrusted input and validate them the same way they would validate any client-supplied data.
  2. Client Behavior:

    • If a client receives an InputRequiredResult message, if the message contains the inputRequests field, then the client MUST construct the requested input before retrying the original request. In contrast, if the message does not contain the inputRequests field, then the client MAY retry the original request immediately.
    • If a client receives a InputRequiredResult message that contains the requestState field, it MUST echo back the exact value of that field when retrying the original request. Clients MUST NOT inspect, parse, modify, or make any assumptions about the requestState contents. If the InputRequiredResult does not contain a requestState field, the client MUST NOT include one in the retry.

Persistent Tool Workflow

The persistent tool workflow will leverage Tasks. Tasks already provide a mechanism to indicate that more information is needed to complete the request. The input_required Task Status allows the server to indicate that additional information is needed to complete processing the task.

The workflow for Tasks is as follows:

  1. Server sets Task Status to input_required. The server can pause processing the request at this point.
  2. Client retrieves the Task Status by calling tasks/get and sees that more information is needed.
  3. Client calls tasks/result
  4. Server returns the InputRequests object.
  5. Client calls tasks/input_response request that includes an InputResponses object along with Task metadata field.
  6. Server resumes processing sets TaskStatus back to working.
Server (Stateful)ClientUserServer (Stateful)ClientUserClient polls for statusProcessing resumes, Client pollstools/call (id: 1, task config)Result (id: 1, taskId, status: working)tasks/get (id: 2, taskId)Result (id: 2, status: input_required)tasks/result (id: 3, taskId)Result (id: 3, inputRequests)Prompts user for inputProvides responsestasks/input_response (id: 4, taskId, inputResponses)Result (id: 4, acknowledged)tasks/get (id: 5, taskId)Result (id: 5, status: completed)tasks/result (id: 6, taskId)Result (id: 6, final content)

Since Tasks are likely longer running, have state associated with them, and are likely more costly to compute, the request for more information does not end the originally requested operation (e.g., the tool call). Instead, the server can resume processing once the necessary information is provided.

To align with MRTR semantics, the server will respond to the tasks/result request with a InputRequests object. Both of these will have the same JsonRPC id. When the client responds with a InputResponses object this is a new client request with a new JSONRPC id and therefore needs a new method name. We propose tasks/input_response.

The above workflow and below example do not leverage any of the optional Task Status Notifications although this SEP does not preclude their use.

Click to expand Example Flow for Persistent Tools

The below example walks through the entire Task Message flow for a Echo Tool which can request additional information from the client via Elicitation.

  1. Client Request to invoke EchoTool.
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "echo",
    "task": {
      "ttl": 60000
    }
  }
}
  1. Server Response with a Task
{
  "id": 1,
  "jsonrpc": "2.0",
  "result": {
    "task": {
      "taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5",
      "status": "working",
      "statusMessage": "Task has been created for echo tool invocation.",
      "createdAt": "2026-01-27T03:32:48.3148180Z",
      "lastUpdatedAt": "2026-01-27T03:32:48.3148180Z",
      "ttl": 60000,
      "pollInterval": 100
    }
  }
}
  1. Client Request periodically checks the status of the Task using tasks/get.
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tasks/get",
  "params": {
    "taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
  }
}
  1. Server Response with Task status input_required
{
  "id": 2,
  "jsonrpc": "2.0",
  "result": {
    "taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5",
    "status": "input_required",
    "statusMessage": "Input Required to Proceed call tasks/result",
    "createdAt": "2026-01-27T03:38:07.7534643Z",
    "lastUpdatedAt": "2026-01-27T03:38:07.7534643Z",
    "ttl": 60000,
    "pollInterval": 100
  }
}
  1. Client Request sends message tasks/result to discover what input is required to proceed.
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tasks/result",
  "params": {
    "taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
  }
}
  1. Server Response returns inputRequests to request additional input
{
  "id": 3,
  "jsonrpc": "2.0",
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "echo_input": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "Please provide the input string to echo back",
          "requestedSchema": {
            "type": "object",
            "properties": {
              "input": { "type": "string" }
            },
            "required": ["input"]
          }
        }
      }
    }
  },
  "_meta": {
    "io.modelcontextprotocol/related-task": {
      "taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
    }
  }
}
  1. Client Request presents the Elicitation to the user and collects the input, then sends message to the server.
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tasks/input_response",
  "params": {
    "inputResponses": {
      "echo_input": {
        "action": "accept",
        "content": {
          "input": "Hello World!"
        }
      }
    },
    "_meta": {
      "io.modelcontextprotocol/related-task": {
        "taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
      }
    }
  }
}
  1. Server Response Server should acknowledge the receipt of the 'tasks/input_response' message by sending a 'JSONRPCResponse'. If the message was successfully received a JSONRPCResultResponse is sent including the taskId. If an error occurs, a JSONRPCErrorResponse is sent. The server can now proceed to complete the Task using the provided input, and the Task status changes to Working.
{
  "id": 4,
  "jsonrpc": "2.0",
  "result": {
    "_meta": {
      "io.modelcontextprotocol/related-task": {
        "taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
      }
    }
  }
}
  1. Client Request continues to poll the input status using tasks/get until server responds with Task Status of Completed
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tasks/get",
  "params": {
    "taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
  }
}
  1. Server Response with Task status completed
{
  "id": 5,
  "jsonrpc": "2.0",
  "result": {
    "taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5",
    "status": "completed",
    "statusMessage": "Task has been completed successfully, call tasks/result",
    "createdAt": "2026-01-27T03:38:07.7534643Z",
    "lastUpdatedAt": "2026-01-27T03:38:08.1234567Z",
    "ttl": 60000,
    "pollInterval": 100
  }
}
  1. Client Request calls tasks/result to get the final result of the Task from the server.
{
  "id": 6,
  "jsonrpc": "2.0",
  "method": "tasks/result",
  "params": {
    "taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
  }
}
  1. Server Response with the final result of the Task
{
  "id": 6,
  "jsonrpc": "2.0",
  "result": {
    "resultType": "complete",
    "isError": false,
    "content": [
      {
        "type": "text",
        "text": "Echo: Hello World!"
      }
    ],
    "_meta": {
      "io.modelcontextprotocol/related-task": {
        "taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
      }
    }
  }
}
Protocol Requirements for Persistent Workflow
  1. Server Behavior:

    • Servers MAY respond to tasks/get by indicating that the task is in state input_required.
    • Servers MUST include an inputRequests field in the tasks/result response when the task is in state input_required.
  2. Client Behavior:

    • When tasks/get shows state input_required, clients MUST call tasks/result to get the input requests. Clients SHOULD construct the results of those requests, and then call tasks/input_response with the input responses to provide the required input for the task.
    • Clients MAY choose not to fulfill the input requests, in which case they can cancel the task.

Interactions Between Ephemeral and Persistent Workflows

If a tool implementation needs the client to respond to a set of input requests before it can even start processing but then later needs to do persistent processing, it can start using the ephemeral workflow and then switch to the persistent workflow by creating a task at that point. This avoids the need for the server to store state until it actually has the information needed to start processing the request. This workflow would look like this:

  1. Client sends tool call request with task metadata.
  2. Server sends back inputRequests response indicating that more information is needed to process the request. This terminates the original request.
  3. Client sends a new tool call request, completely independent of the original one, which includes the inputResponses object along with the task metadata.
  4. Server sends back a task ID, indicating that it will be processing the request in the background. All subsequent interaction will be done via the Tasks API.

Note that the opposite is not true: Once a tool implementation returns a task, it has committed to storing state on the server side for the duration of the task, and there is no way to transition back to the ephemeral model. All subsequent interactions must be performed via the Tasks API.

Guidance for Error Handling

This section provides implementation guidance for error handling in scenarios where the client provides unexpected or malformed data in the inputResponses object.

As with any received request, the server SHOULD validate the data provided by the client is a valid inputResponses object and that the information inside can be correctly parsed. Protocol errors, like malformed JSON, invalid schema, or internal server errors which prevent the processing of the request should return a JSONRPCErrorResponse with an appropriate error code and message.

If additional parameters are provided in the inputResponses object The server SHOULD treat these as optional parameters. Therefore it SHOULD ignore any unexpected information in the inputResponses object that it does not recognize or need.

The client may also fail to send all the information requested in previous inputRequests. If the missing information requested is necessary for the server to process the request, then it SHOULD respond with a new InputRequiredResult.

We discussed having a specific application level error code returned, however the client may not have enough information to recover in all scenarios. Therefore, we decided to rely on the existing mechanics of requesting more input via InputRequiredResult to ensure a client can always recover by having the server request the necessary information again.

Malicious clients could intentionally send incorrect information in the inputResponses object, and generate load on the server by causing it to repeatedly request the same information. However, this is not a new concern introduced by this workflow, since malicious clients could already generate load by sending malformed requests. Server implementors can use standard techniques like rate limiting and throttling to protect themselves from such attacks.

In the ephemeral workflow, this would look like the following:

  1. The client retries the original tool call, this time including the inputResponses object, but the response is missing required information that the server needs to process the request.
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "location": "New York"
    },
    "inputResponses": {
      "not_requested_info": {
        "action": "accept",
        "content": {
          "not_requested_param_name": "Information the server did not request"
        }
      }
    }
  }
}
  1. The server responds with an incomplete response, indicating that the client needs to respond to an elicitation request in order for the tool call to complete, and including request state to be passed back:
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "github_login": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "Please provide your GitHub username",
          "requestedSchema": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              }
            },
            "required": ["name"]
          }
        }
      }
    }
  }
}
  1. The Server responds with an incomplete response, indicating that the client needs to provide missing information for the request to succeed.

In the persistent workflow, this would look like the following: Step 7 from above: Client Request The client mistakenly or maliciously sends unexpected, but well-formed data to the server in response to the input request.

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tasks/input_response",
  "params": {
    "inputResponses": {
      "echo_input": {
        "action": "accept",
        "content": {
          "not_requested_parameter": "Information the server did not request."
        }
      }
    },
    "_meta": {
      "io.modelcontextprotocol/related-task": {
        "taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
      }
    }
  }
}

Step 8 from above. Server Response Server acknowledges the receipt of the response by sending a JSONRPCResultResponse. However, since the response is missing required information, the server does not proceed with processing the task and leaves the Task status as input_required. The next time the client calls tasks/result, the server responds with a new inputRequest requesting the necessary information again.

{
  "id": 4,
  "jsonrpc": "2.0",
  "result": {
    "_meta": {
      "io.modelcontextprotocol/related-task": {
        "taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
      }
    }
  }
}

Rationale

We considered a bidirectional stream approach to replace SSE streams. However, that approach would have made the wire protocol more complicated (e.g., it would have required HTTP/2 or HTTP/3). Also, it would not have eliminated problems for environments that cannot support long-lived connections, nor would it have addressed fault tolerance issues.

There was discussion about whether the input requests should be a map or just a single object, possibly leveraging some field inside of the requests (e.g., the elicitation ID) to differentiate between them. We decided that the map makes sense, since it structurally guarantees the uniqueness of keys, which will avoid the need for explicit checks in SDKs and applications to avoid conflicts.

In the persistent workflow, we considered including the input requests directly in the tasks/get response, rather than requiring the client to see the input_required status and then call tasks/result to get the input requests. We decided to keep those two things separate in deference to implementations that use separate infrastructure for task state and for the actual tool implementation; the idea is that the tasks/get call should have a consistent latency profile, regardless of what the task state actually is. We recognize that this requires an extra round-trip to the server, but we can optimize this in the future if becomes a problem.

Backward Compatibility

Today many sdks support elicitation via an in-line but async fashion which waits for the elicitation response before sending the tool call response on the original SSE stream, this works for MCP Servers that are a single-process or can ensure sticky routing of requests.

def my_tool():
  do_work()
  await elicit_more_info()
  do_more_work()
  return tool_result

SDKs MAY continue to support this style of elicitation for existing tools and for backwards compatibility, however, they SHOULD mark this pattern as legacy/deprecated.

Moving forward examples and SDKs need to support the new style of elicitation where the code can not assume the same process is handling both tool calls. This programming model is less appealing, however it ensures that MCP Servers can go from a single process Stdio MCP server to a multi-process remote MCP Server without major rewrites, and ensures we have a single recommended way to do elicitation moving forward.

def my_tool(request):
  if(request.requestState):
      state = decode(request.requestState)
  if(request.inputResponses):
      additionalInfo = decode(request.inputResponses)

  do_work(state, additionalInfo)
  if(more_info_needed):
    return IncompleteResponse();
  else
    do_more_work()
    return tool_result

Other options considered here were to have two separate programming models that developers could choose between based on their MCP Server deployment single-process or multi-process to continue to support the await semantics, however this would have added complexity to the developer experience, and would have made it more difficult for developers to switch between single-process and multi-process deployments.

Security Implications

Because requestState passes through the client, malicious or compromised clients could attempt to modify it to alter server behavior, bypass authorization checks, or corrupt server logic. To mitigate this, we require servers to validate this state as described in the protocol requirements above.

Reference Implementation

TBD

Acknowledgments

Thanks to Luca Chang (@LucaButBoring) for his valuable input on how to integrate input requests into Tasks.

SEP-2419 Standardize `cache_hint` as a Well-Known Key in `CallToolResult._meta`

Accepted · Standards Track · Created 2026-03-18

Imported from upstream. Proposed as modelcontextprotocol#2419 by @clouatre on 2026-03-18, 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 — accepted. The document carries every required section, and a prototype a reviewer can run is cited at https://github.com/clouatre-labs/aptu-coder. Conformance tests and the reference documentation chapter are still required before Final.

Abstract

This SEP standardizes cache_hint as a well-known key in CallToolResult._meta, with values "no-cache" and "cache". The key allows a server to advise MCP clients whether to cache or skip caching a particular tool result. This is the first well-known key defined for CallToolResult._meta on the server-to-client side. It follows the hint-style, non-mandatory pattern established by progressToken (on request _meta) and the well-known _meta key convention established by SEP-414. No schema interface changes are required; the key is declared in specification prose and is backward-compatible.

Motivation

MCP clients may apply caching to tool results -- for example, LLM-backed clients that support prompt caching may insert tool results into their cache to reduce token costs on repeated calls. For stateless, read-only, deterministic tools, this is beneficial in long-running sessions where the same result may be requested multiple times.

However, for single-pass sessions (subagents, benchmarks, one-shot pipelines), the cache entry is written once and never read again: a net token cost with zero benefit. Today, the server has no mechanism to advise the client against caching its results. The only available workaround (DISABLE_PROMPT_CACHING=1 in Claude Code, for example) is a global opt-out that disables caching for the entire session, not a per-result signal.

This gap is documented in an open upstream request (anthropics/claude-code#34334) asking that Claude Code honor a per-result _meta.cache_hint: "no-cache" signal.

The MCP spec states:

"The _meta property/parameter is reserved by MCP to allow clients and servers to attach additional metadata to their interactions. Additionally, definitions in the schema may reserve particular names for purpose-specific metadata, as declared in those definitions."

cache_hint is exactly this: a purpose-specific name declared for CallToolResult._meta.

The need generalizes beyond a single client. Any MCP client that applies caching at the tool result level faces the same mismatch. Standardizing the key in the MCP specification enables interoperability across clients without requiring bilateral coordination between server and client authors.

Specification

Well-Known Key

The key cache_hint is a well-known key in CallToolResult._meta. Its value is a string. The following values are defined:

Value Meaning
"no-cache" The server advises the client not to cache this result.
"cache" The server advises the client that this result is safe to cache.

If cache_hint is absent, no caching preference is expressed. Clients MAY apply their default caching policy.

Semantics

cache_hint is a hint. The receiver is not obligated to honor it. This matches the precedent established by progressToken -- a hint-style key on request _meta where the spec states "The receiver is not obligated to provide these notifications." cache_hint applies the same non-mandatory semantics on the result side.

A server SHOULD set cache_hint: "no-cache" on results that are expensive to cache without benefit -- for example, large read-only results returned in single-pass contexts where the cache entry will never be read again.

A server MAY set cache_hint: "cache" to affirmatively signal that a result is safe to cache, useful when the client's default policy would otherwise skip caching.

Schema

No TypeScript schema interface change is required. The key is declared in specification prose under the CallToolResult definition. For reference, the existing definition is:

interface CallToolResult {
  _meta?: { [key: string]: unknown };
  content: ContentBlock[];
  isError?: boolean;
  structuredContent?: { [key: string]: unknown };
}

The cache_hint key is a well-known name within the open _meta map. Implementations that do not recognize cache_hint ignore it, as the index signature requires.

Non-Normative Example

A server returning a large file tree result in a single-pass context:

{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "content": [{ "type": "text", "text": "..." }],
    "isError": false,
    "_meta": {
      "cache_hint": "no-cache"
    }
  }
}

A client receiving this result SHOULD skip inserting it into its cache, if it has the capability to do so at the per-result level.

Rationale

Why cache_hint and not io.modelcontextprotocol/cache_hint?

The DNS-prefix convention for _meta keys (proposed in #1788) applies to keys reserved by the MCP project. SEP-414 establishes a precedent for plain names as an exception when compatibility or simplicity requires it (traceparent, tracestate, baggage). progressToken is the only existing plain-name key in the spec and is already in _meta.

cache_hint is proposed as a plain name for two reasons:

  1. The #1788 draft proposes a MetaObject that would place bare keys in a free-use zone, but #1788 is not yet adopted. Adopting the prefix now would lock in a naming decision that #1788 may revise once ratified.
  2. A plain name is simpler to set, read, and document. The HTTP ecosystem uses Cache-Control (plain) rather than a namespaced equivalent.

If #1788 is eventually adopted and the community prefers a prefixed name, the transition (cache_hint -> io.modelcontextprotocol/cache_hint) is non-breaking: both names can coexist during a deprecation window, as the schema allows arbitrary keys.

Why values "no-cache" and "cache" and not a boolean?

HTTP Cache-Control semantics are widely understood by protocol designers. A string enum is extensible -- future values (e.g., "immutable") can be added without a breaking change. A boolean (cache: true/false) cannot be extended.

Why on CallToolResult and not on the tool definition?

Tool-definition annotations (read_only_hint, destructive_hint, etc.) are static -- they describe the tool's general behavior, not the characteristics of a specific result. A tool may return cacheable results in some invocations and non-cacheable results in others (e.g., depending on the size of the output or the session context). Placing cache_hint on the result enables per-result control.

Alternatives Considered

A new cache_control field on CallToolResult: Adds schema surface and a required schema version bump. The _meta mechanism is already present and designed for this use case.

Client-side heuristics only: Clients could infer cacheability from result size or tool annotations. This is less precise and requires bilateral convention rather than an explicit signal.

A new capability or negotiation step: Unnecessarily complex for a hint that has no effect if ignored.

Backward Compatibility

This SEP introduces no breaking changes.

Existing clients that do not recognize cache_hint continue to operate correctly. The open index signature on _meta requires implementations to tolerate unknown keys.

Existing servers that do not set cache_hint are unaffected. Clients apply their existing default caching policy when the key is absent.

Security Implications

cache_hint is a server-to-client hint with no enforcement requirement. A malicious server setting cache_hint: "cache" on a mutable or sensitive result cannot force a client to cache it; a malicious server setting cache_hint: "no-cache" cannot prevent a client from caching.

Clients that choose to honor cache_hint should apply the same validation they apply to any _meta value: verify the key is a known string, ignore unrecognized values, and do not propagate the key to the model or to downstream systems.

No authentication, authorization, or data-validation changes are required.

Reference Implementation

https://github.com/clouatre-labs/aptu-coder (Apache-2.0)

All four tool success paths (analyze_directory, analyze_file, analyze_symbol, analyze_module) set _meta: { "cache_hint": "no-cache" } on every CallToolResult via a shared helper:

fn no_cache_meta() -> Meta {
    let mut m = serde_json::Map::new();
    m.insert(
        "cache_hint".to_string(),
        serde_json::Value::String("no-cache".to_string()),
    );
    Meta(m)
}

An integration test (crates/aptu-coder/tests/integration_tests.rs, test_call_tool_result_cache_hint_metadata) validates round-trip serialization: _meta.cache_hint == "no-cache" in the JSON output.

The implementation uses rmcp 1.5.0. The CallToolResult::with_meta(Some(Meta(map))) API serializes arbitrary key-value pairs into _meta without schema changes.

This satisfies the SEP "prototype implementation" requirement for Accepted status. Full SDK implementations (TypeScript, Python, Java, C#, Go, Kotlin) are required for Final status and will be tracked in the PR thread.

SEP-2468 Recommend Issuer (iss) Parameter in MCP Auth Responses

Final · Standards Track · Created 2026-03-25

Abstract

This SEP proposes recommending the inclusion and requiring the validation of an explicit issuer (iss) parameter in Model Context Protocol (MCP) authorization responses to mitigate authorization mix‑up attacks. By binding authorization responses to a specific authorization server identity, MCP clients can reliably detect and reject responses originating from an unexpected issuer, improving protocol robustness in multi‑identity provider (IdP) environments. This SEP follows the specifications defined in RFC9207.

Motivation

The Model Context Protocol increasingly operates in environments where multiple authorization servers, identity providers, and intermediaries coexist. In such environments, OAuth mix‑up attacks become a realistic threat. Mix-up attacks are when an attacker causes a client to associate an authorization response with the wrong authorization server, potentially leading to token leakage or privilege escalation.

OAuth specifications describe two mitigations for mix‑up attacks: requiring issuer (iss) parameter or using a unique redirect_uri for each issuer a client interacts with. A unique redirect_uri per issuer is not possible with Client ID Metadata Documents (the recommended registration approach) and is operationally expensive with Dynamic Client Registration. As such, the recommendation is for MCP environments to leverage the issuer mitigation.

Requiring an explicit iss parameter in MCP authorization responses provides a simple, interoperable, and well‑understood mechanism to bind responses to the correct authorization server and prevent mix‑up attacks by construction. Since not every authorization server sends the issuer parameter though, this SEP proposes a MUST for clients to validate issuer if provided and a SHOULD for authorization servers supporting MCP scenarios. Future SEPs and releases may change the SHOULD to a MUST.

Specification

Issuer Parameter Requirement

MCP authorization servers SHOULD include an issuer (iss) parameter in authorization responses, including error responses, as defined in RFC9207. Authorization servers that do so MUST advertise it by setting authorization_response_iss_parameter_supported: true in their authorization server metadata.

The iss parameter MUST:

  • Exactly match the issuer identifier advertised via metadata discovery
  • Be a URL that uses the https scheme without query or fragment components (RFC 8414 Section 2)

Client Validation Requirements

MCP clients MUST validate the iss parameter in authorization responses by:

  • Determining the expected issuer for the authorization request
  • Comparing the received iss value against the expected issuer
  • Rejecting the authorization response if the values do not match exactly

If issuer validation fails, the client MUST treat the response as invalid and abort the authorization flow.

Rationale

The iss value is already used in OpenID Connect and JWT‑based token validation. Extending its use to MCP authorization responses:

  • Leverages existing ecosystem knowledge and tooling
  • Avoids introducing MCP‑specific security mechanisms
  • Provides a clear and auditable security for deployments

Alternatives considered

Introducing MCP‑specific issuer binding fields

  • Rejected in favor of reusing established OAuth/OIDC mechanisms.

Requiring unique redirect_uri per issuer

  • CIMD metadata documents are static and cannot enumerate every issuer; with DCR it is technically possible but DCR has operational drawbacks in MCP deployments that make it undesirable to depend on for a security property. RFC 9207 works uniformly across registration approaches.

Discarding iss when the server does not advertise support (strict RFC 9207 §2.4 SHOULD)

  • RFC 9207 §2.4 recommends that clients SHOULD discard responses carrying iss from servers that do not set authorization_response_iss_parameter_supported, but explicitly leaves the decision to local policy ("specific guidance is out of scope"). This SEP specifies comparison instead. The recorded issuer always comes from a metadata document the client has already validated per RFC 8414 §3.3, so a present iss can be checked against an authentic baseline; rejection on mismatch remains unconditional, so the only behavioral difference is accepting a response whose iss matches that baseline — which is not a relaxation. In practice, authorization servers often begin emitting iss before their metadata is updated, and discarding in that window would reject legitimate flows without security benefit.

Backward Compatibility

The iss parameter is additive on the wire. Client validation introduces a behavioral change for hosts whose authorization server advertises authorization_response_iss_parameter_supported: true but whose callback handling does not yet pass iss to the SDK; those flows will be rejected until the host extracts iss from the redirect URI alongside code. SDKs are expected to widen callback signatures additively (e.g., an optional iss argument) so existing call sites continue to compile. Authorization servers that do not advertise support are unaffected. The accompanying RFC 8414 Section 3.3 metadata-validation requirement restates an existing RFC MUST; clients that were not already enforcing it may surface latent issuer misconfigurations on upgrade.

Security Implications

This proposal is a mitigation against mix-up attacks; the security considerations for the mechanism itself are documented in RFC9207 Section 4. In particular, the mitigation depends on clients establishing the expected issuer before redirecting and on the comparison being an exact simple string comparison. See also the MCP security best practices.

Reference Implementation

Both record the expected issuer before redirect and compare any received iss, rejecting on absence only when the server advertises support.


Acknowledgments

Thanks to Sam Morrow, Max Gerber, Aaron Parecki, Stephen Halter, Nate Barbettini, Karl McGuinness, and Den Delimarsky for reviews and discussion in the Auth Mix-Up Attack Prevention working group.

SEP-2484 Require Conformance Tests for Standards Track SEPs to Reach Final Status

Final · Process · Created 2026-03-27

Abstract

This SEP adds a conformance test requirement to the Accepted → Final transition for Standards Track SEPs. Before a Standards Track SEP that changes observable protocol behavior can be marked Final, a conformance scenario covering its normative requirements must be merged into the conformance repository, accompanied by a structured traceability file mapping each MUST/MUST NOT and SHOULD/SHOULD NOT to a check or a documented exclusion. This keeps the conformance suite synchronized with the specification as it evolves, gives SDK maintainers an executable target for implementation, and makes SEP-1730's tier percentages a meaningful measure of spec coverage. Process and Informational SEPs are exempt, as are Standards Track SEPs with no observable protocol behavior.

Motivation

The gap between specification and implementation

The MCP specification is written in English. SDK maintainers translate that English into code, and every translation is an opportunity for drift. SEP-1730 (SDK Tiering) already depends on conformance tests (Tier 1 requires 100% pass rate, Tier 2 requires 80%), but there is no mechanism keeping the suite synchronized with the spec. When a SEP reaches Final, SDK maintainers implement from prose and hope they interpreted it the same way every other SDK did. Conformance tests arrive later, if at all, and when they do they sometimes reveal that two "compliant" SDKs disagree.

Why the existing reference implementation requirement is insufficient

A reference implementation proves the feature can be built: one valid interpretation. A conformance test defines what every implementation must do: the normative requirements as executable assertions. A TypeScript reference implementation tells a Rust maintainer little about whether their code is correct. A conformance test tells them precisely, and when it doesn't, the disagreement surfaces an ambiguity in the spec itself.

Keeping the conformance suite alive

The conformance suite is the yardstick SEP-1730's tier percentages measure against. If it falls behind, an SDK could be "100% compliant" while missing major specification features. Tying tests to the SEP lifecycle creates a forcing function: the suite grows exactly as fast as the spec does.

Specification

Scope

This requirement applies only to Standards Track SEPs that introduce or modify observable protocol behavior: behavior a conformant peer can detect by inspecting messages on the wire, transport-observable side effects (HTTP status codes, headers, connection lifecycle, OAuth redirects), or process-observable side effects for local transports (stdio stream content, exit codes).

The following are exempt:

  • Process SEPs (governance, workflow, community structure)
  • Informational SEPs (guidelines, best practices without normative force)
  • Standards Track SEPs with no observable protocol behavior, for example:
    • Documentation-only clarifications of existing behavior
    • Schema annotations that do not change validation or runtime behavior
    • Security recommendations describing implementation hardening rather than wire-level requirements

The conformance suite itself is not restricted to official SDKs. Any implementation (official SDK, community SDK, or custom deployment) may run it and report a compliance percentage.

The requirement

For a Standards Track SEP in scope to transition from Accepted to Final:

  1. A conformance scenario tagged with the SEP number is merged into the conformance repository, targeting the conformance repository's draft spec-version tag for the upcoming release.
  2. A traceability file accompanies the scenario. See below.
  3. The scenario passes against the SEP's reference implementation.

When that spec version is released, the scenario's spec-version tag is updated from the draft tag to the dated version as part of the normal release process. The conformance harness and the SDK under test must both recognize the draft tag as a negotiable protocol version so that the new requirements are actually exercised.

Traceability file

The traceability file is a structured file (sep-NNNN.yaml) in the conformance repository. It maps each normative requirement in the SEP's Specification section to the check that exercises it, or documents why it is excluded:

sep: 1234
spec_url: https://omcp.tech/specification/draft/section#anchor
requirements:
  - check: sep-1234-foo-present
    text: "MUST include `foo` in the response"
  - check: sep-1234-bar-absent
    text: "MUST NOT send `bar` before initialization"
  - check: sep-1234-qux-present
    text: "SHOULD include `qux` when available"
  - check: sep-1234-baz-rejected
    text: "MUST reject requests with invalid `baz`"

  - text: "MUST retry on 503"
    excluded: "Requires fault injection; not currently supported by framework"
    issue: https://github.com/modelcontextprotocol/conformance/issues/N
  - text: "MUST be rendered in a monospace font"
    excluded: "Client rendering; not observable at the protocol level"

Structured data lets tooling link check failures back to spec sections and lets the conformance CLI report coverage per SEP.

Exclusions come in two flavors. Framework gaps (the behavior is observable but the framework can't express it yet) should link a tracking issue. Not protocol-observable (the requirement governs client rendering, implementation internals, or similar) needs only the excluded reason. A SEP whose requirements are all the second kind is exempt and doesn't need a scenario at all.

The sponsor verifies the traceability file is complete: every MUST, MUST NOT, SHOULD, and SHOULD NOT (and RFC 2119 equivalents: SHALL, REQUIRED, RECOMMENDED) in the SEP's Specification section has a row. Checks for SHOULD-level requirements report as warnings rather than failures. MAY requirements do not need rows. The sponsor does not review test code; that is the conformance repository's normal PR review. What counts as a normative requirement is the sponsor's call.

Who writes the tests

The sponsor is responsible for ensuring a conformance scenario is written. Scenarios are authored in TypeScript; contributors unfamiliar with the conformance repository should start with its CONTRIBUTING guide. In practice the SEP author is often best positioned, since writing the test surfaces ambiguities in the normative language that are cheaper to fix before Final than after.

Specification text is authoritative

Conformance tests are derived from and subordinate to the specification text. Where a test and the spec disagree, the spec is authoritative and the test is a bug.

Conformance test disputes

If an implementer believes a merged conformance test contradicts the spec, they open an issue in the conformance repository citing the specific spec text. A test is considered disputed once a conformance maintainer applies the disputed label; disputed tests do not affect SEP-1730 tier assessments until resolved.

Most disputes resolve through normal issue triage: the test is fixed, the spec is clarified, or the dispute is closed with rationale. If the disagreement is fundamental (the disputing party and the conformance maintainers cannot agree on what the spec means), either party may escalate unilaterally to Core Maintainers for a ruling, though joint escalation is preferred since the goal is to resolve ambiguity rather than win an argument. The same escalation path is available to a sponsor if a scenario PR is blocked on non-technical grounds.

Test stability and tiering

SEP-1730 tier assessments are run against a pinned conformance release version, not the tip of the conformance repository. New checks added to a SEP's scenario after the SEP is Final (whether additional edge cases or coverage of previously-excluded requirements) land in the conformance repository's main branch but only affect tier percentages when the next tiering assessment adopts a newer conformance release.

This means SDK maintainers have a stable target between tiering waves, and the conformance suite can evolve continuously without surprise regressions in tier status.

Sponsor responsibilities

SEP-1850 makes the sponsor responsible for tracking reference implementation progress before marking a SEP as Final. This SEP extends that responsibility: for Standards Track SEPs in scope, the sponsor also confirms that a conformance scenario tagged with the SEP number is merged with a complete traceability file, or that an exemption is documented in the SEP.

Relationship to SEP-1730 (SDK Tiering)

This SEP strengthens SEP-1730's foundation without changing its tier definitions or thresholds. Tier assessments use pinned conformance releases, so new checks do not retroactively affect tier status. Disputed tests do not count toward tier percentages until resolved.

Scenario contributions covering existing spec behavior (not tied to a new SEP) remain welcome and are not required to carry a traceability file.

Relationship to SEP-1627 (Conformance Testing)

This SEP supersedes SEP-1627 by accepting the conformance repository as the canonical home for conformance tests and formalizing its role in the SEP lifecycle. SEP-1627's golden-trace approach was not carried forward; the scenario-and-checks model trades language-neutral fixtures for runtime expressiveness. SEP-1627's protocol-debugger ideas remain valuable future work.

Rationale

Why gate Final rather than Accepted?

Gating Accepted would require tests before Core Maintainers have agreed the feature belongs in the spec, wasting effort on rejected SEPs.

That said, writing a conformance test during SEP drafting is often valuable: it forces precision in MUST/MUST NOT language and surfaces edge cases the prose glosses over. Authors are encouraged to draft a conformance scenario before Core Maintainer review, especially for SEPs with complex behavioral requirements. It is not required, because small SEPs may not justify the upfront effort, and a rejected SEP's test is wasted work.

Gating Final places the hard requirement where the reference implementation requirement already sits: the SEP has consensus, and the remaining work is implementation.

Why a traceability file?

Without a defined coverage bar, "has a conformance test" would be relitigated on every SEP: does one check suffice, or must every MUST be covered? The traceability file makes coverage auditable: every normative statement has a row, and every row is either a check or a documented exclusion. "Sufficient" becomes "the file is complete."

The file also makes gaps visible. A SEP with ten MUSTs and eight exclusions is a signal: either the SEP is genuinely hard to test (the tracking issues say why) or the test author stopped early (the sponsor should push back).

Why put the authorship obligation on the sponsor?

The sponsor already shepherds the SEP through review, tracks the reference implementation, and manages status transitions. Adding "ensure a conformance test is written" is a small marginal addition to an existing role, with a clear owner.

Alternatives considered

Require conformance tests in the SEP PR itself. Rejected: couples two independent review processes with different maintainers and CI.

Gate only "major" SEPs. Rejected: "major" is subjective. The observable-behavior scope is objective: either a conformant peer can detect the change, or it cannot.

Make conformance maintainers the sufficiency judges. Rejected: concentrates veto power in a group not elected to approve spec changes. The traceability-file model lets the sponsor verify completeness without reading test code.

Backward Compatibility

This SEP is not retroactive. SEPs that reached Final before this SEP takes effect are not required to add conformance tests, though contributions are welcome.

Security Implications

None directly. Conformance tests that exercise security-relevant behavior (auth flows, input validation, transport security) improve the ecosystem's security posture by catching regressions, but this SEP does not mandate security-specific coverage beyond what the underlying SEP's MUSTs require.

Reference Implementation

The conformance repository already demonstrates the scenario-tagging pattern this SEP formalizes:

The structured traceability file format and the scenario scaffolding tool (npx @modelcontextprotocol/conformance new-scenario --sep <number>) will be added to the conformance repository before this SEP reaches Final.

The process change is implemented by updating docs/community/sep-guidelines.mdx to add the conformance check to the Accepted → Final transition (see the accompanying changes in this PR).

Prerequisites for Final status

Before this SEP itself can be marked Final, the following conformance-repository work must be complete:

  • Structured traceability file format (sep-NNNN.yaml) and schema
  • Scenario scaffolding tool
  • Conformance harness supports a draft spec-version tag as a negotiable protocol version
  • MAINTAINERS.md published and the repository listed in MCP governance documentation

These are this SEP's own reference implementation checklist, not ongoing process requirements.

SEP-2495 Event-Driven Tool Invocation (Server-Push to LLM Re-entry)

Draft · Standards Track · Created 2026-03-29

Abstract

This SEP proposes an event-driven re-entry mechanism for MCP that allows servers to trigger a new LLM turn in response to asynchronous events. Currently, MCP follows a strict request-response pattern: the LLM client can call tools on the server, but the server cannot initiate a new LLM turn when an event occurs. While MCP supports server-to-client notifications (e.g., notifications/tools/list_changed), these only update metadata — they never re-enter the LLM loop. This limitation prevents real-time, interactive applications where the LLM acts as the orchestrator reacting to user interactions, sensor data, webhooks, or other asynchronous events from the server side.

Motivation

The current request-response model of MCP makes the LLM a passive participant rather than an active orchestrator in interactive scenarios. Consider the following concrete example:

Interactive Desktop App via MCP:

An MCP server (N.E.O.) provides tools for compiling, previewing, and interacting with live Avalonia desktop applications. The intended workflow:

  1. LLM calls compile_and_preview → App window appears with a "Generate Quote" button
  2. LLM calls subscribe_events → Server starts collecting UI events
  3. User clicks the button in the app
  4. Server detects the click event
  5. GAP: The server has no way to notify the LLM client that an event occurred and trigger a new assistant turn
  6. LLM should automatically call get_events, read the click, then call inject_data to display a quote and set_property to change the background color

What actually happens today: The user must manually send a chat message ("I clicked the button") to prompt the LLM to poll get_events. This breaks the interactive experience entirely.

This limitation affects a wide range of use cases:

  • Live dashboards: Server pushes data updates, LLM re-renders visualizations
  • Chat bots with external triggers: Webhooks, emails, or messages arrive and the LLM reacts
  • IoT / sensor monitoring: Threshold alerts trigger LLM analysis
  • Collaborative editing: Multiple users interact with a shared app, LLM orchestrates
  • Game loops: User makes a move, server detects it, LLM responds
  • Form wizards: User fills step 1, LLM dynamically generates step 2 based on input
  • Long-running tasks: Server signals completion, LLM presents results

Imported from upstream. Proposed as modelcontextprotocol#2495 by @hf75 on 2026-03-29, 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 the document is incomplete: no Specification, Rationale, Backward Compatibility, or Security Implications. 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.

Current Workarounds (All Suboptimal)

Workaround Problem
User manually messages the LLM to poll Breaks interactivity, poor UX
Build all logic into the app itself Defeats the purpose of LLM orchestration

SEP-2532 Resource Streaming for Binary Content Delivery

Draft · Standards Track · Created 2026-04-07

  • Status: Draft
  • Type: Standards Track
  • Created: 2026-04-07
  • Author(s): Patrick Rodgers (@patrick-rodgers)
  • PR: TBD

Abstract

This SEP proposes adding a new resources/stream method to the Model Context Protocol, enabling servers to deliver resource content as raw binary HTTP streams rather than base64-encoded JSON-RPC payloads. The client sends a standard JSON-RPC request; the server responds with stream metadata including a downloadUrl. The client then performs a standard HTTP GET against that URL — using the MCP session's authorization credentials — and receives raw binary bytes with no JSON-RPC envelope, no base64 encoding, and no intermediary framing.

This mirrors established patterns in cloud APIs (e.g., Microsoft Graph's /content endpoint returning a redirect to a binary download stream) and leverages HTTP's native capabilities for chunked transfer, range requests, and content negotiation.

Both servers and clients opt into streaming support through capability negotiation, ensuring full backward compatibility. Existing resources/read behavior remains unchanged for context-window-oriented content delivery. This SEP is scoped to HTTP-based remote transports; stdio transport is explicitly out of scope.

Motivation

The Base64 Scaling Problem

The current resources/read method returns content as either text (UTF-8 string) or blob (base64-encoded binary) within a JSON-RPC response envelope. This design works well for small resources destined for a language model's context window, but creates significant problems at scale:

  1. Memory pressure: Base64 encoding inflates content by ~33%. The server must hold the entire encoded payload in memory to construct a valid JSON-RPC response. For a remote server hosting thousands of concurrent users reading multi-megabyte files (e.g., OneDrive documents, GitHub repository archives, SharePoint assets), this means gigabytes of unnecessary memory overhead.

  2. No streaming semantics: JSON-RPC responses must be complete before delivery. A 50MB PDF must be fully base64-encoded, wrapped in JSON, and buffered before a single byte reaches the client. This creates latency spikes and timeout risks.

  3. Context window waste: When a client needs a file's bytes (to save to disk, upload elsewhere, or pass to a tool), routing those bytes through base64 → JSON → parse → decode is pure overhead. The content was never intended for the LLM.

  4. Transport mismatch: Modern transports (Streamable HTTP, WebSocket) natively support binary streaming, but MCP forces all content through a text-based JSON-RPC envelope regardless of transport capabilities.

Real-World Use Cases Blocked Today

The following scenarios are impractical or broken with resources/read alone:

  • Cloud file storage (OneDrive, SharePoint, Google Drive, S3): Files range from kilobytes to gigabytes. Encoding a 100MB PowerPoint as base64 in a JSON-RPC response is not viable for remote servers at scale. (See #527 — "Base64 in JSON RPC will not scale for file content")

  • Repository content (GitHub, GitLab): Repository archives, large binaries, and media assets need to be delivered to clients for local caching or forwarding. GitHub's MCP server team has identified this as a scaling concern for remote servers at high concurrency. (#527 comments)

  • Document processing pipelines: A client may read a PDF resource from one MCP server and upload it to another (OCR service, document converter). The bytes never need LLM context — they're piped between servers. Today, the bytes are needlessly base64-encoded, JSON-wrapped, parsed, and decoded in the middle.

Imported from upstream. Proposed as modelcontextprotocol#2532 by @patrick-rodgers on 2026-04-07, 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.

  • Local file synchronization: An IDE client may want to download resources to the local filesystem for editing, diffing, or version control. Streaming to disk is natural; buffering the entire file as a JSON string is not.

Why Not Out-of-Band Downloads?

Previous proposals (PR #607 uriScope/supportsDirectRead, download URLs) solve this by exiting the protocol — the server provides a URL and the client fetches directly via HTTP. These approaches have merit but also significant limitations:

  1. Servers must become HTTP file servers: Not every MCP server can host download endpoints. Servers behind NAT or in containers may not be directly reachable for arbitrary HTTP requests.

  2. Auth fragmentation: Direct download URLs require separate auth handling (presigned URLs, token forwarding, OAuth token passing). MCP already has an auth model; ad-hoc exit from the protocol means reimplementing it.

  3. Network topology assumptions: The server doesn't know the client's network capabilities. A supportsDirectRead: true resource with an internal URL may be unreachable from the client's network. (connor4312 on PR #607)

  4. Loss of protocol guarantees: Progress reporting, cancellation, error handling, and subscription semantics all exist within MCP. Fully out-of-band downloads lose these.

resources/stream addresses these concerns by keeping the negotiation within the protocol (capabilities, auth, error handling) while delegating byte delivery to HTTP — the transport that already does this well. The server provides a downloadUrl scoped to the MCP session; the client fetches it with the session's credentials. Auth stays unified, the server controls the URL lifecycle, and protocol-level error handling covers the negotiation phase.

Scope

This SEP is scoped to HTTP-based remote transports (Streamable HTTP, and future transports such as WebSocket with HTTP sidecar). The stdio transport is explicitly out of scope — it is designed for local server communication where large binary file transfer is typically handled through filesystem access. A future SEP may address stdio binary framing if demand warrants it.

Community Signal

This problem has been raised repeatedly across the MCP specification repository:

Issue Title Signal
#527 Base64 in JSON RPC will not scale for file content Core issue; 15+ comments, GitHub + OneDrive contributors
#117 Streaming tool use results 49 upvotes, opened by MCP co-creator
#155 Passing attachments (resources) to MCP servers Foundational use case, led to SEP-1306 → SEP-2356
PR #607 Add Resource.uriScope to enable direct reads Closed, "needs a SEP"
SEP-2356 File input support for tools and elicitation Covers client→server; this SEP covers server→client

connor4312 (VS Code MCP) noted on PR #607:

"The main concern just becomes memory usage, which I think could be better solved with streamed reads."

This SEP is the formal proposal for that streamed read mechanism.

Specification

1. Capability Declaration

Server Capability

Servers that support resource streaming MUST declare the stream sub-capability within resources:

{
  "capabilities": {
    "resources": {
      "stream": true,
      "subscribe": true,
      "listChanged": true
    }
  }
}

The stream capability is independent of and additive to existing resource capabilities. A server MAY support any combination:

{ "capabilities": { "resources": { "stream": true } } }
{ "capabilities": { "resources": { "subscribe": true } } }
{
  "capabilities": {
    "resources": { "stream": true, "subscribe": true, "listChanged": true }
  }
}
Client Capability

Clients that can consume resource streams MUST declare the resourceStreaming capability:

{
  "capabilities": {
    "resourceStreaming": {
      "maxStreamSize": 1073741824
    }
  }
}
  • maxStreamSize (optional): Maximum byte size the client is willing to accept in a single stream. Servers SHOULD respect this limit and return an error if the resource exceeds it.

Servers MUST NOT send resources/stream responses to clients that did not declare resourceStreaming support.

2. Resource Metadata Extension

Resources that support streaming MAY indicate this via the existing resource object returned by resources/list:

{
  "uri": "onedrive:///documents/quarterly-report.pptx",
  "name": "quarterly-report.pptx",
  "title": "Q1 2026 Quarterly Report",
  "mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
  "size": 15728640,
  "annotations": {
    "audience": ["user"],
    "priority": 0.6
  },
  "streamable": true
}

The streamable field (boolean, optional, default false) indicates that the server supports resources/stream for this specific resource. This allows servers to selectively offer streaming for resources where it is appropriate (large binaries) while continuing to serve small text resources exclusively via resources/read.

If a server declares the stream capability, it SHOULD set streamable: true on resources where streaming is the preferred delivery mechanism.

3. The resources/stream Method

Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "resources/stream",
  "params": {
    "uri": "onedrive:///documents/quarterly-report.pptx"
  }
}

The request is a standard JSON-RPC call, identical in shape to resources/read. The method name itself is the signal that the client expects raw binary delivery rather than a base64-encoded JSON-RPC payload.

Response: Direct Binary Stream (Primary Mode)

This SEP extends the Streamable HTTP transport to allow a third response Content-Type for resources/stream requests specifically. Today, the transport spec requires servers to return either Content-Type: application/json or Content-Type: text/event-stream. For resources/stream, the server MAY also respond with the resource's actual media type and immediately begin streaming raw bytes:

HTTP/1.1 200 OK
Content-Type: application/vnd.openxmlformats-officedocument.presentationml.presentation
Content-Length: 15728640
Content-Disposition: attachment; filename="quarterly-report.pptx"
MCP-Resource-Uri: onedrive:///documents/quarterly-report.pptx

<raw binary bytes>

That's it. No JSON-RPC response envelope. No base64 encoding. No SSE framing. The server validates the request, resolves the resource, and starts pumping bytes — exactly like Microsoft Graph's /content endpoint or any standard HTTP file download.

The client knows it called resources/stream, so it inspects the response Content-Type to determine what it received:

  • application/json → A JSON-RPC error response (resource not found, stream not supported, etc.). Parse as JSON-RPC and handle the error.
  • Any other Content-Type → Raw binary stream. The Content-Type is the resource's MIME type. Write to disk, pipe to a consumer, or forward to another service.

This is the same content negotiation pattern used broadly in HTTP APIs — the caller knows to expect either an error payload or the content itself.

Required response headers for binary streams:

Header Requirement Purpose
Content-Type MUST The resource's MIME type. Clients use this to determine file handling.
Content-Length SHOULD Total size in bytes. Enables progress display and maxStreamSize enforcement before streaming begins.
Content-Disposition SHOULD Suggested filename via attachment; filename="...".
MCP-Resource-Uri MUST The canonical MCP resource URI. Allows the client to correlate the stream to the original request.

Client behavior for direct binary streams:

  1. Clients MUST check the response Content-Type before processing the body.
  2. If Content-Type is application/json, the client MUST parse the body as a JSON-RPC error response.
  3. For any other Content-Type, the client MUST consume the body as raw binary.
  4. Clients MUST enforce their declared maxStreamSize limit. If Content-Length exceeds it, or if bytes received exceed it, abort the download.
  5. Clients SHOULD stream the body to disk or a consumer incrementally — NOT buffer the entire response in memory.
  6. Clients MUST NOT inject the raw binary content into the LLM context window. The use of resources/stream (rather than resources/read) signals that this content is intended for file-level operations, not token consumption.
Response: Redirect Mode (Alternative)

Servers MAY respond with an HTTP redirect instead of streaming bytes directly. This is useful when the content is hosted in external storage (e.g., CDN, S3, Azure Blob) and the server wants to avoid proxying the bytes:

HTTP/1.1 302 Found
Location: https://storage.blob.core.windows.net/container/quarterly-report.pptx?sv=2024-05-04&sig=...
MCP-Resource-Uri: onedrive:///documents/quarterly-report.pptx

The client follows the redirect transparently and receives the raw binary stream from the storage endpoint.

This enables a pattern identical to Microsoft Graph:

  • GET /me/drive/items/{id}/content → 302 → presigned blob URL → raw bytes

Requirements for redirect mode:

  1. Servers MUST include the MCP-Resource-Uri header on the redirect response.
  2. The redirect target URL SHOULD be self-authenticating (e.g., presigned URL with embedded signature) so that the client does not need to forward MCP session credentials to a third-party storage endpoint.
  3. If the redirect target requires authentication, the server MUST document the expected auth mechanism.
  4. Redirect target URLs SHOULD be time-limited (e.g., valid for 5-15 minutes).
  5. Clients MUST handle HTTP 302 and 307 redirects transparently.
Response: Download URL Mode (Alternative)

For servers that cannot respond with binary directly on the MCP endpoint (e.g., architectural constraints, load balancer limitations), the server MAY respond with a JSON-RPC result containing a downloadUrl:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "uri": "onedrive:///documents/quarterly-report.pptx",
    "mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
    "size": 15728640,
    "downloadUrl": "https://mcp-server.example.com/streams/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

The client detects this mode because Content-Type: application/json with a result (not an error) indicates a metadata response. The client then performs a separate HTTP GET to the downloadUrl with session credentials.

Requirements for downloadUrl mode:

  1. The downloadUrl MUST use HTTPS.
  2. The URL MUST be scoped to the authenticated MCP session and SHOULD be single-use or time-limited.
  3. Clients MUST include the MCP session's authorization credentials when fetching the downloadUrl, unless it is self-authenticating.
  4. The downloadUrl endpoint returns raw binary with the same headers as the direct binary stream mode.

This mode adds a round trip but may be necessary for servers that share a single HTTP endpoint for all MCP traffic and cannot return non-JSON responses on it.

Response Mode Summary

The client handles all three modes through a simple Content-Type check on the response to its resources/stream POST:

Response Content-Type Body Client Action
Resource MIME type (e.g., application/pdf) Raw binary bytes Stream to disk / consumer
Redirect (302/307) Empty (Location header) Follow redirect, stream the target
application/json with error JSON-RPC error Handle error (not found, not streamable, etc.)
application/json with result.downloadUrl JSON-RPC metadata GET the downloadUrl, stream the result

Servers SHOULD prefer direct binary streaming or redirect mode for simplicity and efficiency. The downloadUrl mode exists as a fallback for constrained server architectures.

Example Flows

Flow 1: Direct binary stream (preferred — like Graph /contentStream)

Cloud StorageMCP ServerMCP ClientCloud StorageMCP ServerMCP ClientPOST /mcp (resources/stream { uri: "onedrive:///docs/report.pptx" })Fetch content streamBinary stream200 OK, Content-Type: application/vnd...pptx, raw bytesWrite to disk

Flow 2: Redirect to storage (like Graph /content → 302)

Cloud Storage (presigned URL)MCP ServerMCP ClientCloud Storage (presigned URL)MCP ServerMCP ClientPOST /mcp (resources/stream { uri: "onedrive:///docs/report.pptx" })Generate presigned URL for resource302 Found, Location: https://storage.blob.../report.pptx?sig=...GET (follows redirect)200 OK, raw binary streamWrite to disk

Flow 3: Download URL fallback

MCP ServerMCP ClientMCP ServerMCP ClientPOST /mcp (resources/stream { uri: "repo:///archive.tar.gz" })200 OK, Content-Type: application/json{ result: { downloadUrl, mimeType, size } }GET /streams/x9y8z7... (Authorization: Bearer <token>)200 OK, Content-Type: application/gzip, raw bytesWrite to disk

Flow 4: Error

MCP ServerMCP ClientMCP ServerMCP ClientPOST /mcp (resources/stream { uri: "onedrive:///missing.doc" })200 OK, Content-Type: application/json{ error: { code: -32002, message: "Resource not found" } }Handle error
Error Handling

Errors during the resources/stream request use standard JSON-RPC errors returned as Content-Type: application/json:

Code Message Meaning
-32002 Resource not found The requested URI does not exist
-32003 Stream not supported Server doesn't support streaming for this resource
-32004 Resource too large Resource exceeds client's maxStreamSize
-32603 Internal error Server-side failure preparing the stream

Example:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32003,
    "message": "Stream not supported",
    "data": {
      "uri": "file:///small-config.json",
      "suggestion": "Use resources/read for this resource"
    }
  }
}

Errors during the binary stream itself (after bytes have started flowing) manifest as HTTP-level failures:

  • The connection may drop (client treats as incomplete transfer)
  • The server may send fewer bytes than Content-Length promised (client detects and retries or discards)

For download URL mode, errors at the download endpoint use standard HTTP status codes:

HTTP Status Meaning
200 Success — binary stream follows
206 Partial content (range request)
401 Session credentials invalid or expired
404 Download URL not found or already consumed
410 Download URL expired
500 Server error during content delivery

Clients SHOULD treat download failures as retriable. If the URL returns 410 Gone, the client MAY re-issue the resources/stream JSON-RPC request to obtain a fresh URL.

4. Transport Considerations

Streamable HTTP Transport Extension

This SEP proposes a targeted extension to the Streamable HTTP transport specification. Currently, the spec states:

"If the body is a JSON-RPC request, the server MUST either return Content-Type: text/event-stream, to initiate an SSE stream, or Content-Type: application/json, to return one JSON object."

For resources/stream requests only, this SEP adds a third permitted response type:

For requests with method resources/stream, the server MAY additionally respond with any Content-Type matching the resource's MIME type, delivering the raw binary content directly in the HTTP response body.

This extension is narrowly scoped — it applies only to resources/stream, not to any other JSON-RPC method. All other MCP methods continue to use application/json or text/event-stream exclusively.

Why this is safe:

  • The client explicitly called resources/stream, so it already expects non-JSON responses.
  • The client checks Content-Type before parsing the body — application/json means error/metadata, anything else means binary.
  • Existing MCP methods are unaffected.
  • Clients that don't support resources/stream (i.e., didn't declare resourceStreaming) will never call it, so they never encounter a binary response.
Streamable HTTP (Primary Target)

The Streamable HTTP transport is the natural fit for resources/stream. The interaction cleanly maps to HTTP semantics:

  • Request: Standard JSON-RPC POST to the MCP endpoint (same as any other MCP method)
  • Success response: HTTP 200 with the resource's Content-Type and raw binary body, OR HTTP 302/307 redirect to storage
  • Error response: HTTP 200 with Content-Type: application/json containing a JSON-RPC error
  • Metadata response: HTTP 200 with Content-Type: application/json containing a JSON-RPC result with downloadUrl (fallback mode)

This leverages HTTP's native capabilities — chunked transfer encoding, Content-Length, Content-Disposition, range requests, redirects, CDN caching — without reinventing any of them inside JSON-RPC.

The server's MCP endpoint and any redirect targets MAY be on different origins. When different origins are used, the server is responsible for ensuring the target is accessible to the client and properly authenticated (e.g., via presigned URLs).

WebSocket Transport

For WebSocket-based MCP connections, the JSON-RPC request and response flow over the WebSocket. Since WebSocket frames cannot carry HTTP-style headers, resources/stream over WebSocket MUST use the download URL mode: the server returns a JSON-RPC result with a downloadUrl, and the client performs a separate HTTP GET.

This is deliberate — using a separate HTTP request:

  • Avoids blocking the WebSocket connection for the duration of a large download
  • Allows the server to delegate to CDN/storage without proxying through the WebSocket
  • Keeps the WebSocket free for concurrent JSON-RPC messages
stdio Transport

The stdio transport is out of scope for this SEP. resources/stream is designed for remote server scenarios where HTTP is available for binary content delivery. Local stdio-based servers typically have direct filesystem access, making binary streaming over the protocol unnecessary.

A future SEP may propose binary framing for stdio if demand warrants it. Servers using stdio transport SHOULD NOT declare the stream capability.

5. Interaction with Existing Features

Relationship to resources/read

resources/stream does NOT replace resources/read. The two methods serve different purposes:

Aspect resources/read resources/stream
Primary use Content for LLM context window Raw bytes for files, processing, storage
Response JSON-RPC with inline text/blob Direct binary HTTP response (or redirect/downloadUrl)
Content encoding UTF-8 text or base64 blob Raw binary — no encoding overhead
Content size Small to medium (fits in memory) Any size (streamed to disk)
Client intent "Give me content to reason about" "Give me bytes to save/forward"
Transport Always JSON-RPC HTTP binary response on same MCP endpoint

Servers that support both MUST continue responding to resources/read for all resources. A client MAY use either method for any streamable resource — for example, a client might resources/read a PDF to extract text for the LLM, or resources/stream the same PDF to save it to disk.

Subscriptions

Resource subscriptions (resources/subscribe, notifications/resources/updated) work identically for streamable resources. When a subscribed resource changes, the client receives the update notification and may choose to re-read via resources/read or re-stream via resources/stream.

Resource Templates

Resource templates (resources/templates/list) MAY include streamable: true in the template definition to indicate that instantiated resources support streaming.

6. Error Handling

Error handling for resources/stream is covered in detail in Section 3, Error Handling. In summary:

  • Protocol-level errors (resource not found, stream not supported, too large): Returned as Content-Type: application/json JSON-RPC error responses on the same POST request.
  • HTTP-level errors (download URL expired, auth failure): Standard HTTP status codes on the download request (for download URL mode) or on the binary response itself.
  • Transfer-level errors (connection drop, incomplete transfer): Detected by comparing bytes received against Content-Length. Clients SHOULD retry.

Rationale

Why a New Method Instead of Extending resources/read?

Adding streaming as a parameter to resources/read (e.g., "stream": true) was considered but rejected because:

  1. Semantic clarity: The method name itself communicates intent. Clients and servers can route, log, and handle the two methods differently without inspecting parameters.

  2. Backward compatibility: Existing resources/read handlers don't need modification. New method = new handler.

  3. Transport optimization: Transports can specialize their handling. A Streamable HTTP transport can return a binary response for resources/stream while continuing to return JSON for resources/read, without conditional logic per-request.

  4. SDK ergonomics: client.readResource(uri) returns content for the LLM. client.streamResource(uri) returns a readable stream / file handle. Clean separation.

Why Not Just Download URLs?

Previous proposals (PR #607, supportsDirectRead) suggested adding download URLs as a property on the resource object itself, with the client fetching directly. This SEP takes a different approach: the downloadUrl is returned as part of a resources/stream JSON-RPC response, not as a static resource property.

This distinction matters because:

  • Dynamic URL generation: The server creates the download URL at request time, scoped to the session and time-limited. A static downloadUrl on a resource listing would require either long-lived URLs (security risk) or constant re-generation (complexity).
  • Auth consistency: The client already has session credentials from the MCP connection. The resources/stream method specifies exactly how those credentials flow to the download — no need for presigned URL infrastructure.
  • Protocol-level error handling: If the resource can't be streamed (not found, too large, not streamable), the server returns a JSON-RPC error before the client attempts any download. With static download URLs, errors only surface at HTTP fetch time.
  • Explicit intent: The client calling resources/stream tells the server "I want binary bytes, prepare a download for me." The server can provision appropriately (e.g., request a download URL from Graph API, generate a presigned URL, set up a proxy). With static URLs, the server has to speculatively prepare download paths for all resources.

Why Capability Negotiation on Both Sides?

Both server and client must opt in because:

  • Server: Not all resources are streamable (e.g., dynamically computed text). The server knows which resources support streaming.
  • Client: Not all clients can handle binary streams (e.g., a simple chatbot UI has no filesystem). The client knows its consumption capabilities.

Without both sides opting in, you'd get servers sending binary data to clients that can't handle it, or clients requesting streams from servers that can only produce JSON.

Alternatives Considered

  1. supportsDirectRead / uriScope (PR #607): Static download URL as a resource property. Problems: URL lifecycle management, auth fragmentation, no error handling before fetch. This SEP's dynamic downloadUrl via resources/stream solves all three.

  2. Binary framing within JSON-RPC: Streaming base64-chunked data as JSON-RPC notifications. Retains encoding overhead, adds protocol complexity (stream IDs, chunk sequencing, completion signals). We considered this for stdio support but concluded it's better left to a future SEP.

  3. SSE binary events: Delivering binary content as SSE data: fields (base64 encoded) on the existing Streamable HTTP connection. Same encoding overhead as resources/read, blocks the SSE connection for the duration of the download, prevents concurrent messages.

  4. New transport-level primitive: A generic binary channel in the transport layer. Too broad, too disruptive, requires changes to every transport and SDK. resources/stream is a protocol-level method that works with existing HTTP infrastructure.

  5. Multipart JSON-RPC responses: Non-standard, breaks existing JSON-RPC libraries, limited ecosystem support.

Backward Compatibility

This SEP introduces no backward incompatibilities.

  • resources/read remains unchanged and MUST continue to work for all resources.
  • The stream capability is opt-in for both servers and clients.
  • Servers that don't declare stream in their capabilities will never receive resources/stream requests (well-behaved clients check capabilities).
  • Clients that don't declare resourceStreaming will never receive stream data.
  • The streamable field on resources defaults to false and is ignored by clients that don't understand it.
  • Existing transports, SDKs, and implementations require zero changes to continue functioning.

Migration path: Implementations adopt streaming incrementally. A server can start by marking a few large-file resources as streamable: true. Clients can add streaming support when they're ready. There is no flag day.

Security Implications

New Attack Surfaces

  1. Resource exhaustion via large downloads: A malicious server could attempt to overwhelm a client with an unbounded stream. Mitigation: Client's maxStreamSize capability sets an upper bound. Clients MUST enforce this limit — checking Content-Length before streaming begins and aborting if bytes received exceed the limit.

  2. Download URL abuse: If download URLs are predictable or long-lived, an attacker could access resource content. Mitigation: Download URLs MUST be cryptographically unguessable (e.g., containing a UUID v4 or HMAC-signed token), session-scoped, and time-limited. Servers SHOULD make URLs single-use.

  3. Open redirect via downloadUrl: A compromised or malicious server could return a downloadUrl pointing to an attacker-controlled endpoint. Mitigation: Clients SHOULD validate that the downloadUrl origin matches the MCP server's origin or a set of trusted origins. Clients MUST apply the same TLS verification to the download as to the MCP connection.

  4. Incomplete download exploitation: A client might act on partially downloaded content (e.g., a truncated archive). Mitigation: Clients SHOULD verify Content-Length matches bytes received. Servers MAY include integrity metadata (e.g., ETag, checksum headers) in the download response.

Privacy Considerations

  • Download content has the same privacy characteristics as resources/read content — it is accessed using the same session credentials.
  • The downloadUrl exposes content via an HTTP endpoint. The URL MUST be session-scoped and time-limited to prevent unauthorized access.
  • Download URLs MUST NOT encode or leak information about the resource content, user identity, or server state in their path or query parameters (beyond what is necessary for routing).

Transport Security

  • The downloadUrl MUST use HTTPS/TLS.
  • Clients MUST include the MCP session's authorization credentials when fetching the downloadUrl, unless the URL is self-authenticating (e.g., contains a cryptographic signature in its query parameters, as with presigned URLs).
  • Servers SHOULD set appropriate cache-control headers (Cache-Control: no-store) on download responses to prevent sensitive content from being cached by intermediaries.

Reference Implementation

Note: A reference implementation is required before this SEP can reach "Final" status. The following describes the planned implementation approach.

Planned Implementation

  1. TypeScript SDK: Fork of @modelcontextprotocol/sdk adding:

    • StreamableResource type extending Resource with streamable field
    • client.streamResource(uri) method that returns { downloadUrl, mimeType, size } and provides a helper to fetch the binary stream as a ReadableStream<Uint8Array>
    • Server-side server.setStreamHandler() for registering stream providers that return download URLs
    • Streamable HTTP transport integration
  2. Proof-of-Concept Server: MCP server for OneDrive/SharePoint file access demonstrating:

    • resources/stream returning Graph API download URLs (mirroring /content → 302 pattern)
    • Progress reporting for multi-megabyte files via Content-Length and client-side tracking
    • Fallback to resources/read for small text files
  3. Proof-of-Concept Client: CLI client demonstrating:

    • Stream-to-disk for large resources via HTTP GET to downloadUrl
    • Progress bar display using Content-Length headers
    • maxStreamSize enforcement

Performance Implications

Scenario resources/read resources/stream (direct) resources/stream (redirect)
1MB text file ~1.33MB JSON payload ~1MB raw HTTP response ~1MB from storage
50MB binary file ~67MB JSON, full buffer ~50MB chunked HTTP ~50MB from CDN
500MB archive Likely OOM or timeout ~500MB chunked HTTP ~500MB from storage
Server memory (per request) Full payload in memory Constant (pipe-through) Near zero (redirect)
Round trips 1 1 1 (POST) + 1 (redirect follow)

Key wins:

  • Peak memory: From O(file_size) to O(chunk_size) for the server
  • Time to first byte: Immediate (no need to buffer entire file)
  • Transport efficiency: Raw binary over HTTP avoids 33% base64 overhead

Open Questions

  1. Transport spec amendment process: This SEP proposes extending the Streamable HTTP transport to allow binary Content-Types for resources/stream responses. Should this be a standalone transport amendment, or is it acceptable as part of this SEP? Current thinking: include it here since the extension is narrowly scoped to a single method.

  2. Resume support: Should the spec define a mechanism for resuming interrupted streams (e.g., Accept-Ranges / Range headers on the binary response)? Current thinking: Servers SHOULD support range requests, but this SEP does not mandate it. HTTP range requests are well-understood and servers can adopt them without protocol-level specification.

  3. Relationship to streaming tool results (#117): This SEP addresses resource streaming specifically. Should tool results have an analogous mechanism for returning binary content? Current thinking: Yes, but as a separate SEP. The patterns established here (binary HTTP response, Content-Type switching) could be reused.

  4. Accept header from client: Should the client include an additional value in the Accept header when POSTing a resources/stream request (e.g., Accept: application/json, */*) to signal willingness to receive binary? Current thinking: Yes, the client SHOULD include */* or the expected MIME type in Accept, in addition to application/json. This gives standard HTTP intermediaries correct caching/routing signals.

  5. Concurrent stream limits: Should there be a capability for clients to declare how many concurrent streams they support? Current thinking: Out of scope. Clients can manage their own concurrency. Servers can rate-limit download endpoints independently.

Acknowledgments

  • @SamMorrowDrums — Original issue #527 identifying the base64 scaling problem from the GitHub MCP server perspective
  • @jonathanhefner — supportsDirectRead / uriScope proposal in PR #607, which shaped the out-of-band download approach incorporated here
  • @connor4312 — Key insight that "memory usage could be better solved with streamed reads" and critical review of network topology assumptions
  • @LucaButBoring — Sequence diagram and dual-path (in-protocol + direct download) architecture thinking
  • @jspahrsummers — Original streaming tool results issue #117
  • @localden — Binary elicitation work (SEP-1306) exploring upload patterns that inform the bidirectional story

SEP-2549 TTL for List Results

Final · Standards Track · Created 2026-04-09

Abstract

This SEP proposes adding fields to support caching result objects returned by tools/list, prompts/list, resources/list, resources/read, and resources/templates/list. Two fields will be added ttlMs and cacheScope. The TTL tells clients how long the response may be considered fresh before re-fetching. This allows clients to cache feature lists and reduce reliance on server-push notifications while remaining fully backward compatible. The cacheScope field controls who may cache a response. TTL supplements rather than replaces the existing notification mechanism — both can coexist.

Motivation

Today, MCP clients discover server features by invoking methods on the server. These calls return the current set of features. To learn about changes, clients rely on push notifications from the server. The below table maps the Server Method to Notification Type.

Server Methods Notification Type
tools/list notifications/tools/list_changed
prompts/list notifications/prompts/list_changed
resources/list notifications/resources/list_changed
resources/templates/list notifications/resources/list_changed
resources/read notifications/resources/updated

This approach has several limitations:

  1. HTTP-based transports require SSE Streams: Many clients and servers have challenges supporting long lived SSE streams which are necessary for notifications. The goal is to make SSE streams an optional optimization, but support protocol functionality without them. A TTL allows clients to poll on a predictable schedule without relying on server-push notifications.

  2. Implementation complexity: Both clients and servers must implement notification subscription and delivery infrastructure. Many simple servers have feature lists that change infrequently (or never), yet must still support the notification machinery if they want clients to stay current.

  3. No freshness signal: Even clients that can receive notifications have no indication of how "stable" a list is. A server whose tool list changes once a day and one whose list changes every second look identical to the client — both simply send notifications when changes occur. A TTL provides an explicit freshness hint.

  4. Alignment with web standards: HTTP caching (Cache-Control: max-age) and DNS TTLs have long demonstrated that time-based freshness hints are a simple, well-understood mechanism for reducing unnecessary refetches. MCP can benefit from the same pattern.

Adding a TTL field to list responses solves all of these problems with a minimal, backward-compatible protocol change.

Specification

New interface: CacheableResult

A new CacheableResult interface is introduced as a standalone type extending Result. It owns the ttlMs and cacheScope fields.

Schema change (TypeScript)
/**
 * A result that supports a time-to-live (TTL) hint for client-side caching.
 *
 * @internal
 */
export interface CacheableResult extends Result {
  /**
   * A hint from the server indicating how long (in milliseconds) the
   * client MAY cache this response before re-fetching. Semantics are
   * analogous to HTTP Cache-Control max-age.
   *
   * - If 0, The response SHOULD be considered immediately stale, The client
   *   MAY re-fetch every time the result is needed.
   * - If positive, the client SHOULD consider the result fresh for this many
   *   milliseconds after receiving the response.
   */
  ttlMs: number & { readonly minimum: 0 };

  /**
   * Indicates the intended scope of the cached response, analogous to HTTP
   * Cache-Control: public vs Cache-Control: private.
   *
   * - "public": Any client or intermediary (e.g., shared gateway, proxy)
   *   MAY cache the response and serve it to any user.
   * - "private": Only the requesting user's client MAY cache the response.
   *   Shared caches (e.g., multi-tenant gateways) MUST NOT serve a cached
   *   copy to a different user.
   *
   * Defaults to "public" if absent.
   */
  cacheScope: "public" | "private";
}

Semantics

A TTL is a freshness estimate, not a guarantee. Servers MAY change the underlying list before the TTL expires; servers that do so and have advertised listChanged SHOULD send the corresponding notification.

Servers MUST provide a ttlMs on Results returned by tools/list, prompts/list, resources/list, resources/read, and resources/templates/list.

ttlMs MUST be >= 0. If a server returns a negative value, clients SHOULD ignore it and treat it as 0 (immediately stale).

Condition Client behavior
ttlMs = 0 The response SHOULD be considered immediately stale, The Client MAY re-fetch every time the result is needed.
ttlMs > 0 Client SHOULD consider the response fresh for ttlMs milliseconds from receipt.
Relevant notification received while TTL is active The notification invalidates the cached response. Client SHOULD re-fetch regardless of remaining TTL.
cacheScope = "public" Any client or shared intermediary (gateway, proxy) MAY cache and serve the response to any user.
cacheScope = "private" Only the requesting user's client MAY cache. Shared caches MUST NOT serve a cached copy to a different user.
Freshness calculation

A client records the local time at which the response was received (t_received). The response is considered fresh while now < t_received + ttlMs. Once the TTL expires the response is stale and the client SHOULD re-fetch on next access.

Clients SHOULD NOT treat TTL as a polling interval that triggers automatic background refetches. The TTL is a freshness hint: the client checks freshness when it needs the list, and re-fetches only if stale. Implementations that do choose to poll SHOULD apply jitter and backoff.

Clients MAY re-fetch if they have reason to believe the data has changed, even if the TTL has not yet expired. Examples include receiving an unexpected error on a tool call indicating that the the method was not found or the parameters were invalid.

Clients MAY serve stale responses if errors occur in re-fetching results(e.g., network issues, server downtime). The TTL is a hint for how long the client can safely rely on the data, but real-world conditions may require flexibility.

Cache scope

The cacheScope field controls who may cache a response:

  • "public": The response does not contain user-specific data. Any client, shared gateway, or caching proxy MAY store and serve the cached response to any user. This is appropriate for lists of tools, prompts, and resource templates that are identical for all users.
  • "private": The response contains user-specific data. Only the requesting user's client MAY cache it. Shared caches (e.g., multi-tenant API gateways) MUST NOT serve a "private" cached response to a different user. This is appropriate for resources/read results that depend on the authenticated user, or for filtered list results that vary per user.

This design mirrors HTTP Cache-Control: public vs Cache-Control: private, applying the same well-understood semantics at the MCP protocol level.

Interaction with notifications

TTL and server-push notifications are complementary:

  • A server MAY provide ttlMs without advertising listChanged: true in its capabilities. In this case the client relies entirely on TTL.
  • A server MAY advertise listChanged: true and provide ttlMs. In this case the client can use the TTL to avoid unnecessary refetches between notifications, and the notification acts as an immediate invalidation signal.
ServerClientServerClientCache response, fresh for 5 min2 minutes later...3 minutes later (TTL expired)...Tools change before TTL expiresInvalidate cache immediatelytools/list{ tools: [...], ttlMs: 300000 }Need tools list → cache still fresh, use cachedNeed tools list → cache staletools/list{ tools: [...], ttlMs: 300000 }notifications/tools/list_changedtools/list{ tools: [...], ttlMs: 300000 }

Interaction with pagination

When a list result is paginated (includes nextCursor), each page is an independently cacheable response — consistent with how HTTP Cache-Control treats paginated resources. Specifically:

  • Each page response carries its own ttlMs value. The freshness clock for each page starts at the time that page was received.
  • Servers MAY return different ttlMs values on different pages (e.g., a longer TTL for early pages of a stable list, a shorter TTL for the final page).
  • There is no cross-page consistency guarantee. If the underlying data changes between page fetches, clients may observe duplicates or gaps — the same trade-off that applies to HTTP paginated APIs.
  • Clients that require a consistent snapshot of the full list SHOULD re-fetch from the beginning (without a cursor).
  • If a cursor becomes invalid (e.g., the server returns an error for a previously valid cursor), the client SHOULD discard all cached pages and re-fetch from the beginning.

Servers MUST apply the same cacheScope to all response pages for a given list request. For example, if the first page of a tools/list response has cacheScope: "private", all subsequent pages for that request MUST also be treated as "private".

Error handling

  • For backwards compatibility, If ttlMs is missing, clients SHOULD assume a default ttlMs of 0 (immediately stale) and rely on their own caching heuristics or notifications.
  • If ttlMs is present but is a negative integer, the client SHOULD ignore it and behave as if it were 0 (immediately stale).

Rationale

Why not replace list_changed notifications?

Notifications provide immediate invalidation which is valuable for long-lived connections. TTL provides a complementary mechanism optimized for stateless transports and for reducing unnecessary polling. Both mechanisms serve different use cases and coexist naturally.

Why integer milliseconds for TTL?

We chose integer milliseconds over seconds as we want one unit for ttl across the MCP protocol. Tasks has uses cases for sub-second TTLs, and using milliseconds allows for a consistent representation across all TTLs in MCP.

Many existing systems use integer seconds for TTLs, but some (e.g., gRPC retry pushback) use milliseconds. The key is to choose a single, consistent unit for all TTLs in MCP. Integer milliseconds provides the necessary precision while remaining simple to implement and understand.

System Mechanism Notes
HTTP Cache-Control: max-age Integer seconds The most widely deployed freshness hint in web infrastructure
DNS TTL Integer seconds Controls how long resolvers cache DNS records
GraphQL @cacheControl maxAge integer secs Per-field cache hints in GraphQL responses
gRPC grpc-retry-pushback-ms Milliseconds Server-provided retry hint (different use case, similar pattern)

Why not use HTTP caching directly?

MCP is transport-agnostic. While HTTP-based transports could theoretically use Cache-Control headers, MCP also operates over stdio, and supports pluggable transports where HTTP headers may not be available. Embedding the TTL in the JSON response body ensures it works uniformly across all transports.

Backward Compatibility

  • Existing servers that do not provide it continue to work unchanged. If a ttlMs field is missing, clients SHOULD assume a default ttlMs of 0 (immediately stale) and rely on their own caching heuristics or notifications, which is the current behavior.
  • Existing clients that do not understand the field will ignore it, as MCP result objects permit additional properties via [key: string]: unknown on the Result base type.
  • cacheScope is required because there is no safe default for older servers. The server must explicitly declare the intended cache scope to prevent unintended caching of user-specific data.
  • No existing fields or behaviors are modified or removed.
  • No capability negotiation is required.
  • SDK Maintainers can choose to add defaults for ttl and cacheScope in their SDKs to simplify adoption, but this is not required for compliance.

Reference Implementation

No reference implementation yet.


Security Implications

A misconfigured or malicious server could set an excessively long TTL, causing clients to cache stale data for longer than desired. However, since the TTL is a hint and clients can choose to ignore it or re-fetch if they suspect changes, the security risk is minimal. Clients should be designed to handle unexpected TTL values gracefully.

SEP-2567 Sessionless MCP via Explicit State Handles

Final · Standards Track · Created 2026-03-11

Abstract

This proposal removes the protocol-level session concept from MCP, replacing implicit session-scoped state with explicit, server-minted state handles that the model carries and threads through subsequent calls. SEP-2575 removes the initialize handshake and carries protocol version and capabilities per-request; this proposal is the complementary change that removes sessions and the Mcp-Session-Id header. Together they make MCP stateless at the protocol layer.

After more than a year in the spec, sessions have not converged on a consistent meaning across clients: some scope them per tool call, some per application launch, some per page load, and almost none resume them. A server author cannot predict what scope or lifetime a session will have when their server is connected to an arbitrary client, which has made the session unreliable as a container for application state. This proposal holds that application state can be served by explicit identifiers, and that the session abstraction adds constraints (fixed cardinality, undefined lifetime, uncacheable list endpoints across session boundaries) without corresponding benefit.

Under this proposal, a server that currently scopes a shopping cart (for example) to the session instead exposes a tool create_basket() that returns a basket_id and threads that ID through subsequent tool calls, e.g. add_item(basket_id, ...). The model decides what is shared and what is isolated; list endpoints become cacheable across what used to be session boundaries; and agent orchestrators can freely share or not share application state as needed. Explicit state handles are not a new protocol construct — there is no schema or wire format for them. They are a tool-design pattern; the protocol change is the removal of sessions, which leaves handles as the way to express cross-call state.

Motivation

What sessions scope today

The current spec is imprecise about which behaviors are session-bound, but in practice five categories of things attach to a session's lifetime:

  1. Negotiated capabilities and protocol version. The result of initialize — which protocol version is in use and which optional capabilities each side supports — is established once per session and assumed for its duration. SEP-2575 resolves this by removing initialize and carrying version/capability information per-request, so this proposal treats it as already addressed.

  2. Elicitation and sampling intermediate state. When a tool call triggers an elicitation/create or sampling/createMessage round-trip, the server has to correlate the eventual response with the original in-flight tool call — state that today lives implicitly in the session. SEP-2322 (Multi Round-Trip Requests) resolves this by carrying the correlation state explicitly through the request/response cycle, so this proposal treats it as already addressed.

  3. Application state. The canonical example is a shopping cart: add_item(), add_item(), checkout(), with the cart existing implicitly per-session. This generalizes to any stateful workflow — a Playwright browser instance, a database transaction, an open file descriptor.

  4. Mutable list endpoints. tools/list (and resources/list, prompts/list) can legally return different results over a session's lifetime. For example, a database server could expose a connect_database tool that, once called, makes query and list_tables appear in subsequent tools/list results.

  5. Resource subscriptions. Subscription lifetime is tied to session lifetime. (SEP-2575 introduces messages/listen as the delivery channel for server-to-client notifications; subscription lifetime under that model is not re-examined here.)

With (1), (2), and (5) handled by other SEPs, this proposal addresses (3) and (4).

Problems with session scoping

The issues below apply whether sessions are mandatory (the current spec) or made optional.

Session lifetime is undefined, and servers can't design around it

The spec does not say when a session begins or ends, because it depends on the host application. In practice, deployed clients vary widely and few scope sessions to a conversation: ChatGPT creates a fresh session for every individual tool call, and Claude.ai did the same until recently;1 most desktop and IDE clients create one at application launch and keep it for the process lifetime; web clients typically create one per page load. Almost no clients resume a prior session after a disconnect or restart, and on the server side the reference TypeScript SDK provides no public API for reconstructing a session on a different node, so multi-node deployments cannot honor resumption even when a client attempts it.2 A subagent might share its parent's session or get its own — there is no convention.

This matters because server authors are the ones deciding what to scope to the session, and they need to know what a session corresponds to in order to do that correctly. A Playwright server that ties a browser instance to the session needs to know whether that means one user turn, one agent process, or one long-lived chat. The spec does not specify this and different hosts give different answers, so the server is designing against an abstraction whose semantics it does not control.

The practical consequence is that session-scoped application state often does not survive. Against a per-tool-call client it is destroyed before the next call; against a per-app-launch client it is shared across every conversation in the window and then lost on restart; against any client that does not resume, it is gone when the app restarts. Servers that appear to be using session state successfully are usually stdio servers relying on process lifetime, which is a property of the transport rather than the protocol.

List endpoints cannot be cached across sessions

Because tools/list may be session-dependent, a client cannot assume a result fetched in one session is valid in the next. Every new session must re-fetch, even when the server's tool set is fixed at build time and never changes, which is the common case.

The Python SDK's own design issue for client-side list caching lists "what is the cache key — per-session or per-server-URL?" as an open question,3 and gateway implementers have shipped per-session caching specifically because they could not assume cross-session validity from the spec.4 Every list endpoint must be treated as potentially session-scoped, so each must be re-fetched per session to be safe.

For hosts that regularly spawn subagents, this is a multiplier on the hot path. The possibility that a server is session-scoped forces O(subagents × servers) calls to tools/list: every subagent, for every server, every time, even if the underlying tool set has not changed since the orchestrator first connected. The client cannot skip the call because it cannot know in advance which servers are session-scoped. For an orchestrator spawning many short-lived subagents, this overhead can exceed the protocol traffic of the actual tool calls. Under this proposal the same workload is O(servers): the orchestrator fetches each list once and every subagent reuses the cached result.

If list endpoints were a function only of the server deployment and the authenticated principal, clients could cache them and invalidate on an explicit signal. SEP-2549 specifies such a signal (a server-advertised TTL plus notifications/*/list_changed), but its caching model is only sound if the list does not also vary per session. Removing sessions makes that model safe; a subagent can then inherit its parent's cached lists at zero cost.

Cardinality is fixed at one per session

Session state has a cardinality of exactly one per session. The model gets one cart, one browser, one of whatever the server scopes to the session; it cannot have two, and it cannot have zero.

This is a problem when different pieces of state need different scopes. Consider an orchestrator that spawns several subagents to independently research products to buy. The subagents should add to the same shopping cart (they are collaborating on one order) but each needs its own browser state (they are browsing different sites in parallel).

No session boundary satisfies both:

Session model Cart (want: shared) Browser (want: isolated)
Subagents share parent's ✓ shared ✗ shared (clobbers)
Subagents get their own ✗ isolated ✓ isolated

With explicit IDs the orchestrator calls create_basket() once, passes the resulting basket_id to each subagent, and each subagent separately calls create_browser() for its own browser_id. The model decides what is shared and what is isolated per piece of state, rather than having one scope imposed on everything.

The same lack of an identifier also means session state is not addressable from outside the session that created it. A cart created in one chat is invisible to another chat; if a user wants to resume work in a new conversation, hand something off to a different agent, or share state with a colleague, the session model provides nothing to refer to it by. An explicit basket_id can be passed to any of those.

Specification

Summary of changes

  1. Remove the session concept from the protocol. The Mcp-Session-Id header is removed and the spec language describing session lifecycle and session-scoped behavior is deleted. The protocol is sessionless at every layer. (SEP-2575 removes the initialize handshake but explicitly defers session removal to this proposal.)

  2. List endpoints are session-independent. With no session, the results of tools/list, resources/list, and prompts/list have no per-session or per-connection scope to depend on. Lists can still change for other reasons (server deployment, auth changes); caching and invalidation mechanics for those are specified separately in SEP-2549.

  3. Stateful workflows use explicit handles. With sessions gone, servers that need to maintain state across tool calls do so by returning an identifier from a creation tool and accepting it as a parameter on subsequent calls.

That third point is not a protocol change. There is no handles/* method, no handle type in the schema, no wire-level concept of a handle at all. From the protocol's perspective a handle is a string in a tool result and a string in a tool argument, indistinguishable from any other tool data. "Explicit state handles" is a tool-design pattern that the spec documents and recommends — in the same way it might document pagination or error-message conventions — not something it implements. The normative content of this SEP is the removal in (1); (2) follows from it, and (3) is the guidance that fills the gap.

Explicit state handles

Pattern

Where a server would previously have relied on implicit session-scoped state — add_item calls operating on a per-session cart — it instead exposes an explicit creation tool that returns a handle:

// → tools/call
{ "name": "create_basket", "arguments": {} }

// ← result
{ "content": [{ "type": "text", "text": "Created basket bsk_a1b2c3" }],
  "structuredContent": { "basket_id": "bsk_a1b2c3" } }

The model then threads that handle through subsequent calls as an ordinary argument:

// → tools/call
{ "name": "add_item",
  "arguments": { "basket_id": "bsk_a1b2c3", "sku": "shoes" } }

// ← result
{ "content": [{ "type": "text", "text": "Added shoes to bsk_a1b2c3 (1 item)" }] }

// → tools/call
{ "name": "checkout",
  "arguments": { "basket_id": "bsk_a1b2c3" } }

Nothing here is a protocol extension: basket_id is an ordinary string field in structuredContent and an ordinary string argument to subsequent tools. This pattern is already the norm in widely-deployed remote MCP servers that manage durable resources:

Server (official, remote) Create tool → returned ID Operate tools taking that ID
Linear create_issue → issue id get_issue, update_issue, create_comment
Notion notion-create-pages → page id notion-update-page, notion-move-pages
GitHub create_pull_request → PR number pull_request_read, update_pull_request, merge_pull_request
Stripe create_customer → customer id create_invoice, list_subscriptions

The approach can be adopted for less-persistent objects (a browser context, an in-progress cart) by giving the created object a limited lifetime, and/or limiting its discoverability to the principal that created it. The server owns the state, the client holds a name for it, and authorization is checked on every call.

Guidance for servers

None of the following is normative. Handles are a tool-design pattern, not a protocol feature, and servers are free to shape them however fits their domain. The pattern works best when:

  • Handles are opaque. A handle that encodes internal structure (cart_user42_2026-03-11) invites clients to parse it or models to guess it; an opaque handle such as bsk_a1b2c3 does not.
  • Possession is not authorization (where auth exists). For authenticated servers, validate (handle, auth_context) on every call; handles will end up in chat logs, copy-paste buffers, and subagent prompts. For unauthenticated servers, where the handle is necessarily a bearer token, generate it with at least 128 bits of cryptographically secure entropy and bound its lifetime. See Security Implications.
  • Durability is documented in the tool description. Handles outlive connections by design, so "the state lasts until the connection closes" is no longer applicable. Put the policy in the create_* tool's description — "returns a basket_id; baskets expire after 24h idle" — so it is visible to the model when it decides to create state. A policy only in server documentation is not visible to the model.
  • Expired handles return useful errors. When a tool receives a handle for state that has expired or been destroyed, the error should say so — "basket bsk_a1b2c3 has expired" rather than "invalid argument". A clear expiry error lets the model recover by calling create_* again; an opaque error typically leads to retries or failure.
  • Creation takes parameters. create_context(cluster="staging") is preferable to create_context() followed by set_cluster(ctx, "staging"): one round-trip instead of two, and the state cannot exist half-configured.
  • Cleanup is available. A destroy_*(handle) tool lets models release resources. A list_*() tool lets a model recover after losing track of what it created. Neither is required.
Guidance for clients

From the client's perspective, a handle is an ordinary string in a tool result. The main client responsibility is ensuring that string survives context compaction; if the conversation is summarized and the handle is in the discarded portion, the state is orphaned. Clients that track tool-call results across compaction boundaries handle this already.

Session-independent list endpoints

With sessions removed, list endpoints no longer have a session to vary against. This is the only constraint this SEP places on tools/list, resources/list, and prompts/list: there is no longer a per-session or per-connection scope for their results to depend on. This does not preclude varying the list by the authorization presented on the request: credentials are carried on each request, so a server returning different tool sets to different principals or scopes is relying on per-request input, not connection state. Lists can also still change over time for other reasons — a server deploys a new version, a user's plan or granted scopes change — and this SEP does not enumerate or restrict those.

How clients learn that a cached list has gone stale is the subject of SEP-2549, which defines a server-advertised TTL on list responses and the interaction with notifications/*/list_changed. The two SEPs are complementary: this one removes the session as a source of variation, so there is a stable thing to cache; SEP-2549 specifies how long to cache it and when to invalidate.

One consequence of the constraint above is that servers can no longer mutate list results as a side effect of other requests; the pattern from the Motivation — where calling connect_database() makes query and list_tables appear in subsequent tools/list results — is no longer permitted. For the same effect, the server exposes query and list_tables unconditionally at list time and has them take a connection_id argument returned by connect_database(). A query call without a valid connection_id fails with an error directing the model to call connect_database() first; the dependency is expressed in the tool's input schema and description rather than in the list result.

Consequential spec edits

Beyond removing the §Session Management section itself, several other places in the current spec define behavior in terms of session scope and need re-scoping:

  • JSON-RPC request ID uniqueness. The spec currently requires that a request id "MUST NOT have been previously used by the requestor within the same session." The purpose of id is for the sender to correlate an incoming response with the request that produced it; the receiver only echoes it. With sessions removed, the constraint is re-scoped accordingly: a sender MUST NOT issue a request whose id matches that of another request it has sent and not yet received a response for. This is transport-agnostic, sufficient for correlation under every transport, and is what the TypeScript and Python SDKs already do via a monotonically increasing counter per client object. (JSON-RPC 2.0 §4 itself imposes no uniqueness requirement — it only requires the receiver to echo the id — so this remains an MCP-level constraint.)
  • SSE event ID uniqueness. The spec currently scopes SSE event IDs as "globally unique across all streams within that session." With sessions removed, the constraint is simply that the ID is globally unique across all streams the server manages, so that a Last-Event-ID resolves to a single stream. The existing guidance that event IDs encode the originating stream already implies this.
  • Pagination cursor validity. The spec currently advises clients not to "persist cursors across sessions." With sessions removed, this advice disappears. Cursor stability and snapshot consistency are outside the scope of this proposal.
  • List-endpoint variability. The tools/list, resources/list, and prompts/list pages each say results "MAY change over the lifetime of the connection." These are re-scoped per §Session-independent list endpoints: results MAY change over time but MUST NOT vary per-connection or as a side effect of other requests on the connection.
  • Wording. A handful of phrases that use "session" descriptively — "stateful session protocol" in the architecture overview, "available during the session" in capability negotiation, "same logical session" in authorization, the elicitation prohibition on associating state "with session IDs alone," and similar — are reworded or removed. These carry no semantic change beyond the session removal itself.

Rationale

Why remove sessions rather than just default them off?

SEP-2575 already addresses making MCP work behind load balancers and without sticky routing by removing the initialize handshake. The reasons for also removing sessions, rather than retaining them as an opt-in capability, are:

  • Opt-in sessions still prevent list caching. A client cannot cache tools/list across session boundaries unless it knows the server does not opt into session-scoped mutation, and it cannot know that in advance. The client therefore re-fetches per session per server even though few servers opt in. The O(subagents × servers) cost from the Motivation section is caused by sessions being possible, not by sessions being used, so making them optional does not remove it.
  • The primitive influences server design. Offering session-scoped state in the spec leads server authors to use it for workflows that would be better served by explicit IDs.
  • Fewer primitives reduce implementation surface. Every protocol concept must be implemented by SDK authors, documented, and learned by new users.

Expressiveness

A session provides exactly one scope per connection. Explicit IDs provide as many scopes as the model creates, and each can be shared or isolated independently. Anything expressible with a session is expressible with a single ID the model creates at the start of the conversation; the converse does not hold.

Resumption

Because handles appear in tool results, they are part of the chat transcript. Any client that persists its chats — which is most of them — therefore persists the handles automatically. Reopening a conversation after an app restart, a page reload, or on a different device puts the handle back in front of the model with no additional resumption machinery, and this behavior is consistent across clients. Session-based state, by contrast, requires the client to persist and resend Mcp-Session-Id out of band, which (as covered in the Motivation) almost no clients do.

Anticipated objections

Garbage collection

Sessions provide a lifecycle signal — when the session ends, state is freed. Without it, the model might forget to call destroy_basket(), and state leaks.

However, sessions do not deliver this reliably in practice. As covered in the Motivation, real clients either never end the session (per-app-launch), end it constantly (per-tool-call), or end it at moments uncorrelated with the conversation (page reload, network blip). Stateless HTTP servers behind load balancers never see a connection-close. Servers already rely on TTL-based expiry today; the session boundary is not what performs cleanup.

Explicit IDs with a documented durability policy ("baskets expire after 24h idle") is the same mechanism, made explicit.

Models have to carry the IDs forward

With implicit session state, the server tracks the identifier; with explicit IDs, the model is responsible for threading basket_abc123 through every relevant call. The failure modes are hallucinating a slightly-wrong ID, or the ID falling out of context when the conversation is compacted.

Models already carry opaque identifiers through conversations routinely — file paths, URLs, commit hashes, PR numbers, UUIDs returned from prior tool calls — and current models do this reliably. Compaction is the harder case, but it affects any long-horizon state: if the compactor drops live tool-call results, the model loses track of what is in the session-scoped cart as well, not just the cart's ID.

IDs in chat history

A basket_id that can be pasted anywhere could become an unauthenticated capability in the user's chat log.

For authenticated servers, the ID should be a name, with the server checking (id, auth_context) on every call. Google Doc IDs sit in URLs and browser history; access is controlled by ACL, not ID secrecy. The same applies here.

For servers without authentication, the ID is necessarily a bearer token — possession is the only thing the server can check. In that case the handle should follow standard practice for unguessable capability tokens: generated from a cryptographically secure random source with at least 128 bits of entropy (e.g. UUIDv4, or 22+ characters of URL-safe base64), never derived from predictable inputs, and given a bounded lifetime. This is the same posture as other ephemeral public IDs in common use — "anyone with the link" share URLs, password-reset tokens, Stripe Checkout session IDs — and carries the same tradeoff: convenient, but anyone who obtains the token has access for its lifetime.

Breaking change

Sessions are in the spec today; removing them breaks anyone relying on them.

An automated survey of a 1000-repo random sample of open source MCP servers (classified by per-repo LLM analysis) found:

Category Share Migration
No application-level reference to MCP session ID 90.0% None
Map<sessionId, Transport> routing (TS SDK boilerplate) 3.5% Removed by a sessionless SDK transport
Transport setup only (sessionIdGenerator, never read) 2.8% Delete one constructor option
Session-keyed application state 2.5% Migrate to explicit handles or auth principal
Proxy / gateway sticky routing 0.7% Needs designed replacement
Auth binding (JWT claims, PKCE verifier keyed on session) 0.5% Replace with server-generated nonce or token subject

The bolded rows are the repos that use the session ID for application semantics. The hardest-hit category — gateways that spawn one upstream per session — needs a designed replacement rather than a mechanical edit; see Backward Compatibility.

Backward Compatibility

This is a breaking change for servers that rely on protocol-level session state. The migration path depends on server category:

Stdio servers using process-lifetime state. These are the most common stateful servers today. Mechanically they are not broken by this proposal in their default deployment — the process lifetime still exists, and a server that keeps a single in-memory browser instance per process continues to function with a stdio client that spawns one process. However, such servers SHOULD NOT rely on process-lifetime state and SHOULD migrate to explicit handles. Process lifetime has the same undefined-scope problem this SEP removes for HTTP (whether the process corresponds to one conversation, one application launch, or something else is up to the host), and a server that depends on it cannot offer equivalent behavior over HTTP, where there is no process per client. Stdio servers never had Mcp-Session-Id, so the header removal itself does not affect them.

HTTP servers using Mcp-Session-Id. These are less common and must migrate to explicit handles. The migration is mechanical: replace the session-scoped state map with a handle-keyed state map, add a create_* tool, add the handle as a parameter to stateful tools.

Servers using session ID as a telemetry key. Some servers tag traces, logs, or rate-limit buckets with the session ID to correlate activity within a session. This already worked inconsistently across clients — against per-tool-call clients every event lands in its own bucket, and against clients that don't resume the correlation breaks at every restart. These use cases need to move to a different scoping mechanism, typically the authenticated principal (bearer token subject, API key) or a request-level correlation ID.

Proxies and gateways using session ID for sticky routing. Gateways that route by Mcp-Session-Id lose their routing key — but they only needed one because their upstreams were stateful. If the upstream is stateless (or migrates to explicit handles, where the state key is in the tool arguments and any replica can serve it from shared storage), the gateway needs no sticky routing at all. The residual case is gateways that bridge HTTP to stdio by spawning one subprocess per session; those need a different correlation key, which is a transport-layer concern (route by authenticated principal, or a cookie / gateway-issued header) rather than something this SEP defines.

Servers binding auth artifacts to session ID. A small number of servers store OAuth PKCE verifiers, session→user pinning maps, or JWT claims keyed on the session ID. In the PKCE case the server is already passing a correlation value through the OAuth state parameter (the browser callback is not an MCP request and never carried Mcp-Session-Id), so the change is to put a server-generated nonce in state instead of the session ID. Session→user pinning was a defense against the session-routing/auth decoupling described in Security Implications and is not needed once every request is independently authenticated. The migration is mostly mechanical, though worth a review since auth code is involved.

Clients. Clients become simpler: they no longer track or resend session identifiers, or need to determine whether a given server is stateful. List-endpoint caching becomes safe.

Rollout is a clean break: sessions are removed in the next spec version, with no deprecation window. Servers that currently rely on session-scoped state stay on the current protocol version until they have migrated to explicit handles. Protocol version negotiation already handles mixed-version deployments — a client that supports both versions speaks the old protocol to an unmigrated server and the new one to everyone else. This avoids shipping a version where clients support both modes simultaneously, which would prevent the caching benefit (a client cannot cache list endpoints if any connected server might be session-scoped).

Security Implications

Handle exposure

The main security consideration introduced by this SEP is that handles will end up in places session IDs did not — chat logs, subagent prompts, copy-paste buffers, potentially other users' screens.

This is a change in exposure surface, not a new class of vulnerability. Session IDs are already capability-bearing in practice: the Python SDK's stateful session manager, for example, routes by Mcp-Session-Id alone without verifying that the authenticated identity on the request matches the one that created the session, so a leaked session ID allows hijack by any other authenticated principal.5 The "validate (id, auth_context) on every call" guidance below applies equally to today's session IDs and to explicit handles; this SEP makes the requirement more visible because handles are more visible.

For authenticated servers, the recommended posture is the same one Google Doc IDs and GitHub PR numbers take: the ID identifies the resource, and the auth context on the request determines access. Servers that validate (handle, auth_context) on every call are unaffected by handle exposure.

For unauthenticated servers there is no auth context to check, so the handle is a capability token. These should be generated with at least 128 bits of cryptographically secure entropy, never derived from predictable inputs, and given a bounded lifetime — the same practice as "anyone with the link" share URLs or password-reset tokens. Exposure of such a handle grants access for its lifetime; servers should size that lifetime accordingly.

This is guidance, not a protocol requirement, since the protocol has no handle concept to enforce against.

Reference Implementation

All official SDKs except PHP already provide a stateless mode, implemented as not generating a session ID (e.g. sessionIdGenerator: undefined in the TypeScript SDK, stateless_http=True in the Python SDK). This SEP makes that mode the only option for servers speaking the new protocol version. SDKs that support multiple protocol versions retain the session-ID-generating code path for older versions; the change is that it is no longer reachable when the negotiated protocol version is the one this SEP introduces.

Future Work

This SEP deliberately does not introduce a protocol-level concept of a handle: from the wire's perspective basket_id is an ordinary string. A consequence is that nothing marks basket_id as a state handle to the client or model — the relationship between create_basket's output and add_item's input is inferred from naming and tool descriptions, not declared.

A follow-up proposal could make that relationship explicit, for example via shared JSON Schema $defs referenced across a server's tool input and output schemas, or via a tool annotation that marks a result field as a handle. That would let orchestrators identify which values are live state (for compaction, hand-off, or cleanup purposes) without parsing tool descriptions. It is left out of scope here to keep this SEP to the minimum needed to remove sessions.

Footnotes

  1. microsoft/playwright-mcp#1045, Sep 2025 — server author reports both ChatGPT and Claude.ai closing the session after each tool call, dropping browser state; "Connector tool calls generating fresh MCP session each invocation", OpenAI Developer Community, Nov 2025. ↩

  2. modelcontextprotocol/typescript-sdk#1658, Mar 2026 — StreamableHTTPServerTransport stores session state in private instance fields with no API to rehydrate from external storage. ↩

  3. modelcontextprotocol/python-sdk#2108, Feb 2026. ↩

  4. agentgateway/agentgateway#1510, Apr 2026. ↩

  5. modelcontextprotocol/python-sdk#2100. ↩

SEP-2575 Make MCP Stateless

Final · Standards Track · Created 2025-06-18

Abstract

A truly stateless protocol, where every request is self-contained and can be understood in isolation, is highly desirable for its inherent simplicity, scalability, and reliability. The current Model Context Protocol (MCP) is not stateless by default. The specification requires an initialization handshake that establishes a session state between the client and server, which persists for the duration of the connection.

This inherent statefulness makes it difficult to run MCP at scale. Placing an MCP server behind a standard load balancer, for example, is challenging because a client's session is coupled to the specific server instance holding its state.

This proposal outlines a series of changes to enable stateless MCP as the default, embracing a "pay as you go" model for protocol complexity and state. Under this model, we provide simple, stateless features by default and only introduce the overhead of stateful, long-lived connections for cases where that functionality is actually required.

Specifically, this SEP proposes removing the state-establishing initialization handshake and replacing it with discrete, stateless alternatives. This initial step allows each request to be processed independently, simplifying server-side logic and paving the way for robust, scalable deployments.

Motivation

The Model Context Protocol (MCP) specification currently mandates a stateful initialization handshake. This design choice creates significant challenges for scalability, reliability, and implementation simplicity. This SEP is motivated by the need to address these shortcomings.

The Problem with Statefulness

The core issue is that a server must retain session state from previous requests to understand subsequent ones. This is in direct opposition to the design of modern, cloud-native systems which favor stateless services for their resilience and scalability.

  1. Impediment to Scalability: The most critical issue is the difficulty of load balancing stateful MCP. A simple stateless load balancer (e.g., L4/L7 round-robin) cannot be used, as it would route a client's requests to different backend servers, none of which would have the correct session state. Operators are forced to implement complex and fragile solutions like sticky sessions, which bind a client to a specific server. This complicates infrastructure, can lead to uneven load distribution, and makes horizontally scaling the service non-trivial.
  2. Poor Resilience and Fault Tolerance: In a stateful model, if the specific server instance handling a client session fails, that session state is lost. The client must detect the connection failure, re-establish a connection (likely to a new server instance via the load balancer), and perform the entire initialization handshake again. This process is disruptive and inefficient, adding complexity around "resumability".
  3. Increased Implementation Complexity: The current model imposes a significant burden on developers.
    • Server-side: Developers must implement logic to create, manage, and eventually garbage-collect per-client session state. This is a common source of bugs and memory leaks.
    • Client-side: Developers must write complex code to manage a persistent connection and handle the inevitable network failures and reconnections, including the logic to resynchronize state after a disconnect.

Design Principles

This proposal establishes a "pay as you go" model for protocol complexity, guided by the following principles in order of preference:

  1. Prioritize Stateless-ness: Whenever possible, a request must be self-contained, providing all information the server needs to process it without relying on state from previous requests.
  2. Prefer State References: If a fully stateless exchange is not practical, references to state should be passed in every request.
  3. Treat Statefulness as a Last Resort: The complexity of stateful logic and long-lived streaming connections should only be accepted when no simpler alternative exists to solve a critical use case.

Transport Consistency

It is critical that these stateless principles are applied consistently across all transports. Keeping the stdio and http implementations in sync ensures a unified developer experience, allowing the core protocol semantics to be learned once and applied everywhere. This consistency simplifies the creation of transport-agnostic libraries and tooling, and prevents protocol fragmentation where different transports behave in fundamentally different ways. A single, coherent protocol model is essential for a healthy ecosystem.

Specification

Overview

This specification fundamentally refactors the MCP interaction model to be stateless-first. Currently, MCP requires a mandatory 3-way initialization handshake before any resources can be exchanged. This handshake negotiates and establishes several key pieces of information:

  1. MCP Protocol Version
  2. Server Capabilities and serverInfo
  3. Client Capabilities and clientInfo

The requirement of this initialization handshake enforces the establishment of a state that is expected to persist for subsequent communication between client and server. Furthermore, by bundling these negotiations into a single initialization phase, the specification creates an implied link between them, particularly between the exchange of capabilities and a mandatory connection lifecycle.

This proposal is to remove the initialization handshake and "unbundle" its functions into discrete, stateless components. We will provide new, more clearly defined mechanisms for clients and servers to exchange this information without a mandatory state-creating cycle.

Note: Session management (both transport-level and application-level) is addressed separately by SEP-2322 and SEP-2567. This SEP focuses exclusively on removing the initialization handshake and providing stateless alternatives for version negotiation, discovery, and capabilities.

Protocol Version

To make requests self-contained, metadata previously negotiated during the handshake must now be included with every request.

HTTP

For the HTTP transport, protocol version MUST be passed as an HTTP header. The header value MUST match the value provided in the request payload's _meta field; otherwise the server MUST return a 400 Bad Request (see SEP-2243).

  • MCP-Protocol-Version: 2025-06-18
    • Purpose: To inform the server which version of the MCP specification the client is using for this specific request.
    • Requirement: This header is MANDATORY. Servers should reject requests with a missing or unsupported version.
    • This header MUST match the value provided in the Request as specified below.
Per-request Version

The protocol-version MUST be embedded directly within the _meta field of the request payload. For HTTP, this _meta MUST match the associated HTTP header, or else the server should return a 400 Bad Request.

The following diff illustrates the required changes to RequestMetaObject:

export interface RequestMetaObject extends MetaObject {
  progressToken?: ProgressToken;
+ /**
+  * The MCP Protocol Version being used for this request.
+  */
+ "io.modelcontextprotocol/protocolVersion": string;
  // Additional per-request fields (clientInfo, clientCapabilities, logLevel)
  // are introduced in the Per-Request Client Capabilities section below.
}
Unsupported Protocol Versions

If a server receives a request with a protocol version it does not implement (whether the version is unknown to the server or is a known version the server has chosen not to support, such as an experimental or draft version), it MUST return a JSON-RPC error response. For HTTP, the response status code MUST be 400 Bad Request. The error MUST conform to the following structure:

export const UNSUPPORTED_PROTOCOL_VERSION = -32022;

export interface UnsupportedProtocolVersionError extends Omit<
  JSONRPCErrorResponse,
  "error"
> {
  error: Error & {
    code: typeof UNSUPPORTED_PROTOCOL_VERSION;
    data: {
      /**
       * An array of protocol version strings that the server supports.
       */
      supported: string[];
      /**
       * The protocol version that was requested by the client.
       */
      requested: string;
    };
  };
}
Version Negotiation Flow

Without an initialization handshake, version negotiation happens inline:

  1. The client sends a request with its preferred protocol version in the MCP-Protocol-Version header and io.modelcontextprotocol/protocolVersion _meta field.
  2. If the server supports that version, it processes the request normally.
  3. If the server does not support the requested version, it returns an UnsupportedProtocolVersionError containing its list of supported versions.
  4. The client selects a mutually supported version from the list and retries.

Alternatively, a client MAY call server/discover first to learn the server's supported versions before sending any other requests.

Discovery for Server Capabilities

To allow clients to adapt to different server implementations, this specification introduces a discovery RPC. This provides a standard mechanism for a server to advertise its supported protocol versions and capabilities.

Servers MUST implement server/discover. Clients MAY call it but are not required to — a client is free to invoke any RPC without first calling the discovery endpoint. If a client calls an unsupported RPC, the server MUST return a Method not found JSON-RPC error (-32601). For HTTP, the response status code MUST be 404 Not Found.

server/discover RPC
  • Purpose: To allow a client to query the server for its supported protocol versions, capabilities, and other metadata.

Request Schema:

export interface DiscoverRequest extends Request {
  method: "server/discover";
  params?: {};
}

Response Schema:

export interface DiscoverResult extends Result {
  /**
   * A list of MCP Protocol Version strings that this server supports.
   * The client should choose a version from this list for use in
   * subsequent requests.
   */
  supportedVersions: string[];

  /**
   * An object detailing the capabilities of the server.
   */
  capabilities: ServerCapabilities;

  /**
   * Information about the server software implementation.
   */
  serverInfo: Implementation;

  /**
   * Natural language instructions describing how to use the server and
   * its features. This can be used by clients to improve an LLM's
   * understanding of available tools (e.g., by including it in a system prompt).
   */
  instructions?: string;
}

Per-Request Client Capabilities

To complete the decoupling from the initial handshake, client capabilities are no longer negotiated once at initialization. Instead, a client MUST specify its capabilities on every request. This ensures the server is always fully informed about what optional features the client can handle for that specific transaction. An empty capabilities object means the client supports no optional capabilities — servers MUST NOT infer capabilities from prior requests.

Per-Request Metadata Schema

Every request's _meta carries a small set of fields that previously lived in the initialization handshake. The full RequestMetaObject shape:

export interface RequestMetaObject extends MetaObject {
  progressToken?: ProgressToken;
  /**
   * The MCP Protocol Version being used for this request.
   */
  "io.modelcontextprotocol/protocolVersion": string;
  /**
   * Identifies the client software.
   */
  "io.modelcontextprotocol/clientInfo": Implementation;
  /**
   * Capabilities of the client for this specific request.
   */
  "io.modelcontextprotocol/clientCapabilities": ClientCapabilities;
  /**
   * The desired log level for this request.
   */
  "io.modelcontextprotocol/logLevel"?: LoggingLevel;
}

Field semantics:

  • "io.modelcontextprotocol/protocolVersion": string — the MCP Protocol Version. Required. See the Protocol Version section above for negotiation details.
  • "io.modelcontextprotocol/clientInfo": Implementation — identifies the client software. Required. The Implementation schema requires name and version; other fields are optional.
  • "io.modelcontextprotocol/clientCapabilities": ClientCapabilities — the client's capabilities for this request. Required.
  • "io.modelcontextprotocol/logLevel": LoggingLevel — the desired log level for this request. Optional. If absent, the server MUST NOT send any log notifications for this request. The client opts in to log messages by explicitly setting a level. Replaces the logging/setLevel RPC.

Roots are intentionally not included as a per-request _meta field. Servers that need the client's roots MUST request them via the MRTR ListRootsRequest mechanism (see SEP-2322), which avoids putting potentially large root lists on every request and follows the "pay as you go" principle.

A request missing any required field is malformed; the server MUST reject it with INVALID_PARAMS (and 400 Bad Request for HTTP).

Response Streaming

These declared capabilities govern what the server may include in the response stream. SEP-2322 (MRTR) defines how server-to-client interactions are embedded inline within responses via IncompleteResult; this SEP specifies that those interactions are governed by the per-request clientCapabilities declared in RequestMetaObject.

For HTTP, any request's response MAY be delivered as an SSE stream (Content-Type: text/event-stream) instead of a single JSON object. Only notifications (e.g., notifications/progress, notifications/message) flow as independent messages on this stream, followed by the final result. Server-to-client interactions (sampling, elicitation, listRoots) are not sent as independent requests — they are embedded as input requests inside an IncompleteResult returned from specific request paths (e.g., CallTool, GetPrompt, ListResources). The client satisfies the input requests and retries the original request.

Request Cancellation

How a client cancels an in-flight request depends on the transport:

  • HTTP. Closing the SSE response stream MUST be treated by the server as cancellation of that request. Because each request has its own response stream, the transport-level disconnect is unambiguous.
  • STDIO. The client MUST send a notifications/cancelled notification referencing the request ID. STDIO has a single shared channel, so there is no per-request stream to close.

Servers SHOULD stop work on a cancelled request as soon as practical and MUST NOT send any further messages for it.

Resumable Streams Are Removed

Because connection drops now implicitly cancel a request, resumable SSE streams (via Last-Event-ID reconnection) are removed. They contradict the stateless-by-default paradigm: resuming would require the server to retain per-request state across connection failures.

Workloads that need durability or resumability MUST use the tasks primitive instead, which provides explicit mechanisms for fetching results after a connection drop.

Missing Required Capabilities

A server MUST NOT rely on capabilities the client has not declared. If processing a request requires a capability the client did not declare in its clientCapabilities, the server MUST return a JSON-RPC error specifying the missing capabilities. For HTTP, the response status code MUST be 400 Bad Request.

export const MISSING_REQUIRED_CLIENT_CAPABILITY = -32021;

export interface MissingRequiredClientCapabilityError extends Omit<
  JSONRPCErrorResponse,
  "error"
> {
  error: Error & {
    code: typeof MISSING_REQUIRED_CLIENT_CAPABILITY;
    data: {
      /**
       * The capabilities the server requires from the client
       * to process this request.
       */
      requiredCapabilities: ClientCapabilities;
    };
  };
}

subscriptions/listen RPC

This SEP introduces a new subscriptions/listen RPC that replaces the previous HTTP GET endpoint and ensures consistent behavior between HTTP and STDIO. A client uses it to open a long-lived channel for receiving notifications outside the context of a specific request.

The HTTP GET endpoint used by Streamable HTTP for server-to-client messages is removed in this version of the protocol. All communication uses POST.

Per SEP-2260, only notifications (not requests) flow on this channel; server-initiated requests use MRTR (see Response Streaming above) and are scoped to a specific client request.

Request Schema
export interface SubscriptionsListenRequest extends Request {
  method: "subscriptions/listen";
  params: {
    _meta: {
      "io.modelcontextprotocol/protocolVersion": string;
      "io.modelcontextprotocol/clientInfo": Implementation;
      "io.modelcontextprotocol/clientCapabilities": ClientCapabilities;
      // ... other meta fields
    };

    /**
     * The notifications the client wants to receive on this stream.
     * Each notification type is opt-in; the server **MUST NOT** send
     * notification types the client has not explicitly requested here.
     */
    notifications: {
      /**
       * If true, receive notifications/tools/list_changed.
       */
      toolsListChanged?: boolean;

      /**
       * If true, receive notifications/prompts/list_changed.
       */
      promptsListChanged?: boolean;

      /**
       * If true, receive notifications/resources/list_changed.
       */
      resourcesListChanged?: boolean;

      /**
       * Subscribe to notifications/resources/updated for specific
       * resource URIs. Replaces the resources/subscribe RPC.
       */
      resourceSubscriptions?: string[];
    };
  };
}

The notifications field is required and the client MUST explicitly opt in to each notification type it wants to receive. If a field within notifications is omitted (or set to false), the server MUST NOT send notifications of that type.

Acknowledgment Notification

The server sends this notification first to acknowledge that the subscription has been established. The subscription is long-lived and has no natural "completion result"; it ends when:

  • the client explicitly cancels it (closing the SSE stream on HTTP, or sending notifications/cancelled on STDIO);
  • the underlying connection is closed (HTTP timeout, TCP disconnect, STDIO process exit); or
  • the server tears it down (e.g., shutdown), in which case it MUST close the SSE stream (HTTP) or send notifications/cancelled referencing the subscription's request ID (STDIO).
export interface SubscriptionsAcknowledgedNotification extends Notification {
  method: "notifications/subscriptions/acknowledged";
  params: {
    /**
     * The notification subscriptions the server has agreed to honor.
     * Only includes notification types the server actually supports.
     * If the client requested an unsupported notification type
     * (e.g., promptsListChanged when the server has no prompts),
     * it is omitted from this set.
     */
    notifications: {
      toolsListChanged?: boolean;
      promptsListChanged?: boolean;
      resourcesListChanged?: boolean;
      resourceSubscriptions?: string[];
    };
  };
}
Multiple Concurrent Subscriptions

A client MAY have multiple active subscriptions concurrently (e.g., one listening for tools-list changes, another for resource updates). Each subscription is identified by the JSON-RPC request ID of its SubscriptionsListenRequest.

To allow STDIO clients to demultiplex notifications belonging to different subscriptions on the single shared channel, every notification delivered as part of an active subscription MUST include the subscription's request ID in _meta:

{
  "jsonrpc": "2.0",
  "method": "notifications/tools/list_changed",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/subscriptionId": "<original listen request id>"
    }
  }
}

This same correlation pattern applies to other server-to-client notifications that need to be associated with a specific request, such as notifications/progress (which uses the originating request's ID).

Stopping a Subscription
  • HTTP. Closing the SSE response stream stops the subscription.
  • STDIO. The client sends notifications/cancelled referencing the listen request's ID. The server MUST stop sending notifications for that subscription.
Transport Behavior

HTTP. The client sends SubscriptionsListenRequest via POST. The server's response is an open SSE stream (Content-Type: text/event-stream), and the first JSON-RPC message on this stream MUST be a SubscriptionsAcknowledgedNotification.

STDIO. The client sends SubscriptionsListenRequest at any time. The server MUST acknowledge it by sending a SubscriptionsAcknowledgedNotification. Subsequent notifications flow on the bidirectional STDIO channel, each tagged with the subscription's request ID as described above. If the connection is terminated (e.g., the server crashes and restarts), the client MUST re-send SubscriptionsListenRequest to re-establish its subscriptions.

Deprecated and Removed RPCs

To simplify the protocol and align with the move to per-request capabilities, the following RPC methods and notifications are removed:

  • initialize / notifications/initialized: The initialization handshake is removed. Version negotiation is handled per-request via MCP-Protocol-Version headers and _meta fields. Capability discovery is handled by server/discover.
  • logging/setLevel: Removed. The log level is now specified per-request via the 'io.modelcontextprotocol/logLevel' _meta field. There is no replacement RPC.
  • roots/list: Removed as a top-level server-to-client RPC. Servers that need the client's roots MUST request them via the MRTR ListRootsRequest mechanism (see SEP-2322).
  • notifications/roots/list_changed: Removed. Roots are fetched on demand via MRTR, so there is no need for a change notification.
  • resources/subscribe / resources/unsubscribe: These methods are removed. Resource subscriptions are inherently stateful — the server must remember which resources each client has subscribed to. Instead, clients declare the resources they want updates for in the notifications param of the subscriptions/listen request. The server sends notifications/resources/updated on the listen stream for matching resources.
  • ping: Removed in both directions. Server-to-client ping is removed because servers can no longer independently send requests. Client-to-server ping is also removed because any normal RPC call already proves server liveness, and transport-layer mechanisms (HTTP keep-alives, SSE comments, STDIO process status) handle connection-health checks more appropriately.

Rationale

Stateless-First by Default

The primary design decision of this SEP is to remove the mandatory initialization handshake, making stateless interaction the default model for the protocol. This choice is rooted in the "pay as you go" principle and the desire to align MCP with modern, cloud-native architecture. By making the simplest interaction model the default, we lower the barrier to entry and reduce implementation complexity for the most common use cases. This immediately enables straightforward horizontal scaling and improves resilience, as any request can be handled by any server instance.

Alternative Considered: Optional Handshake

An alternative we considered was to keep the existing stateful handshake but make it optional. In this model, a client could choose to either perform the handshake to establish a persistent session or skip it and send self-contained requests.

Why it was rejected:

Supporting two parallel interaction models would have dramatically increased the complexity of the protocol and every implementation. Servers and clients would need to build, test, and maintain two separate logic paths, leading to a larger surface area for bugs. It also violates the design principle of having one clear, obvious way to perform a core function. By making a clean break, we ensure the entire ecosystem can move forward and benefit from a simpler, more scalable, and more robust foundation.

Explicit Session Management

This proposal originally included dedicated sessions/create and sessions/delete RPCs to manage the lifecycle of a logical session.

Session management is now addressed separately by SEP-2567, which proposes removing sessions entirely and replacing them with explicit state handles. This aligns with the sessions-vs-sessionless decision made by the Core Maintainers.

Separation of Concerns

A core principle of this proposal is the "unbundling" of the monolithic initialization handshake into a suite of discrete, single-purpose RPCs. The original handshake mixed the concerns of protocol negotiation and capability discovery into a single, complex interaction. The new design explicitly separates these:

  • Discovery: Handled exclusively by server/discover.
  • Capabilities: Handled on a per-request basis via the _meta field or the subscriptions/listen RPC.

The rationale for this is to create a more modular, flexible, and understandable protocol. Each component now has a single, well-defined responsibility. This allows clients to use only the parts of the protocol they need, adhering to our "pay as you go" principle.

Alternative Considered: A Monolithic Handshake

We could have kept a single, monolithic handshake RPC and simply added more parameters and complex logic to it to support the stateless-first model.

Why it was rejected:

A single, do-it-all RPC is difficult to implement, test, and evolve. It forces all clients, even the simplest ones, to be aware of the protocol's most complex features. By separating these concerns, we've made the protocol easier to learn and implement correctly, while also making it more flexible and extensible for the future.

Backward Compatibility

While this proposal attempts to preserve existing functionality and use-cases, this proposal introduces a fundamental, backward-incompatible change. Thus, it will require a new version of the protocol.

Supporting Multiple Versions

While this SEP removes the initialize handshake, a server that wishes to support both old and new clients MAY do so. Such a server can continue to implement the old initialize RPC to handle legacy clients, while also exposing the new stateless RPCs (server/discover, etc.) for updated clients.

Both servers and clients should be able to handle changes in the versions appropriately. Two example scenarios are outlined below, where vPrev indicates the version prior to the SEP, and vAfter indicates a version after it.

Client (supporting vPrev) → Server (vPrev, vPost)
  1. Client sends initialization
  2. Server supports vPrev, so initialization is returned per spec
  3. Client and server communicate per vPrev.
Client (supporting vPrev, vPost) → Server (vPrev)

For HTTP, the client may attempt any vPost request (e.g., tools/list with the MCP Protocol Version header). The server returns 400 Bad Request (or Unsupported protocol version); the client falls back to vPrev (and performs initialization) for future requests.

For STDIO, the client cannot rely on a per-request error to detect the server's version. A client that supports both a vPost (which does not require initialization) and a legacy version that does require initialize SHOULD probe with server/discover first to determine which to use:

  1. Client sends server/discover with the MCP Protocol Version _meta field set to its preferred vPost.
  2. If the server supports vPost (or any vPost-style version the client also supports), the client uses the discovered version for subsequent requests.
  3. If the server returns Unsupported protocol version or Method not found, the client falls back to its supported legacy version and performs the initialize handshake.

A client that supports only vPost-style versions has no need to probe — it simply uses its preferred version and handles Unsupported protocol version errors normally.

Security Implications

Without a session handshake, every request must be independently authenticated and authorized. Implementations MUST ensure that authentication is not bypassed by the removal of the initialization phase.

Beyond per-request authentication, this proposal does not introduce additional security concerns.

Reference Implementation

// TODO

FAQ

What is protocol level statelessness?

Wikipedia defines a stateless protocol as:

A stateless protocol is a communication protocol in which the receiver must not retain session state from previous requests. The sender transfers relevant session state to the receiver in such a way that every request can be understood in isolation, that is without reference to session state from previous requests retained by the receiver.

This does NOT mean that you can't build stateful applications on top of a stateless protocol. HTTP is an example of a stateless protocol, which most of the web is built on today. However it does mean that the state cannot exist in the protocol itself, and should instead specify the state in the request (or failing that, a reference to the state for the server or client to track).

Does this make MCP a fully stateless protocol?

Not entirely (hence 'by default'). Depending on your interpretation of "requests", the SSE streams mentioned (both client-initiated and server-initiated) tend to have multiple requests within a context of a stream. However, these streams are constrained to a single HTTP request and optional to use, meaning that the complexity is both constrained and optional to use when the situation requires it.

Why is it important for STDIO to be stateless as well?

The transport MCP is using should be an implementation detail only. If one version of a protocol supports functionality that doesn't cleanly map over to another version of the protocol, they are really two different protocols.

This makes it easy for developers to switch their services from one transport to another without needing to make significant changes to the behavior of their applications, and easier to proxy between different transports correctly. Otherwise, there will continue to be feature gaps and division between these different implementations, leading to both confusion and incompatibility.

How does server/discover relate to the MCP Server Card?

The server/discover RPC overlaps with the MCP Server Card proposal, which defines a .well-known/mcp.json document for HTTP-based discovery. Both mechanisms are intentionally retained: the Server Card is well-suited to HTTP (no auth required, cacheable, indexable) while server/discover provides a unified RPC interface that works consistently across HTTP and STDIO transports. The two should be aligned on content where applicable.

Open Questions

What belongs in _meta vs. as a top-level protocol field?

This SEP places several previously-handshake-negotiated values (protocolVersion, clientInfo, roots, logLevel, clientCapabilities) into per-request _meta fields under the io.modelcontextprotocol/ namespace. This follows the spec's allowance for "purpose-specific metadata" reserved by definitions in the schema.

However, this risks overloading _meta over time — at what point do we add top-level fields again? One possible distinction: required protocol-level fields (e.g., protocolVersion) might better live as top-level fields, while optional or extension-provided values stay in _meta. This question deserves broader discussion before this SEP is finalized.

Should clientInfo be part of ClientCapabilities?

Currently, clientInfo (Implementation type) and clientCapabilities (ClientCapabilities type) are separate fields. In a per-request model, having a single field for all client metadata would reduce overhead. However, clientInfo serves a different purpose (identity/UI) than capabilities (feature negotiation). Should clientInfo be folded into ClientCapabilities, remain a separate per-request _meta field, or be handled through a different mechanism entirely (e.g., only sent via subscriptions/listen)?

Changes since SEP became Final

This SEP is preserved as a historical record of what was accepted. The list below tracks changes made to the specification after this SEP reached Final status. Refer to the current specification for the authoritative, up-to-date requirements.

  • Client identity became optional request metadata. #3002 made io.modelcontextprotocol/clientInfo optional. Clients SHOULD include it on every request unless specifically configured not to do so.
  • Server identity moved to optional result metadata. #3002 introduced io.modelcontextprotocol/serverInfo in result _meta and removed the top-level DiscoverResult.serverInfo field to avoid duplicate representations. Servers SHOULD include this metadata on every result unless specifically configured not to do so.
  • Subscriptions gained a graceful completion result. #2953 defined a subscriptions/listen result for server-initiated graceful closure, replacing the SEP's statement that a subscription has no natural completion result. Servers SHOULD send this result before closing the stream.

SEP-2577 Deprecate Roots, Sampling, and Logging

Final · Standards Track · Created 2026-04-14

  • Status: Final
  • Type: Standards Track
  • Created: 2026-04-14
  • Author(s): Kurtis Van Gent (@kurtisvg)
  • Sponsor: @kurtisvg
  • PR: #2577

Note: This SEP is predicated on a hypothetical SEP where MCP considers a specification version supported for one year past its original release date. The deprecation timeline described here assumes that policy is in place.

Abstract

This SEP deprecates the following core protocol features:

  • Roots (roots/list, notifications/roots/list_changed)
  • Sampling (sampling/createMessage, ClientCapabilities.tasks.requests.sampling)
  • Logging (logging/setLevel, notifications/message)

These features are deprecated starting in the specification version that includes this SEP (expected June 2026). They will continue to be fully functional in all specification versions released within one year of that version's release.

Each of those subsequent versions will in turn support the features for one year after its own release, assuming the one-year-per-version support policy proposed in a separate SEP. This provides implementations with an extended migration window before the features are fully removed.

During the deprecation period, wire-level behavior is unchanged. No types are removed, no capability negotiation changes, and no existing implementations break. The deprecation serves as a signal to the ecosystem to stop building on these features and to plan for their eventual removal.

Motivation

The MCP specification aims to remain minimal and focused. Features that see low adoption, overlap with existing alternatives, or impose disproportionate implementation burden relative to their value are candidates for removal. Keeping such features in the core specification increases the burden for every client and server, slows protocol evolution, and makes the specification harder to learn. The following three features meet these criteria.

Deprecating these features was proposed during a recent core contributor meeting. This SEP formalizes that proposal with a concrete implementation plan. See discussion #2536.

Roots

Roots provides "informational guidance" about which directories or files a server should operate on. In practice:

  • Low adoption: Few clients implement roots support, and few servers rely on it. The feature support matrix shows limited client coverage.
  • Vague semantics: The specification describes roots as informational — servers are not required to respect them, which reduces their utility.
  • Overlapping alternatives: Working directory context can be provided through tool parameters, resource URIs, server configuration, or environment variables — all of which are more explicit.

Sampling

Sampling allows servers to request LLM completions from the client. While conceptually powerful, it has struggled with adoption:

  • Complex to implement: Correct sampling implementation requires human-in-the-loop approval, model selection logic, security considerations, and (since SEP-1577) tool loop support. This complexity has contributed to low client adoption.
  • Low adoption: The feature support matrix shows that few clients support sampling, despite the feature being available since the November 2024 specification.
  • Direct alternatives: Servers that need LLM capabilities can integrate directly with LLM provider APIs, giving them full control over model selection, parameters, and streaming.

Logging

Logging allows servers to send structured log messages to clients via the protocol:

  • Overlapping infrastructure: Standard logging mechanisms (stderr for stdio transports, OpenTelemetry for structured observability) are mature, widely adopted, and better suited to logging than an application-protocol channel.
  • Low value relative to complexity: Adding log message types, severity levels, and the logging/setLevel request to the core specification increases the implementation surface for all clients and servers.

Specification

Overview of changes

  1. Mark deprecated features with @deprecated annotations in the schema
  2. Add deprecation notices to feature documentation pages
  3. No wire-level protocol changes during the deprecation period

Schema changes

Add @deprecated JSDoc annotations to the following items in schema/draft/schema.ts. No types, interfaces, or union members are removed.

Deprecated capabilities
Capability Location
ClientCapabilities.roots Client capability for listing roots
ClientCapabilities.sampling Client capability for LLM sampling
ClientCapabilities.tasks.requests.sampling Task-augmented sampling sub-capability
ServerCapabilities.logging Server capability for log messages
Deprecated types — Roots
Type Description
Root Represents a root directory or file
ListRootsRequest Server-to-client request for roots/list
ListRootsResult Result containing roots array
ListRootsResultResponse JSON-RPC response wrapper
RootsListChangedNotification Client notification when roots change
Deprecated types — Sampling
Type Description
CreateMessageRequestParams Parameters for sampling/createMessage
CreateMessageRequest Server-to-client request for sampling
CreateMessageResult Result from a sampling request
CreateMessageResultResponse JSON-RPC response wrapper
SamplingMessage A message in a sampling conversation
SamplingMessageContentBlock Content block union for sampling messages
ToolChoice Controls model tool selection during sampling
ToolUseContent Tool use content block in sampling messages
ToolResultContent Tool result content block in sampling messages
ModelPreferences Server preferences for model selection
ModelHint Hints for model selection
Deprecated types — Logging
Type Description
LoggingLevel Syslog severity level enum
SetLevelRequestParams Parameters for logging/setLevel
SetLevelRequest Client-to-server request to set level
SetLevelResultResponse JSON-RPC response wrapper
LoggingMessageNotificationParams Parameters for log message notification
LoggingMessageNotification Server-to-client log message
Annotation format

Each deprecated item SHOULD receive a JSDoc @deprecated tag with a brief explanation:

/**
 * Present if the client supports listing roots.
 *
 * @deprecated Deprecated as of this specification version. Will be included
 * in all versions released within one year, then may be removed.
 */
roots?: {
  listChanged?: boolean;
};
Union types

The following union types reference deprecated types but MUST NOT be modified during the deprecation period. They will be updated when the deprecated types are removed:

  • ClientNotification (includes RootsListChangedNotification)
  • ClientResult (includes CreateMessageResult, ListRootsResult)
  • ServerRequest (includes CreateMessageRequest, ListRootsRequest)
  • ServerNotification (includes LoggingMessageNotification)

Documentation changes

Add a deprecation warning block at the top of each feature's documentation page, after the title:

docs/specification/draft/client/roots.mdx:

<Warning>
**Deprecated**: The Roots feature is deprecated as of this specification
version. It will remain fully functional in all specification versions released
within one year of the <YYYY-MM-DD> release. Each of those versions will
continue to support it for one year after its own release.
</Warning>

docs/specification/draft/client/sampling.mdx:

<Warning>
**Deprecated**: The Sampling feature is deprecated as of this specification
version. It will remain fully functional in all specification versions released
within one year of the <YYYY-MM-DD> release. Each of those versions will
continue to support it for one year after its own release.
</Warning>

docs/specification/draft/server/utilities/logging.mdx:

<Warning>
**Deprecated**: The Logging feature is deprecated as of this specification
version. It will remain fully functional in all specification versions released
within one year of the <YYYY-MM-DD> release. Each of those versions will
continue to support it for one year after its own release.
</Warning>

Capability negotiation

During the deprecation period, capability negotiation is unchanged:

  • Clients and servers that support deprecated features SHOULD continue to declare the corresponding capabilities.
  • Implementations that encounter deprecated capabilities MUST still handle them correctly.
  • Implementations SHOULD emit a warning (e.g., in logs or developer tooling) when deprecated capabilities are negotiated.
  • New implementations SHOULD NOT add support for deprecated features unless needed for backward compatibility with existing counterparts.

Timeline

  • Deprecated: In the next specification release (currently planned for June 2026).
  • Included in subsequent releases: All specification versions released within one year of this version's release MUST continue to include these features as deprecated.
  • Per-version support: Each version that includes these features will support them for one year after that version's release, per the one-year-per-version support policy proposed in a separate SEP.
  • Removal: Specification versions released more than one year after this version's release MAY remove these features entirely.

Rationale

Why deprecate rather than move to extensions?

These features are already implemented in many clients and servers. The extensions mechanism (SEP-2133) specifies that unless an extension is provided, implementations must behave as if the extension is not present. Retrofitting this logic into existing SDKs — especially across multiple protocol versions — would be complex and error-prone. Deprecation followed by removal is less disruptive: implementations can continue using the features as-is during the transition period, then simply stop when the features are removed.

Why deprecate rather than remove immediately?

While adoption of these features is low, they are still in use. Removing them immediately would cause unnecessary churn and disruption for users, client and server owners, and SDK builders. A deprecation window minimizes this impact by giving the ecosystem time to migrate at its own pace.

Why these three features specifically?

These were identified during a core contributor meeting as the features with the weakest adoption-to-complexity ratio. Each has viable alternatives outside the protocol, and none are critical to the core resource/tool/prompt interaction model that defines MCP. See discussion #2536.

Backward Compatibility

During the deprecation period, there are no backward compatibility issues. All deprecated features continue to work identically. No wire-level changes are introduced.

After removal (in specification versions released more than one year after this version):

  • Implementations negotiating an older protocol version that includes these features will still have access to them through that version's schema.
  • Implementations negotiating a version that has removed these features will no longer have access to them.

Security Implications

Deprecating these features has a net positive effect on security:

  • Sampling is the most security-sensitive of the three. It allows servers to request LLM completions through the client, which creates attack surface for prompt injection and data exfiltration. Removing it reduces this risk.
  • Roots exposes information about the client's filesystem to servers. Removing it reduces the risk of servers using root information to attempt directory traversal or access files outside intended boundaries.
  • Logging has minimal security implications, but removing it simplifies the protocol surface area.

No new security concerns are introduced by deprecation.

Reference Implementation

No reference implementation is required. This SEP only marks existing functionality as deprecated — no new protocol behavior is introduced.

SEP-2596 Specification Feature Lifecycle and Deprecation Policy

Final · Process · Created 2026-04-17

Abstract

This SEP defines a lifecycle for individual features within the Model Context Protocol specification, separate from the revision lifecycle of the specification document itself. It introduces three feature states (Active, Deprecated, Removed), the criteria and procedure for moving between them, a minimum window between deprecation and removal, and the documentation required at each transition. The goal is a predictable timeline that SDK authors and implementers can plan migrations against when protocol surface area is retired.

Motivation

The specification has already retired or signaled retirement of several features, but each case has been handled ad hoc:

  • The HTTP+SSE transport is described as "deprecated" in the Streamable HTTP backwards-compatibility guidance, with no stated removal date.
  • The includeContext values "thisServer" and "allServers" are labeled "soft-deprecated" in sampling/createMessage and in schema.ts, with the note that they "may be removed in future spec releases."
  • JSON-RPC batching was added in revision 2025-03-26 and removed in 2025-06-18, a single release later, with no deprecation period.
  • Open proposals such as consolidating Resource and ResourceTemplate (#1540) and deprecating roots, sampling, and logging (SEP-2577) would each retire existing surface area but have no process to follow.

This inconsistency has costs. Implementers cannot tell whether "deprecated" and "soft-deprecated" mean different things, or how long either state lasts before removal. Community questions such as discussion #2177 (asking when the SSE transport will actually be removed) have no policy to point to. At the NYC maintainer meeting, large implementers described indefinite support for past protocol versions as "corrosive tech debt." The Stability over velocity design principle observes that "removing from [the spec] is nearly impossible" but offers no path for the cases where removal is warranted.

The Core Maintainers agreed at the April 1, 2026 meeting that MCP needs "a formal versioning status and a defined deprecation cycle" with "direction agreed, mechanics TBD." This SEP proposes those mechanics.

Specification

Scope

This policy governs features of the MCP core specification: protocol messages, capabilities, transports, schema types, and normative behavioral requirements. It does not govern the independent lifecycle of SDK-specific APIs, registry policies, or the revision lifecycle of the specification document itself (Draft, Current, Final), which is defined in the versioning guide.

Note that "Final" is used in two senses in this document: a specification revision is Final when superseded by a later one (per the versioning guide), and a SEP reaches Final when its status advances per the SEP guidelines. Context disambiguates; where it does not, this document writes "the SEP reaches Final" or "Final revision" explicitly.

Feature states

A specification feature is in exactly one of three states:

State Meaning Implementer expectation
Active The feature is part of the Current specification revision with no planned removal. Implement per the feature's normative requirements.
Deprecated The feature remains in the specification but is scheduled for removal. A migration path is documented (see below). New implementations SHOULD NOT adopt the feature. Existing implementations SHOULD migrate before the earliest removal date.
Removed The feature has been deleted from draft and will be absent from the next Current revision. It remains documented in the Final revision it last appeared in. Implementations targeting that next Current revision MUST NOT depend on the feature.

The term "soft-deprecated" is retired. Existing uses in the specification are reclassified as Deprecated under this policy (see Transition).

Removal from the specification does not oblige an SDK to drop the feature from releases that continue to support an earlier revision in which it was Active or Deprecated; that timeline is governed by the SDK's own revision-support policy (see Open Questions).

A Deprecated feature MAY be restored to Active by a SEP that supersedes the deprecation SEP and documents the changed circumstances. Restoration follows the same approval path as deprecation. If the feature is later deprecated again, the minimum deprecation window in Deprecating a feature is measured afresh from the revision in which the new deprecation takes effect.

Deprecating a feature

A feature MAY be proposed for deprecation when at least one of the following holds:

  • It has been superseded by another feature that covers the same use cases.
  • It presents a security, privacy, or interoperability risk that cannot be mitigated in place.
  • Ecosystem telemetry or SDK maintainer consensus indicates negligible adoption relative to its maintenance cost.

Deprecation is a specification change and therefore requires a SEP per the SEP guidelines. The deprecation SEP MUST:

  1. Identify the feature by name and link to its definition in schema.ts (where applicable) and the specification prose.
  2. State the rationale against the criteria above.
  3. Document the migration path, or state explicitly that none is required. If the migration path names a replacement feature, that feature MUST be Active in the revision in which the deprecation takes effect; the replacement and the deprecation MAY land in the same revision. A feature is not deprecated under this policy while its documented replacement is still only in draft.
  4. Specify the minimum deprecation window: the number of months, at least twelve, that the feature MUST remain Deprecated before it is eligible for removal. The window is measured from the release of the specification revision in which the feature is first marked Deprecated, not from the date the SEP reaches Final. The feature becomes eligible for removal in the first specification revision released as Current on or after the window elapses; that point is the feature's earliest removal.

When the deprecation SEP reaches Final the deprecation is scheduled: the following changes land in the draft specification (schema/draft/ and docs/specification/draft/). The feature becomes Deprecated when the revision carrying these changes is released as Current under the versioning guide, and the minimum deprecation window is counted from that release. Anchoring the clock to the revision release means every feature deprecated in the same revision shares one earliest removal rather than each carrying a date derived from when its own SEP happened to land.

  • The feature's entry in schema.ts gains a @deprecated JSDoc tag referencing the deprecation SEP and the revision in which the deprecation takes effect.
  • The specification prose for the feature gains a deprecation notice with the same information.
  • The changelog.mdx for that revision gains an entry under a "Deprecated" heading. This SEP introduces "Deprecated" and "Removed" as standing changelog headings alongside the existing Major/Minor/Other groupings.
  • The feature is added to the deprecated registry with its deprecation SEP, the revision in which it became Deprecated, its migration path, and its earliest removal.

The deprecated registry

docs/specification/draft/deprecated.mdx is a single page listing every feature currently in the Deprecated state. It is the canonical answer to "what is on its way out, and by when," so that an implementer does not have to reconstruct that picture from deprecation entries spread across revision changelogs. Each row records the feature, its deprecation SEP, the revision in which it became Deprecated, the documented migration path, and its earliest removal. A deprecation adds a row; a removal moves the row to a Removed section of the same page with a link to the changelog entry, so the page also serves as the historical record. The registry carries no normative force of its own; it is a derived view kept consistent with the per-feature notices and changelog entries, which are the normative records.

Tier 1 SDK obligations

A feature lifecycle is only as effective as the implementations that surface it to consumers. The specification artifacts above record that a feature is Deprecated; Tier 1 SDKs (per SEP-1730) deliver that record to the implementers who would otherwise discover the removal by breakage. Once the revision in which a feature becomes Deprecated is released as Current, Tier 1 SDKs:

  • MUST mark the corresponding API surface deprecated using the language's native mechanism (for example @Deprecated in Java, [Obsolete] in .NET, @deprecated JSDoc in TypeScript, the Deprecated: doc convention in Go) in their next release, referencing the deprecation SEP and the earliest removal date where the mechanism permits. The marker applies to the SDK API surface and is not conditioned on the specification revision a consumer targets; surfacing it to consumers still on an earlier revision is intentional forward signal.
  • SHOULD emit a runtime warning when a deprecated feature is exercised, using the language's idiomatic mechanism (for example Python's DeprecationWarning, Node.js's process.emitWarning, or a configurable logger). A runtime warning reaches developers who never read API documentation and is an observable signal a conformance test can assert against.

These obligations are conformance criteria for Tier 1 status. A Tier 1 SDK that persistently fails to surface a Deprecated feature is subject to the Tier Relegation Process in SEP-1730.

Removing a feature

  1. Once a feature is set for removal, the removal is executed at the discretion of the Core Maintainers after the minimum deprecation window has elapsed, during release preparation, under the governance decision process. Removal does not require its own SEP. Before removing a feature the Core Maintainers MUST confirm that the migration target named in the deprecation SEP, if any, is still Active.
  2. A SEP is required for any other change to a deprecation or removal, for example extending or shortening the timeline (Expedited removal) or restoring the feature to Active (Feature states).

Note that features may remain Deprecated, without removal, for much longer than the minimum deprecation window.

SDKs implement deprecation as part of the SDK Tiering System (see Tier 1 SDK obligations); removal imposes no additional requirements on SDK maintainers.

When a removal decision is taken, the feature is deleted from schema/draft/schema.ts (where present) and the draft specification prose; changelog.mdx for that revision gains an entry under the "Removed" heading that links to the deprecation SEP and the last Final revision in which the feature was present; and the feature's registry row moves to the Removed section with a link to that changelog entry.

Expedited removal

The twelve-month floor MAY be shortened when the feature presents an active security risk, meaning a vulnerability with a published security advisory or documented in-the-wild exploitation for which no in-place mitigation exists. Shortening the window requires Core Maintainer approval under the governance decision process, recorded in the deprecation SEP or, where the risk surfaces after that SEP is already Final, in a short expedited-removal SEP that references it. The shortened window MUST still provide at least ninety days between the feature becoming Deprecated and its earliest removal.

Roles

Action Who
Propose deprecation, extension, or restoration Any contributor, per the SEP process
Sponsor A Maintainer or Core Maintainer, per the SEP process
Approve a deprecation SEP Core Maintainers, per the governance decision process
Decide a removal during release preparation Core Maintainers, per the governance decision process
Approve an extension or restoration SEP Core Maintainers, per the governance decision process
Approve expedited removal Core Maintainers, per the governance decision process

As with all Core Maintainer decisions, Lead Maintainers retain veto authority over each of the approvals above, per the governance roles definition.

Transition

Two features were already described as deprecated in the specification before this policy existed (see Motivation). When this SEP reaches Final they are classified as Deprecated and seeded into the registry; the deprecation-SEP requirements in Deprecating a feature are not applied retroactively. The deprecation decision in each case predates this policy; this section records it under the new vocabulary so the terms "deprecated" and "soft-deprecated" carry a single defined meaning going forward.

Both features were publicly deprecated well over twelve months before this SEP, so the minimum deprecation window has in practice already been served; re-anchoring their clock to a future revision release would restart a window the ecosystem has already had. Each is therefore given a three-month grace period from the date this SEP reaches Final before it is eligible for removal, matching the floor the Expedited removal clause sets for the shortest permissible window. Removal still follows Removing a feature: a Core Maintainer decision at release preparation, not an automatic event when the grace period ends.

Feature Migration target Earliest removal
HTTP+SSE transport Streamable HTTP Three months after this SEP is Final
includeContext: "thisServer" / "allServers" Omit the field or use "none" Follows Sampling (SEP-2577)

includeContext is a parameter of sampling/createMessage. SEP-2577 deprecates the Sampling feature as a whole; the two affected includeContext values follow that feature's deprecation schedule rather than carrying an independent removal clock, and are removed no later than Sampling itself.

This grandfathering applies only to features the specification already described as deprecated on the date this SEP reaches Final. Every subsequent deprecation follows Deprecating a feature in full, and removal of the grandfathered features follows Removing a feature without exception.

When this SEP reaches Final the following land in draft/ directly, with no separate implementation gate: the versioning guide is updated to reference this policy; deprecated.mdx is created seeded with the two features above; the "Deprecated" heading is added to changelog.mdx with both entries; and each feature gains the @deprecated schema annotation and prose notice described in Deprecating a feature. For includeContext the annotation is on the property as a whole, since per-value @deprecated tags are not expressible on a string-literal union; the HTTP+SSE transport has no schema.ts types and is annotated in the transport prose only.

Rationale

Why a separate state model from specification revisions?

The versioning guide already defines Draft, Current, and Final for specification revisions. Those states describe the editorial maturity of a whole document and say nothing about whether a given message or field within a Current revision is on its way out. The Kubernetes deprecation policy, the Node.js deprecation cycle, and IETF practice such as RFC 8996 (which deprecates TLS 1.0 and 1.1 within the TLS protocol family) all maintain feature-level deprecation rules alongside their release versioning for this reason.

Why a SEP to deprecate but not to remove?

The deliberation that needs community review is the decision to retire a feature and the choice of migration path; that is what the deprecation SEP carries. Once it reaches Final the project has committed to removal and fixed the earliest date, so carrying out that decision on schedule adds no new judgment and a second SEP for it is process for its own sake. The deliberate maintainer decision still exists as the release-preparation removal decision and its confirmations in Removing a feature, mirroring the tier advancement procedure in SEP-1730 where advancement is a maintainer decision rather than a timer expiring. A SEP is reserved for the cases that do change the committed outcome: extending the window, restoring the feature, or shortening the floor for a security risk. This keeps the process consistent with the SEP guidelines treating a change to protocol surface area as SEP-worthy while not demanding a SEP to ratify a change already made.

Why twelve months?

The NYC maintainer meeting floated a "one year supported plus one year deprecation" model and recorded reluctance to commit to longer windows given how quickly the agentic space is moving. The same discussion flagged even that model as a possible burden on SDK maintainers; this SEP keeps the twelve-month floor because removal is permissive rather than automatic (Removing a feature), so a feature stays Deprecated as long as the ecosystem needs rather than the SDKs racing the calendar. Measuring the window from the revision release rather than from the SEP date keeps it observable: it is the same clock SDK authors and implementers already track for the revision itself. Because the deprecation only takes effect when its revision is released, a replacement introduced in that same revision is proven over the twelve-month window itself; a separate prior revision is not required for that purpose. The window spans at least two of the six-month release cycles discussed at the same meeting: one for SDK maintainers to ship migration support and one for downstream adoption. Core Maintainers may leave a feature Deprecated for longer; twelve months is the minimum.

Relationship to SEP-1400 (Semantic Versioning)

SEP-1400 proposes replacing date-based revision identifiers with semantic versioning. The two proposals address different questions: SEP-1400 is about how revisions are numbered, and this SEP is about how features within a revision are retired. This SEP measures the removal window from a revision release rather than from a revision identifier, so it does not depend on the identifier scheme; it applies unchanged whether revisions are dated or semantically versioned.

Consensus

Direction was agreed at the NYC maintainer meeting (March 31, 2026) and confirmed at the April 1, 2026 Core Maintainer meeting, which recorded "formal versioning status and SDK deprecation cycle (direction agreed, mechanics TBD)." Community demand is visible in discussion #2177 (asking when SSE removal will happen) and discussion #1980 (asking to sunset a backwards-compatibility requirement that has outlived its purpose).

Backward Compatibility

This SEP introduces a process and does not change protocol behavior. The Transition section assigns a Deprecated state and an earliest removal to two features that are already informally deprecated. Neither had a stated removal date, so making the timeline explicit (a three-month grace period for features the ecosystem has already had more than a year to migrate away from) does not shorten any commitment implementers were given.

Security Implications

None identified. This is a governance change with no new protocol surface, transport, authentication flow, or trust boundary. A defined deprecation path has an indirect security benefit: it gives the project a predictable mechanism for retiring features that are later found to be unsafe, which is what the Expedited removal clause is for.

Reference Implementation

This SEP defines a process and has no reference implementation. The specification edits that apply the policy to the two existing informal deprecations are described in Transition and land directly in draft/ when this SEP reaches Final.


Open Questions

  • Specification revision support window. The NYC meeting also discussed how long Tier 1 SDKs must support a given specification revision (as distinct from a feature within one). That policy belongs in an amendment to SEP-1730, but it determines whether the deprecation window in this policy is observable in practice. If a Tier 1 SDK supports only the latest revision, a consumer that updates the SDK between releases can move directly from one that predates the deprecation to one that postdates the removal, never seeing the Deprecated marker in Tier 1 SDK obligations. Requiring Tier 1 SDKs to support all revisions released as Current within a trailing window at least equal to the twelve-month deprecation floor closes that gap. The SEP-1730 amendment should be pursued alongside this SEP.
  • Telemetry source for the "negligible adoption" criterion. The policy permits deprecation on adoption grounds, but the project has no shared telemetry today. Until one exists, this criterion relies on SDK maintainer attestation.
  • Feature maturity tiers. This SEP applies a uniform twelve-month floor to every Active feature. The Kubernetes deprecation policy uses alpha/beta/GA tiers with shorter windows for less mature features, which would have allowed the JSON-RPC batching reversal cited in Motivation without a year-long deprecation. Whether MCP should adopt an Experimental tier with a shorter or zero window is left for a follow-up SEP.
  • Wire-level deprecation signal. Tier 1 SDK obligations puts the deprecation warning into official SDKs; implementers that do not use one and do not read the changelog still receive no warning before removal. A wire-level signal (for example a _meta deprecation field on responses, comparable to the Kubernetes Warning header) would close that gap but is a Standards Track change outside the scope of this Process SEP.

SEP-2598 Pluggable Transports

Draft · Standards Track · Created 2026-04-17

Imported from upstream. Proposed as modelcontextprotocol#2598 by @kurtisvg on 2026-04-17, 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

MCP defines two Standard transports — stdio and Streamable HTTP — which compliant clients and servers support. A steady stream of proposals (gRPC, WebSockets, SSH, QUIC, message queues) seeks to elevate additional transports into the core specification. Every such addition has two compounding costs: it grows the surface the specification must own and keep internally consistent, and it adds a mandatory implementation, test, and maintenance obligation to every official SDK. One proposal adopted into core becomes N parallel SDK implementations and an ongoing carrying cost for the maintainer team.

This SEP formalizes a third path: pluggable transports at the SDK layer, split cleanly into two layers:

  • The Transport interface — a public, stable extension point plus a conformance test harness — lives in SDK core. Every Tier 1 MCP SDK MUST expose it (SEP-1730).
  • Concrete transport implementations (WebSocket, gRPC, SSH, …) live outside SDK core as separate packages. Most ship simply as third-party packages that consume the §4 interface; only the most widely-adopted are expected to be formalized as official SDK Extensions under SEP-2133. Neither tier factors into SDK conformance tiering.

The existing requirement that custom transports preserve the JSON-RPC wire format is relaxed: custom transports MUST preserve MCP semantics but MAY use any on-the-wire encoding, enabling real-world transports such as gRPC/Protobuf. In exchange, each custom transport MUST publish a bidirectional mapping to JSON-RPC so that proxies can bridge across transports. The outcome is SDK flexibility without specification complexity.

Motivation

Pressure to expand the Standard transport set

Since the introduction of MCP, several draft proposals — tracked as issues or draft SEP pull requests rather than accepted SEPs — have sought to expand transport support. Two push explicitly for core-spec inclusion; one deliberately stops short:

  • Draft SEP-1288 (WebSocket transport, issue #1288) — in-review, sponsored — proposes WebSocket as a core transport for stateful, bidirectional connections in serverless environments.
  • Draft SEP-1352 (gRPC transport, issue #1352) and the earlier issue #966 — draft, unsponsored — propose gRPC as a core transport for high-throughput enterprise deployments with binary framing and native streaming.
  • Draft SEP-2325 (SSH custom transport, PR #2325) — Informational, not Standards Track — is explicitly framed as a community example of using the existing Custom Transports clause, not as a request for core-spec inclusion. It is included here as evidence that custom-transport distribution is already happening ad hoc, not as a core-spec proposal this SEP needs to displace.

The first two, if adopted into core, would compound two costs the protocol cannot absorb:

  • Specification complexity. Every transport admitted into the core adds another chapter of normative text — framing rules, security considerations, lifecycle edge cases, authorization interaction, backwards-compatibility guidance — that the specification must own and keep consistent with every other transport and every future protocol change. The cost is not one-time; it is paid again each time the spec evolves.
  • SDK maintainer burden. Each new core transport is a mandatory implementation task for every official SDK (Python, TypeScript, Go, Java, …). A single core addition becomes N parallel implementations, N sets of tests, and N ongoing maintenance streams. Because the cost lands on SDK maintainers rather than on the proposal author, there is a structural bias toward growth that only a firm default of "no" can counter.

The current escape hatch is insufficient

The current spec acknowledges custom transports in two short paragraphs:

Clients and servers MAY implement additional custom transport mechanisms to suit their specific needs. The protocol is transport-agnostic and can be implemented over any communication channel that supports bidirectional message exchange.

Implementers who choose to support custom transports MUST ensure they preserve the JSON-RPC message format and lifecycle requirements defined by MCP. Custom transports SHOULD document their specific connection establishment and message exchange patterns to aid interoperability.

In practice this produces three failure modes:

  1. No SDK contract. SDKs today implement stdio and Streamable HTTP internally with no standardized extension point. A developer who needs gRPC must fork the SDK or wrap it from the outside, duplicating session, lifecycle, and authorization logic — and falling behind on upgrades.
  2. No cross-SDK parity. Even where individual SDKs expose an abstraction, the shape varies across Python, TypeScript, Go, and Java. A third-party transport cannot be meaningfully packaged for the ecosystem.
  3. JSON-RPC lock-in rules out binary-encoded custom transports. The MUST ... preserve the JSON-RPC message format clause blocks custom transports whose wire format is not JSON-RPC — most prominently gRPC, which serializes Protobuf. (Byte-tunnel transports like SSH that wrap an existing JSON-RPC stream are unaffected by this clause; the issue is specifically with transports that want a different on-the-wire encoding.) The current language effectively forbids the encoding-level flexibility that motivates these proposals.

What formalization unlocks

  • Spec scope stays bounded; ecosystem interoperability is preserved.
  • Advanced integrators get a supported, stable extension point.
  • Popular custom transports (WebSocket, gRPC, SSH) have a well-defined distribution path via SDK Extensions rather than lobbying for core inclusion.
  • SDK tiering gains a concrete, testable conformance criterion.
  • Transport-to-transport bridging via generic proxies becomes tractable. Because every custom transport publishes a bidirectional JSON-RPC mapping (§3.1), a single proxy pattern — JSON-RPC in the middle, custom encoding on each side — can bridge any two transports. A gRPC-only client can reach an SSH-tunneled server, a Standard-only client can reach a WebSocket server, and so on, without per-pair proxy code.

Specification

1. Standard transports (unchanged)

stdio and Streamable HTTP remain the only Standard transports. Existing normative requirements in the transports chapter (including Clients SHOULD support stdio whenever possible) are unchanged.

2. No additional transports in core

The core MCP specification does not add new transport mechanisms. Proposals for gRPC, REST-style polling, WebSockets, QUIC, message queues, SSH, and similar transports are out of scope for the core specification and SHOULD instead be pursued as SDK Extensions (see §5).

3. Custom transports are not required to carry JSON-RPC

Two places in the transports chapter currently bind the entire protocol to JSON-RPC at the wire level and MUST both be updated:

  1. The chapter's opening sentence — MCP uses JSON-RPC to encode messages. JSON-RPC messages MUST be UTF-8 encoded. — is scoped to Standard transports. Updated text, in effect:

    Standard MCP transports (stdio and Streamable HTTP) use JSON-RPC to encode messages, and JSON-RPC messages MUST be UTF-8 encoded on those transports. Custom transports (see below) MAY use a different on-the-wire encoding subject to the requirements in §Custom Transports.

  2. The Custom Transports section's requirement that implementers MUST ... preserve the JSON-RPC message format is removed and replaced with semantics-only preservation. Updated text, in effect:

    Custom transports MUST preserve the MCP lifecycle and request/response semantics defined by this specification, but MAY use any on-the-wire encoding. A custom transport MAY transcode JSON-RPC messages into a different serialization (e.g., Protobuf, CBOR, a custom binary framing) provided every MCP method, parameter, result, notification, and error is faithfully round-tripped and no MCP semantics are lost or added.

Standard transports (stdio, Streamable HTTP) continue to carry JSON-RPC unchanged. Both relaxations apply only to custom transports reached via the SDK Transport interface defined in §4.

3.1 Documented JSON-RPC mapping (proxyability requirement)

In exchange for wire-format flexibility, every custom transport MUST publish a bidirectional mapping between its wire encoding and the equivalent JSON-RPC representation of every MCP message it carries. The mapping MUST:

  1. Cover every MCP method, notification, result, and error the transport supports, including initialize and shutdown.
  2. Specify translation for fields whose shape changes across encodings (e.g., id when JSON-RPC allows string/number/null but the custom encoding uses a fixed type; binary payloads in fields typed as strings; error codes and error data).
  3. Define behavior for MCP messages the custom encoding cannot represent natively — either by extending the encoding, by falling back to an embedded JSON-RPC envelope, or by explicitly documenting the method as unsupported by this transport.
  4. Be precise enough that an independent implementer could build a proxy translating between the custom transport and a Standard JSON-RPC transport without reference to the custom transport's source code.

This requirement is what keeps the ecosystem reachable across transports. A client that speaks only Standard transports SHOULD be able to reach a server behind a custom transport via a generic translating proxy, and vice versa. The mapping is the specification of that proxy.

The round-trip verification of this mapping is part of the SDK conformance harness defined in §4.2.

4. SDK core: the Transport interface and conformance harness

The Transport interface is part of SDK core, not an Extension. It is the extension point that makes custom transports possible; it is not itself a custom transport. Every Tier 1 MCP SDK MUST ship:

4.1 The Transport interface

A public, stable Transport interface that:

  1. Allows callers to supply a custom transport implementation at client and server construction time, without forking or patching the SDK.
  2. Preserves all MCP lifecycle semantics (initialize, shutdown, notifications, request/response correlation). It MUST NOT require the transport to emit or accept JSON-RPC-encoded bytes; wire encoding is the transport's choice (§3). SDKs SHOULD present messages to the transport at a level of abstraction that admits non-JSON encodings (e.g., typed message objects rather than pre-serialized JSON strings).
  3. Surfaces transport-level errors (connection loss, framing errors, authentication failures) through a consistent error type that callers can distinguish from protocol-level errors.
  4. Is transport-agnostic in shape: the interface MUST NOT leak stdio-specific or Streamable-HTTP-specific concepts (e.g., stderr, HTTP status codes) into its public surface.

SDKs MAY expose language-idiomatic variations (e.g., async iterators in TypeScript, channels in Go, context managers in Python) provided the semantics above are preserved.

4.2 The conformance test harness

A reusable test harness that any Transport implementation can be run against. The harness MUST:

  1. Exercise every MCP method, notification, result, and error defined by the protocol version the SDK targets, plus the full initialize and shutdown lifecycle.
  2. Include the round-trip test required by §3.1 (encode a corpus of JSON-RPC messages into the transport's wire encoding and decode them back, asserting semantic equality against the published mapping).
  3. Cover framing edge cases (partial writes, oversized messages, interleaved notifications) regardless of wire encoding.
  4. Be invocable by downstream transport authors as a library — i.e., transport Extensions (§5) import the harness to self-certify before release.

At least one worked third-party transport example MUST be referenced in the SDK's documentation to demonstrate the interface.

5. Transport implementations live outside SDK core

Concrete non-Standard transports (WebSocket, gRPC, SSH, QUIC, …) are not part of SDK core and do not factor into SDK tier compliance. They ship as separate implementations distributed independently of the SDK, and consume the interface and harness from §4.

This SEP recognizes two tiers of transport implementation:

  • Third-party packages (the default). Anyone MAY publish a Transport implementation as a standalone package. It MUST implement the interface from §4.1, preserve MCP semantics per §3, publish a JSON-RPC mapping per §3.1, and pass the SDK's conformance harness (§4.2) before release. It MAY add its own transport-specific tests on top of the shared harness (reconnection behavior, auth flow, wire encoding edge cases). No MCP governance step is required to ship one; it is simply a package that depends on an SDK.
  • Formalized SDK Extensions (the exception). A small number of transport implementations with broad ecosystem adoption MAY be promoted to official SDK Extensions under SEP-2133. Formalization buys governance, review, co-maintenance, and the visibility of appearing in Extension indexes. It does not change what the implementation has to do technically — the bar above applies either way. The vast majority of transport implementations are expected to remain in the third-party tier; Extension promotion is reserved for transports the ecosystem converges on.

SEP-2133 already carves Extension support out of SDK conformance tiering ("Extension support is not required for ... SDK conformance tiers"). This SEP's Tier 1 requirement therefore applies only to the interface and harness in §4, never to any particular transport implementation in either tier.

In all cases, transport implementations are selected by the application at client/server construction time. They are not negotiated over MCP initialize — that would be a bootstrap paradox, since the transport must already exist to carry initialize.

6. SDK tiering

Under SEP-1730, a Tier 1 SDK MUST:

  • Ship the Transport interface (§4.1).
  • Ship the conformance test harness (§4.2).
  • Include tests in the SDK's own conformance suite that verify its stdio and Streamable HTTP adapters pass the harness.

Shipping any particular transport Extension (WebSocket, gRPC, SSH) is not a Tier 1 requirement and is explicitly delegated to the Extension ecosystem. Tier 2 and Tier 3 SDKs MAY skip the Transport interface and harness entirely; their stdio/Streamable HTTP implementations remain valid.

6.1 Grandfathering for existing stable SDKs

SDKs that have already shipped a stable major version at the time this SEP is accepted MAY retain Tier 1 status without implementing §4.1 and §4.2 until their next major version bump. This is a one-time grace window: the material refactor described in Backward Compatibility (moving the JSON-RPC serializer out of the Transport contract) is disruptive enough that forcing it into a patch or minor release would violate semver guarantees downstream consumers rely on.

SDKs that reach their first stable release after this SEP is accepted MUST comply from that release; no grandfathering applies.

Grandfathered SDKs SHOULD publish a target release for §4 compliance and link to it from their README so ecosystem transport authors can plan against it.

Rationale

Why a small Standard set is load-bearing

Promoting a transport into the Standard set is the most expensive move the protocol can make, on two axes:

  • Protocol complexity. Each Standard transport lives in the spec forever. Its framing, lifecycle, security, and authorization chapters must be maintained in lockstep with every other transport and every future protocol change. Small additions today become large review surfaces tomorrow.
  • SDK maintainer workload. Every Standard transport is a permanent implementation obligation for every official SDK. Three additions turn what was a two-transport workload into a five-transport workload across every language, with the corresponding test matrices and bug reports.

Keeping the Standard set at stdio and Streamable HTTP is therefore not a minimalist aesthetic — it is the mechanism that keeps both the specification and the SDK maintainer team tractable as the ecosystem grows.

Why the interface lives in the SDK, not the spec

  • The spec governs wire format and semantics; it does not mandate SDK shape.
  • An interface in the spec would drift from language-idiomatic SDK APIs or force all SDKs into an awkward shared shape.
  • Pinning the interface at the SDK layer lets each SDK evolve it language-idiomatically while the tiering system enforces a common contract.

Why a formal interface beats the status quo for auditability

Today's custom transports are typically SDK forks or monkey-patches that are hard to audit and drift out of sync with upstream security fixes. A formal, public Transport interface lets third-party transports ship as independently reviewable packages that sit alongside an un-forked SDK. Vulnerability discovery, coordinated disclosure, and upstream security patches all become more tractable when the extension point is explicit rather than a private contract with SDK internals.

Why require a JSON-RPC mapping (§3.1)

Relaxing the JSON-RPC preservation requirement without any replacement constraint would let a custom transport drift into being its own protocol. Requiring a published bidirectional mapping to JSON-RPC pins the transport to MCP's actual semantics and gives the ecosystem a concrete way to bridge across transports: a Standard-only client can reach a custom-transport server (and vice versa) through a generic translating proxy whose behavior is fully specified by the mapping. JSON-RPC remains the lingua franca even when it is not the wire format.

Alternatives considered

  • Add gRPC / WebSocket to the spec. Rejected: produces the compatibility matrix the core goal prevents. See draft SEP-1288 and draft SEP-1352 for concrete instances. (SSH/draft SEP-2325 is not in this category — it is already framed as Informational and custom, not a core-spec proposal.)
  • Leave custom transports informal (status quo). Rejected: produces no SDK parity, no conformance story, and no supported distribution path.
  • Central registry of blessed custom transports. Considered. The Extensions mechanism from SEP-2133 already provides this pattern; a parallel transport registry would duplicate governance.
  • Specify a wire-level transport negotiation handshake. Rejected as scope creep. Negotiation is a higher-order concern; this SEP deliberately limits itself to the SDK extension point.

Prior art

The "small core + SDK extension points" pattern is well-established:

  • OpenTelemetry Exporters. The OTel specification keeps the core data model small and defines a pluggable SpanExporter / LogRecordExporter / MetricExporter interface that third parties implement. A given language SDK ships a handful of first-party exporters (OTLP, stdout); the rich ecosystem of backend integrations (Jaeger, Zipkin, Prometheus, vendor-specific) lives in separate packages per language, all conforming to the same interface shape. This is the closest direct analogue to what this SEP proposes.
  • Python WSGI / ASGI. PEP 3333 and the ASGI spec define a single narrow interface between Python web servers and web applications. Dozens of servers (Gunicorn, uWSGI, Uvicorn, Daphne, Hypercorn, …) and hundreds of frameworks compose freely against that contract, without the HTTP spec itself needing to name any of them.
  • Java JDBC. The java.sql.Driver / Connection interfaces define a contract; every database vendor ships a driver implementing it. The Java platform does not need to bless Postgres, MySQL, or Oracle — the interface is the blessing.
  • gRPC interceptors and transport credentials. gRPC keeps its wire protocol fixed and exposes interceptors and credential plugins for cross-cutting behavior. Applications extend behavior without modifying the protocol.

In each case, the ecosystem kept wire-level or protocol-level scope small and moved flexibility into a stable library-surface contract — exactly the trade this SEP applies to MCP transports.

Backward Compatibility

  • No normative changes to stdio or Streamable HTTP. Both continue to carry JSON-RPC with the existing framing rules.
  • No change to the JSON-RPC wire protocol itself.
  • Normative changes to the transports chapter (§3). Two clauses are amended: the chapter-opening sentence is scoped to Standard transports, and the Custom Transports JSON-RPC preservation requirement is replaced with a semantics-only one. The wire-format relaxation is strictly more permissive, but §3.1 adds a new obligation — every custom transport MUST publish a bidirectional JSON-RPC mapping. Existing JSON-RPC-preserving custom transports therefore remain technically compliant on the wire but MUST publish a (near-trivial) identity mapping to meet §3.1. Custom transports that previously transcoded JSON-RPC into another encoding were non-compliant under the old rule and become compliant under the new rule once they ship a mapping.
  • SDKs that already expose a transport abstraction MAY need signature adjustments to match §4.1, plus the conformance harness described in §4.2. SDKs that do not expose an abstraction MUST add one to retain Tier 1 status per SEP-1730; this is a material refactor (moving the JSON-RPC serializer to the Standard-transport adapter rather than hard-wiring it into the Transport contract), not a cosmetic change.
  • Grandfathering for existing stable SDKs (§6.1). SDKs already at a stable major version when this SEP is accepted retain Tier 1 status without §4 compliance until their next major version bump. The §4 refactor is too disruptive to land in a patch or minor release without breaking semver. SDKs whose first stable release is after acceptance comply from day one.
  • Existing third-party transports written against SDK internals MAY need to migrate to the new public interface. This is a one-time cost and eliminates the ongoing fork-maintenance burden.

Security Implications

The trust model for custom transports is largely unchanged by this SEP — custom transports are already permitted today, and the concerns that apply to them (third-party code in the data path, full visibility into MCP message contents) apply identically whether the transport is built against the new interface or today's ad-hoc escape hatch. This section surfaces only the risks specifically introduced or shaped by this SEP.

  • Transcoding fidelity is a new correctness boundary. Because custom transports MAY use a non-JSON-RPC wire encoding (§3), transcoding becomes a place where MCP semantics can be silently lost, reordered, or forged. The conformance harness (§4.2) and the round-trip mapping test (§3.1) exist specifically to bound this risk; transport Extensions MUST pass the harness before release.

Reference Implementation

TODO — to be completed before this SEP reaches Final. Planned scope:

  • Prototype the Transport interface in the TypeScript SDK and the Python SDK (branches linked from the PR).
  • Port one non-Standard transport (WebSocket, following SEP-1288) as an SDK Extension demonstrating the interface end-to-end.
  • Publish a transport conformance test harness that exercises the full MCP lifecycle against any Transport implementation.

Accepted SEPs this proposal builds on:

  • SEP-1730 — SDK tiering system; this SEP adds Tier 1 conformance requirements (interface + harness).
  • SEP-2133 — Extensions framework; the distribution path for formalized transport implementations.

Draft / in-review proposals this SEP addresses:

  • Draft SEP-1288 — issue #1288 — WebSocket transport, in-review, sponsored. Addressed by this SEP via the third-party-package / Extension path rather than core inclusion.
  • Draft SEP-1352 — issue #1352 — gRPC transport, draft, unsponsored. Also issue #966 (earlier gRPC proposal). Addressed via the third-party-package / Extension path.
  • Draft SEP-2325 — PR #2325 — SSH custom transport, Informational. A concrete example of the custom-transport path this SEP formalizes; aligned in spirit.

SEP-2624 Interceptors for Model Context Protocol

Draft · Standards Track · Created 2025-11-04

Abstract

This SEP proposes an interceptor framework for the Model Context Protocol (MCP) that allows context operations to be intercepted, validated, and transformed at key points in the agentic lifecycle. It introduces a new MCP primitive for operations that shape agent context, including MCP-defined operations such as tool invocations, resource access, and prompt handling, as well as other well defined context operations with a standardized interface. Like other MCP primitives, interceptors are deployed as MCP servers and can be invoked by both clients and servers. The framework defines two types of interceptors, validators, which check context and return a pass/fail decision, and mutators, which transform context and return a modified payload. Interceptors follow a deterministic, trust-boundary-aware execution model and are discoverable and invocable through MCP's existing JSON-RPC patterns.

Motivation

The Problem: A Sprawling Ecosystem Without Reusability

The MCP ecosystem is rapidly developing a sprawling landscape of sidecars, proxies, and gateways to address cross-cutting concerns like validation, security, auditing, and rate limiting. However, these implementations are largely:

  • Non-reusable: Each solution is tightly coupled to specific deployment patterns or languages
  • Non-interoperable: Different approaches don't work together, creating vendor lock-in
  • Duplicative: Similar functionality is reimplemented across different tools and servers
  • Fragmented: No common interface or discovery mechanism exists

Imported from upstream. Proposed as modelcontextprotocol#2624 by @Degiorgio on 2026-04-22, 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.

This leads to an M × N problem: Each of M clients must integrate with each of N middleware solutions, resulting in M × N integration points and configurations.

The Solution: Standardized Plug-and-Play Interceptors

This SEP proposes standardizing interceptors in the same plug-and-play fashion that made MCP tools successful. The core goal is to transform the M × N problem into an M + N problem:

  • Clients implement the interceptor call pattern once
  • Servers expose interceptors through a standardized interface once
  • Platform teams can deploy interceptors across all compatible clients and servers

Key Advantages

1. Plug-and-Play Simplicity

Just as tools made MCP powerful through discoverability and standardization, interceptors bring the same benefits to cross-cutting concerns:

Add gateway-level interceptor → Plug in configuration → All tools and servers gain new capabilities

2. Language-Agnostic Deployment

Unlike framework-specific middleware (e.g., FastAPI middleware, Express.js middleware), interceptors enable platform teams to:

  • Introduce new capabilities company-wide regardless of implementation language
  • Guarantee consistent behavior across Python, TypeScript, Go, Rust, or any other server implementation
  • Deploy guardrails and policies without requiring code changes to each service

3. Sidecar and Service-Based Architecture

While traditional library-based middleware requires code integration, interceptors support:

  • First-party interceptors: Deployed as code running locally within the server process
  • Third-party interceptors: Deployed as separate services that servers call out to
  • Hybrid approaches: Mix local and remote interceptors based on security, performance, and organizational requirements

This flexibility is critical for:

  • Security: Sensitive validation logic can run in isolated, hardened services
  • Compliance: Audit logging can be centralized and immutable
  • Performance: High-throughput operations can use in-process interceptors while complex policies use dedicated services

4. Well-Defined Invocation Lifecycle

Interceptors provide a standardized execution model with clear semantics for:

  • Life-Cycle Event subscription and filtering
  • Request/response phase handling
  • Priority-based ordering for mutations
  • Validation severity levels (info, warn, error)
  • Error handling and propagation

Technical Gaps Addressed

The current MCP specification lacks a standardized mechanism for:

  1. Input Validation: No way to validate tool parameters, prompt inputs, resource requests, sampling requests, or elicitation requests before processing
  2. Output Transformation: No capability to transform or sanitize responses before they reach clients or servers
  3. Cross-cutting Concerns: No support for logging, rate limiting, or content filtering that applies across multiple protocol operations (both server and client features)
  4. Policy Enforcement: No mechanism to enforce organizational policies on what prompts can be sent to LLMs, what tool results are returned, what sampling models can be used, or what data can be elicited from users
  5. Security Boundaries: When MCP servers and clients cross trust boundaries, there's no way to inspect or validate messages in either direction
  6. LLM Interaction Control: While MCP handles tool/resource/prompt orchestration and sampling requests, there's no hook into these interactions for content filtering or prompt injection detection
  7. Client Feature Protection: No way to prevent servers from requesting excessive sampling tokens, asking for sensitive information through elicitation, or accessing unauthorized filesystem roots

These limitations become critical in enterprise deployments where:

  • Multiple MCP servers with varying trust levels need coordination
  • Compliance requirements mandate audit trails and content filtering
  • Security policies require validation of inputs and outputs
  • Performance monitoring requires instrumentation across protocol operations

Ecosystem Benefits

For Platform Teams

  • Deploy interceptors once, gain coverage across all compatible servers and clients
  • Enforce company-wide policies without modifying individual services
  • Centralize security, compliance, and audit concerns

For Server Developers

  • Inherit rich ecosystem of interceptors without custom integration work
  • Focus on core business logic rather than cross-cutting concerns
  • Support both local and remote interceptor patterns

For Client Developers

  • Discover and utilize interceptors through standard MCP mechanisms
  • Consistent behavior across different servers
  • Reduced integration complexity

For the Community

  • Build reusable interceptors that work across the entire ecosystem
  • Share best practices through standardized implementations
  • Foster innovation in security, auditing, and policy enforcement

An interceptor framework addresses these needs by providing standardized extension points that maintain MCP's design principles while enabling powerful cross-cutting functionality and solving the ecosystem's reusability challenge.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

Interceptor

Definition: a Context Operation is any operation that shapes, accesses, or modifies agentic context, such as tool invocations (which add results to context), resource access (which add content to context), prompt handling (which provide context templates), and skills (which orchestrate context).

Definition: a Lifecycle Event is an occurrence during a Context Operation, a specific moment when a context operation is initiated (request phase) or completed (response phase).

Example of MCP Lifecycle Events include when tools/call is invoked, when resources/read returns data, or when sampling/createMessage is requested.

An Interceptor is an MCP primitive that provides governance for context operations through validation or mutation logic. Like tools, prompts, and resources, interceptors are discoverable, and hosted on MCP servers.

Interceptors come in two types: Validators (see Validator) and Mutators (see Mutator).

Important: How do Interceptors Differ from Tools?

Tools and interceptors have fundamentally different invocation models:

Tools Interceptors
Invoked by LLM (non-deterministic) Invoked on specific Lifecycle Events (deterministic)
Result handling Automatically added to LLM context Returned to invoker (client, server, or agent harness) who then decides what to do with it.
Purpose Extend agent capabilities Govern context operations
Interface
interface Interceptor {
  /**
   * Unique identifier for the interceptor
   */
  name: string;

  /**
   * Semantic version of this interceptor
   */
  version?: string;

  /**
   * Human-readable description
   */
  description?: string;

  /**
   * Interceptor operation type
   */
  type: "validation" | "mutation";

  /**
   * Hooks: defines which Lifecycle Events and phases trigger this interceptor.
   * Each entry specifies a set of events and a single phase.
   */
  hooks: Array<{
    /**
     * List of Lifecycle Events this interceptor hooks into
     */
    events: InterceptionEvent[];

    /**
     * Execution phase
     */
    phase: "request" | "response";
  }>;

  /**
   * Execution mode (default: "active")
   * - "active": Normal blocking / transforming behavior
   * - "audit": Non-blocking operation
   *   - For validators: logs violations without blocking execution
   *   - For mutators: computes transformations without applying them (shadow mutations)
   */
  mode?: "active" | "audit";

  /**
   * Failure routing policy (default: false - fail-closed)
   *
   * Enforce mode:
   * - false (fail-closed): If the interceptor crashes or times out, block the message
   * - true (fail-open): If the interceptor fails, allow the message to proceed
   *
   * Note: In audit mode, the message itself always proceeds (audit never blocks execution).
   * failOpen controls whether audit failures are treated as errors or warnings.
   */
  failOpen?: boolean;

  /**
   * Priority hint for mutation interceptor ordering (lower executes first).
   *
   * Can be specified as:
   * - A single number: applies to both request and response phases
   * - An object: specify different priorities per phase
   *
   * Range: 32-bit signed integer (-2,147,483,648 to 2,147,483,647)
   * Default: 0 if omitted
   *
   * Tie-breaker: Interceptors with equal priorityHint are ordered alphabetically by name.
   * For validation interceptors, this field is ignored.
   *
   * Examples:
   *   priorityHint: -1000                           // Same priority for both phases
   *   priorityHint: { request: -1000 }              // Request: -1000, Response: 0 (default)
   *   priorityHint: { response: 1000 }              // Request: 0 (default), Response: 1000
   *   priorityHint: { request: -1000, response: 1000 }  // Different per phase
   */
  priorityHint?:
    | number
    | {
        request?: number;
        response?: number;
      };

  /**
   * Protocol version compatibility
   */
  compat?: {
    /**
     * Minimum MCP protocol version required
     */
    minProtocol: string;
    /**
     * Maximum MCP protocol version supported (optional)
     */
    maxProtocol?: string;
  };

  /**
   * Optional JSON Schema for interceptor configuration
   * Documents the expected configuration format
   */
  configSchema?: {
    type: "object";
    properties?: Record<string, unknown>;
    required?: string[];
    additionalProperties?: boolean;
  };
}
Hooks

Hooks define which Lifecycle Events trigger an interceptor's invocation. The hooks array in the interceptor definition contains one or more entries, each declaring a set of Lifecycle Events (events, e.g., tools/call, resources/read) and a phase (phase: "request" or "response") where this interceptor will be invoked. Interceptors that run on both phases use two entries — one per phase.

The following interception events are defined by this specification. This list is NOT exhaustive, implementations MAY define additional interception events for custom or non-MCP context operations.

type InterceptionEvent =
  // MCP Server Features
  | "tools/list"
  | "tools/call"
  | "prompts/list"
  | "prompts/get"
  | "resources/list"
  | "resources/read"
  | "resources/subscribe"

  // MCP Client Features
  | "sampling/createMessage"
  | "elicitation/create"
  | "roots/list"

  // LLM Interaction Events (using common format)
  | "llm/completion"

  // Wildcards
  | "*" // Matches all events

  // Implementations MAY define additional events
  | string;

Wildcards:

  • "*": Matches all Lifecycle Events. Interceptors that hook into "*" MUST be invoked for every event on the phase specified by the enclosing hook entry.

Implementations MAY support additional wildcard patterns, such as namespace wildcards (e.g., "tools/*" to match all tool events, "resources/*" to match all resource events). Custom wildcard patterns SHOULD follow glob-style conventions.

Implementations MAY extend the set of interceptable Lifecycle Events beyond those defined in this specification. Custom events SHOULD follow the namespace/operation naming convention (e.g., "custom/myOperation").

Validator

A Validator is defined as strictly non-mutating interceptor that inspects a context operation and returns a structured decision. Validators MUST NOT modify the payload.

Common use cases of Validators include:

  • As Governance Checks (guardrails):
    • PII detection,
    • Prompt injection scanning
    • Credential detection
  • As Generic Context decision procedures
    • JSON schema validation,
    • Code compilation checks,

When invoked a validator receives the event payload and MUST return a ValidationResult.

ValidationResult:

interface ValidationResult {
  // Common Interceptor fields
  interceptor: string; // Name of the interceptor
  type: "validation"; // Interceptor type
  phase: "request" | "response"; // Execution phase
  durationMs?: number; // Execution time in ms
  info?: Record<string, unknown>; // Additional interceptor-specific data

  // Validation-specific fields
  valid: boolean; // Overall validation outcome
  severity?: "info" | "warn" | "error";
  messages?: Array<{
    path?: string; // JSON path to the offending field
    message: string; // Human-readable explanation
    severity: "info" | "warn" | "error";
  }>;
  suggestions?: Array<{
    // Optional corrections
    path: string;
    value: unknown;
  }>;
  signature?: {
    // Future: cryptographic proof of validation
    algorithm: "ed25519";
    publicKey: string;
    value: string;
  };
}

See Execution Model for validator execution semantics.

Mutator

A Mutator is a transformation interceptor that modifies context as it flows through the system. Mutators MAY change payloads to enforce policies, sanitize data, or enrich context.

Common use cases include:

  • As Governance Enforcers:
    • PII redaction,
    • Credential scrubbing,
    • Toxicity filtering
  • As Generic Context Transformers:
    • Response formatting
    • Context Augmentation
    • Prompt template injection

When invoked a mutator receives the event payload and MUST return a MutationResult

MutationResult:

interface MutationResult {
  // Common Interceptor fields
  interceptor: string; // Name of the interceptor
  type: "mutation"; // Interceptor type
  phase: "request" | "response"; // Execution phase
  durationMs?: number; // Execution time in ms
  info?: Record<string, unknown>; // Additional interceptor-specific data

  // Mutation-specific fields
  modified: boolean; // Whether the payload was changed
  payload: unknown; // Transformed payload (or original if not modified)
}

See Execution Model for mutator execution semantics.

JSON-RPC Methods

Interceptor Discovery

Request:

{
  jsonrpc: "2.0",
  id: 1,
  method: "interceptors/list",
  params?: {
    // Optional filter by event
    event?: InterceptionEvent;
  }
}

Response:

{
  jsonrpc: "2.0",
  id: 1,
  result: {
    interceptors: [
      {
        name: "content-filter",
        version: "1.2.0",
        description: "Filters sensitive content from prompts and responses",
        type: "mutation",
        hooks: [
          { events: ["llm/completion", "prompts/get"], phase: "request" },
          { events: ["llm/completion", "prompts/get"], phase: "response" }
        ],
        priorityHint: -500,  // Same priority for both request and response
        compat: {
          minProtocol: "2024-11-05"
        },
        configSchema: {
          type: "object",
          properties: {
            sensitivityLevel: {
              type: "string",
              enum: ["low", "medium", "high"]
            },
            redactPatterns: {
              type: "array",
              items: { type: "string" }
            }
          }
        }
      },
      {
        name: "parameter-validator",
        version: "2.0.1",
        description: "Validates tool call parameters",
        type: "validation",
        hooks: [
          { events: ["tools/call"], phase: "request" }
        ],
        compat: {
          minProtocol: "2024-11-05",
          maxProtocol: "2025-12-31"
        },
        configSchema: {
          type: "object",
          properties: {
            strictMode: { type: "boolean" }
          }
        }
      },
      {
        name: "pii-redactor",
        version: "2.1.0",
        description: "Redacts PII from requests and responses",
        type: "mutation",
        hooks: [
          { events: ["tools/call", "llm/completion"], phase: "request" },
          { events: ["tools/call", "llm/completion"], phase: "response" }
        ],
        failOpen: false,  // Fail-closed: block if interceptor fails (security-critical)
        // Different priorities for request vs response
        priorityHint: {
          request: -1000,  // Redact early when sending
          response: 1000   // Redact late when receiving (after decryption)
        },
        compat: {
          minProtocol: "2024-11-05"
        },
        configSchema: {
          type: "object",
          properties: {
            patterns: {
              type: "array",
              items: { type: "string" }
            }
          }
        }
      },
      {
        name: "sampling-guard",
        version: "1.0.0",
        description: "Enforces token limits and model policies for sampling requests",
        type: "validation",
        hooks: [
          { events: ["sampling/createMessage"], phase: "request" }
        ],
        compat: {
          minProtocol: "2025-06-18"
        },
        configSchema: {
          type: "object",
          properties: {
            maxTokens: { type: "number" },
            allowedModels: {
              type: "array",
              items: { type: "string" }
            }
          }
        }
      },
      {
        name: "elicitation-pii-blocker",
        version: "1.0.0",
        description: "Prevents elicitation requests from asking for sensitive information",
        type: "validation",
        hooks: [
          { events: ["elicitation/create"], phase: "request" }
        ],
        compat: {
          minProtocol: "2025-06-18"
        },
        configSchema: {
          type: "object",
          properties: {
            blockedPatterns: {
              type: "array",
              items: { type: "string" }
            },
            sensitiveFields: {
              type: "array",
              items: { type: "string" }
            }
          }
        }
      },
      {
        name: "audit-logger",
        version: "1.0.0",
        description: "Logs all MCP operations for compliance",
        type: "validation",
        hooks: [
          { events: ["*"], phase: "request" },
          { events: ["*"], phase: "response" }
        ],
        mode: "audit",
        failOpen: true,
        configSchema: {
          type: "object",
          properties: {
            destination: {
              type: "string",
              enum: ["local", "remote", "both"]
            },
            includePayloads: { type: "boolean" }
          },
          required: ["destination"]
        }
      }
    ]
  }
}
Interceptor Invocation

Request for Validation:

{
  jsonrpc: "2.0",
  id: 2,
  method: "interceptor/invoke",
  params: {
    name: "parameter-validator",
    event: "tools/call",
    phase: "request",
    payload: {
      // Original request content
      method: "tools/call",
      params: {
        name: "get_weather",
        arguments: {
          location: "malicious_sql_injection"
        }
      }
    },
    config?: {
      // Optional interceptor-specific configuration
    }
  }
}

Response for Validation:

{
  jsonrpc: "2.0",
  id: 2,
  result: {
    valid: false,
    severity: "error",
    messages: [
      {
        path: "params.arguments.location",
        message: "Input contains potentially malicious content",
        severity: "error"
      }
    ],
    // Optional: suggested corrections
    suggestions?: [
      {
        path: "params.arguments.location",
        value: "[REDACTED]"
      }
    ],
    // Future: Cryptographic signature for validation result
    // This enables verification that validation occurred at trust boundary
    signature?: {
      algorithm: "ed25519",
      publicKey: "...",
      value: "..."
    }
  }
}

Note: The signature field is reserved for future use to enable cryptographic verification of validation results at trust boundaries. This will allow recipients to verify that validations were performed by authorized interceptor before accepting data.

Request for Mutation:

{
  jsonrpc: "2.0",
  id: 3,
  method: "interceptor/invoke",
  params: {
    name: "content-filter",
    event: "llm/completion",
    phase: "request",
    payload: {
      // LLM completion request (following common format)
      messages: [
        {
          role: "user",
          content: "What is the password for admin@example.com?"
        }
      ],
      model: "gpt-4"
    }
  }
}

Response for Mutation:

{
  jsonrpc: "2.0",
  id: 3,
  result: {
    modified: true,
    payload: {
      messages: [
        {
          role: "user",
          content: "What is the password for [REDACTED_EMAIL]?"
        }
      ],
      model: "gpt-4"
    },
    // Additional information about the mutation
    info: {
      redactions: 1,
      reason: "PII detected and redacted"
    }
  }
}
Invocation Configuration and Context

Interceptors MAY receive configuration and context:

interface InterceptorInvocationParams {
  name: string;
  event: InterceptionEvent;
  phase: "request" | "response";
  payload: unknown;

  // Optional configuration for this invocation
  config?: Record<string, unknown>;

  // Optional timeout in milliseconds
  // If exceeded, interceptor execution is cancelled and returns timeout error
  timeoutMs?: number;

  // Optional context information
  context?: {
    // Identity information
    principal?: {
      type: "user" | "service" | "anonymous";
      id?: string;
      claims?: Record<string, unknown>;
    };

    // Tracing information
    traceId?: string;
    spanId?: string;

    // Timing information
    timestamp: string; // ISO 8601

    // Session information
    sessionId?: string;

    // FUTURE: Interceptor state propagation
    // Allows interceptor to share state through the chain
    // This field is reserved for future enhancement
    interceptorState?: Record<string, unknown>;
  };
}

Timeout and Cancellation:

When an interceptor is invoked with timeoutMs specified:

  • The interceptor execution MUST be cancelled if it exceeds the timeout
  • A timeout error MUST be returned with code -32000 (Server error)
  • Interceptors in audit mode that timeout MUST be logged but MUST NOT block the main flow
  • When failOpen is true, a timed-out interceptor MUST allow the message to proceed
// Timeout error response
{
  jsonrpc: "2.0",
  id: 1,
  error: {
    code: -32000,
    message: "Interceptor execution timeout",
    data: {
      interceptor: "slow-validator",
      timeoutMs: 5000,
      phase: "request"
    }
  }
}
Error Handling

Interceptor invocation errors follow standard JSON-RPC error format:

{
  jsonrpc: "2.0",
  id: 4,
  error: {
    code: -32603,
    message: "Interceptor execution failed",
    data: {
      interceptor: "content-filter",
      reason: "Configuration invalid"
    }
  }
}

Execution Model

Execution Model Summary:

  • Sending: Mutate → Validate → Send. A mutation error MUST halt the chain before validation runs.
  • Receiving: Validate → Mutate → Process. A validation error MUST prevent mutations from running.
  • Mutations: Sequential by priorityHint (alphabetical tie-break). MUST be atomic — the entire chain succeeds or none apply.
  • Validations: Parallel. Only severity: "error" blocks; "warn" and "info" MUST NOT block.
  • Audit Mode: Interceptors in audit mode MUST NOT block execution regardless of results.
  • failOpen: true: If an interceptor fails (crash/timeout), the message MUST be allowed to proceed.
  • failOpen: false (default): If an interceptor fails, the message MUST be blocked.

The execution model follows a Trust-Boundary-Aware Pattern where validation acts as a security gate.

Trust Boundary Execution Pattern

Execution order depends on data flow direction:

Sending Data (Client → Server or Server → Client):

Mutate (sequential) → Validate (parallel) → Send
  • Mutations MUST run sequentially to prepare/sanitize data
  • Validations MUST run after mutations to verify before crossing boundary

Receiving Data (Server ← Client or Client ← Server):

Receive → Validate (parallel) → Mutate (sequential)
  • Validations MUST run first as a security barrier (can block)
  • Mutations MUST run only after all validations pass

Implementations MUST follow this trust-boundary-aware ordering when executing interceptors to ensure validation guards every boundary crossing.

Interceptor Type Behaviors

Type Execution Failure Behavior Ordering Audit Mode
Mutation Sequential Chain halts, returns last valid state By priorityHint (low→high), then alphabetically Shadow mutations: compute but don't apply
Validation Parallel severity: "error" rejects request None (parallel) Log violations without blocking

Key semantics:

  • Mutations MUST be atomic: the entire chain succeeds or none of the mutations apply
  • Validations with severity: "warn" or "info" MUST NOT block execution
  • Only validations with severity: "error" MUST block execution
  • Interceptors in audit mode MUST NOT block execution regardless of results
Priority Resolution

When ordering mutations, the invoker resolves phase-specific priorities. If the chain entry has overrides.priorityHint, it takes precedence over the interceptor's declared default:

function resolvePriority(
  entry: ChainEntry,
  phase: "request" | "response",
): number {
  const hint = entry.overrides?.priorityHint ?? entry.interceptor.priorityHint;
  if (hint === undefined) return 0;
  if (typeof hint === "number") return hint;
  return hint[phase] ?? 0;
}

Recommended Priority Ranges:

To promote consistency across interceptor implementations, the following priority ranges are RECOMMENDED:

Priority Range Purpose Example Use Cases
-2,000,000,000 to -1,000,000 System-critical security Anti-malware scanning, SQL injection prevention
-999,999 to -10,000 Security sanitization PII redaction, credential scrubbing, XSS filtering
-9,999 to -1,000 Input/output normalization Character encoding, format standardization
-999 to -1 Content transformation Language translation, markdown rendering
0 Default (no priority specified) General-purpose interceptor
1 to 999 Enrichment Metadata injection, tagging, formatting
1,000 to 9,999 Optimization Compression, caching, batching
10,000 to 999,999 Low-priority transformations Response enrichment, metadata injection
1,000,000 to 2,000,000,000 Low-priority finalization Audit stamps, response wrapping

Phase-Specific Priorities:

Different interceptors MAY need different priorities for request vs response phases:

// PII Redactor: Redact early when sending, late when receiving
{
  name: "pii-redactor",
  priorityHint: {
    request: -50000,   // Before other transforms
    response: 50000    // After decompression, decryption
  }
}

// Compression: Compress late when sending, decompress early when receiving
{
  name: "compressor",
  priorityHint: {
    request: 5000,     // After all transforms
    response: -5000    // Before other processing
  }
}
Sequence Diagrams

Client-Side Interceptor Invocation:

MCP ServerValidation(Validator)Mutation 2(Content Filter)Mutation 1(PII Redactor)MCP ClientApplicationMCP ServerValidation(Validator)Mutation 2(Content Filter)Mutation 1(PII Redactor)MCP ClientApplicationCLIENT REQUEST (Sending)STEP 1: Mutations (Sequential)STEP 2: Validation (Parallel)Trust Boundary Check═══ TRUST BOUNDARY ═══CLIENT RESPONSE (Receiving)STEP 1: Validation (Parallel)Trust Boundary CheckSTEP 2: Mutations (Sequential)tools/call requestDiscover interceptor via interceptor/listOrder mutations by priorityinterceptor/invoke(pii-redactor, priority=10){ modified: true, payload: {...} }interceptor/invoke(content-filter, priority=20){ modified: true, payload: {...} }interceptor/invoke(validator){ valid: true }Modified & validated requesttools/call responseinterceptor/invoke(response-validator){ valid: true }interceptor/invoke(response-enhancer, priority=10){ modified: false, payload: {...} }Final validated & processed response

Server-Side Interceptor Invocation:

Tool ImplementationMutation(Sanitizer)Validation(PII Checker)Validation(Schema Validator)MCP ServerMCP ClientTool ImplementationMutation(Sanitizer)Validation(PII Checker)Validation(Schema Validator)MCP ServerMCP ClientSERVER REQUEST (Receiving)═══ TRUST BOUNDARY ═══STEP 1: Validation (Parallel)Trust Boundary Checkpar[Validation after receiving][Validation]STEP 2: Mutations (Sequential)SERVER RESPONSE (Sending)STEP 1: Mutations (Sequential)STEP 2: Validation (Parallel)Trust Boundary Checkpar[Validation before sending][Validation]═══ TRUST BOUNDARY ═══alt[Any validation failed][All validations passed]tools/call requestDiscover interceptor via interceptor/listOrder mutations by priorityinterceptor/invoke(schema-validator){ valid: true }interceptor/invoke(pii-checker){ valid: true }Error: Validation failedinterceptor/invoke(sanitizer, priority=10){ modified: true, payload: {...} }Execute toolTool resultinterceptor/invoke(response-sanitizer, priority=10){ modified: true, payload: {...} }interceptor/invoke(schema-validator){ valid: true }interceptor/invoke(pii-checker){ valid: true }Mutated & validated response

Cross-Boundary Interceptor (Client + Server):

ToolServer Mutation(Sanitizer)Server Validation(Schema/PII Validator)MCP ServerClient Validation(Validator)Client Mutation(PII Redactor)MCP ClientApplicationToolServer Mutation(Sanitizer)Server Validation(Schema/PII Validator)MCP ServerClient Validation(Validator)Client Mutation(PII Redactor)MCP ClientApplicationCLIENT REQUEST (Sending - Trust Boundary)Client STEP 1: Mutations (Sequential)Client STEP 2: Validation (Parallel)Trust Boundary Check═══════════════════════║ TRUST BOUNDARY ║═══════════════════════Server STEP 1: Validation (Parallel)Trust Boundary CheckServer STEP 2: Mutations (Sequential)SERVER RESPONSE (Sending - Trust Boundary)Server STEP 1: Mutations (Sequential)Server STEP 2: Validation (Parallel)Trust Boundary Check═══════════════════════║ TRUST BOUNDARY ║═══════════════════════Client STEP 1: Validation (Parallel)Trust Boundary CheckClient STEP 2: Mutations (Sequential)Validation guards both sides of trust boundarytools/call with PIIinterceptor/invoke(pii-redactor){ modified: true, payload: "[REDACTED]" }interceptor/invoke(validator){ valid: true }Validated requestinterceptor/invoke(schema/pii-validator){ valid: true }interceptor/invoke(sanitizer){ modified: true, payload: {...} }Execute tool safelyResultinterceptor/invoke(response-sanitizer){ modified: true, payload: {...} }interceptor/invoke(schema-validator){ valid: true }Validated responseinterceptor/invoke(response-validator){ valid: true }interceptor/invoke(response-enhancer){ modified: false, payload: {...} }Final validated result
Chain Execution

Chain execution is a convenience utility, provided by SDKs to enforce the execution model defined above. Its purpose is to execute interceptors — potentially hosted across N MCP servers — in a way that enforces the trust-boundary-aware ordering, type-specific execution semantics, and failure behaviors.

Important: The execution model defined above MUST be followed by all implementations, whether invoking interceptors individually via interceptor/invoke or using this convenience utility.

Capability vs Policy

The chain distinguishes between two layers of interceptor configuration:

  • Capability (server-declared): The interceptor's type, hooks, and configSchema as advertised via interceptors/list. These describe what the interceptor can do.
  • Policy (invoker-declared): Execution parameters such as failOpen, priorityHint, mode, timeoutMs, and hook narrowing that the invoker provides via overrides on each chain entry. These describe how the interceptor should behave in a specific deployment.

Interceptor servers declare defaults for policy fields (failOpen, priorityHint, mode) alongside their capabilities. Invokers MAY provide overrides per-interceptor in the chain to adjust execution policy. When an override is present, it takes precedence over the server-declared default. Hook overrides can only narrow the set of events (restrict to a subset of declared hooks), never widen (add events the interceptor does not declare). Implementations MUST reject override hooks that reference events not present in the interceptor's declared hooks.

This separation enables:

  • Same interceptor running in different modes across deployments (audit in staging, active in production)
  • Platform teams adjusting failOpen and timeoutMs based on SLO requirements without modifying interceptor servers
  • Fine-grained routing via hook narrowing without server-side changes
Orchestration

The orchestration pattern is as follows:

  1. Discover: Call interceptors/list on one or more MCP servers to collect all registered interceptors.
  2. Resolve Policy: For each chain entry, merge overrides with interceptor defaults (overrides take precedence). Filter entries whose override hooks exclude the current event/phase.
  3. Sort: Order mutations by resolved priorityHint (ascending, with alphabetical tie-breaking by interceptor name).
  4. Order by Trust Boundary: Apply the trust-boundary-aware execution model — mutations before validations when sending, validations before mutations when receiving.
  5. Execute: Call interceptor/invoke on the appropriate MCP server for each interceptor, applying resolved failOpen, mode, and timeoutMs. Mutations MUST be invoked sequentially (each receiving the output of the previous). Validations MAY be invoked in parallel.
  6. Aggregate: Collect all results, assembling the final payload and validation summary.

SDK libraries are expected to provide reference implementations of this orchestration logic so that individual applications do not need to implement it from scratch.

Interceptor Chain

The chain is constructed from interceptor entries, each describing an interceptor and the server that hosts it:

interface InterceptorChain {
  /**
   * All interceptor entries in the chain, collected from one or more servers.
   * Entries are merged and ordered by the chain
   * according to the execution model.
   */
  entries: ChainEntry[];

  /**
   * Execute the chain for a given event and phase.
   */
  execute(params: ChainExecutionParams): ChainExecutionResult;
}

interface ChainEntry {
  /**
   * The interceptor descriptor (as returned by interceptors/list).
   * Contains the interceptor's capabilities and author-declared defaults.
   */
  interceptor: {
    name: string;
    type: "mutation" | "validation";
    hooks: Array<{
      events: InterceptionEvent[];
      phase: "request" | "response";
    }>;
    priorityHint?: number | { request?: number; response?: number };
    mode?: "active" | "audit";
    failOpen?: boolean;
  };

  /**
   * The MCP server that hosts this interceptor.
   * Used to route interceptor/invoke calls to the correct server.
   * The concrete type is implementation-specific.
   */
  server: MCPServerConnection;

  /**
   * Optional invoker-side overrides for this interceptor's execution policy.
   * Fields present here take precedence over the interceptor's declared defaults.
   */
  overrides?: InterceptorOverrides;
}

interface InterceptorOverrides {
  /**
   * Override failure routing policy for this interceptor.
   * Takes precedence over the interceptor's declared failOpen default.
   */
  failOpen?: boolean;

  /**
   * Override mutation ordering priority.
   * Takes precedence over the interceptor's declared priorityHint default.
   * Ignored for validation interceptors.
   */
  priorityHint?: number | { request?: number; response?: number };

  /**
   * Override execution mode.
   * Takes precedence over the interceptor's declared mode default.
   * - "active": Normal blocking / transforming behavior
   * - "audit": Non-blocking operation (log-only for validators, shadow for mutators)
   */
  mode?: "active" | "audit";

  /**
   * Per-interceptor timeout in milliseconds.
   * If exceeded, the interceptor is cancelled and the resolved failOpen
   * determines whether the message proceeds or is blocked.
   */
  timeoutMs?: number;

  /**
   * Narrow the interceptor's declared hooks.
   * Each entry MUST be a subset of the interceptor's declared hooks.
   * If provided, the interceptor is only invoked for matching events/phases
   * from this narrowed set rather than its full declared hooks.
   * Implementations MUST reject overrides that widen beyond declared hooks.
   */
  hooks?: Array<{
    events: InterceptionEvent[];
    phase: "request" | "response";
  }>;
}
ChainExecutionParams
interface ChainExecutionParams {
  /**
   * Lifecycle event (hook) and phase for this chain
   */
  event: InterceptionEvent;
  phase: "request" | "response";

  /**
   * The payload to pass through the interceptor chain
   */
  payload: unknown;

  /**
   * Optional: restrict chain to specific interceptors by name
   */
  interceptors?: string[];

  /**
   * Optional: per-interceptor configuration overrides
   */
  config?: Record<string, Record<string, unknown>>;

  /**
   * Optional: timeout for the entire chain in milliseconds
   * implementations MUST cancel the entire chain if the aggregate timeout is exceeded.
   */
  timeoutMs?: number;

  /**
   * Optional: context passed to all interceptors
   */
  context?: {
    principal?: {
      type: "user" | "service" | "anonymous";
      id?: string;
    };
    traceId?: string;
    timestamp: string;
  };
}
ChainExecutionResult
interface ChainExecutionResult {
  /**
   * Overall chain execution status
   */
  status: "success" | "validation_failed" | "mutation_failed" | "timeout";

  /**
   * Lifecycle event (hook) and phase for this chain
   */
  event: InterceptionEvent;
  phase: "request" | "response";

  /**
   * Results from all executed interceptors
   */
  results: Array<{
    interceptor: string;
    type: "mutation" | "validation";
    phase: "request" | "response";
    // Mutation-specific fields
    modified?: boolean;
    payload?: unknown;
    // Validation-specific fields
    valid?: boolean;
    severity?: "error" | "warn" | "info";
    messages?: Array<{ message: string; severity: "error" | "warn" | "info" }>;
    // Audit mode
    mode?: "audit";
    info?: Record<string, unknown>;
    // Timing
    durationMs: number;
  }>;

  /**
   * Final payload after all mutations (if chain completed)
   */
  finalPayload?: unknown;

  /**
   * Validation summary
   */
  validationSummary: {
    errors: number;
    warnings: number;
    infos: number;
  };

  /**
   * Total execution time
   */
  totalDurationMs: number;

  /**
   * If chain was aborted, details about where and why
   */
  abortedAt?: {
    interceptor: string;
    reason: string;
    type: "validation" | "mutation" | "timeout";
  };
}
Example Interceptor Chain

For a tools/call event, assume the following interceptor are available:

// From interceptor/list response:
[
  {
    name: "pii-redactor",
    type: "mutation",
    hooks: [
      { events: ["tools/call", "llm/completion"], phase: "request" },
      { events: ["tools/call", "llm/completion"], phase: "response" },
    ],
    priorityHint: {
      request: -1000, // Run first when sending
      response: 1000, // Run last when receiving
    },
  },
  {
    name: "content-filter",
    type: "mutation",
    hooks: [
      { events: ["llm/completion"], phase: "request" },
      { events: ["llm/completion"], phase: "response" },
    ],
    priorityHint: -500, // Same priority for both phases
  },
  {
    name: "format-normalizer",
    type: "mutation",
    hooks: [
      { events: ["tools/call"], phase: "request" },
      { events: ["tools/call"], phase: "response" },
    ],
    priorityHint: { request: 100 }, // Only applies to requests, response uses default (0)
  },
  {
    name: "schema-validator",
    type: "validation",
    hooks: [{ events: ["tools/call"], phase: "request" }],
    // No priorityHint - runs in parallel with other validators
  },
  {
    name: "parameter-validator",
    type: "validation",
    hooks: [{ events: ["tools/call"], phase: "request" }],
    // No priorityHint - runs in parallel
  },
  {
    name: "audit-logger",
    type: "validation",
    hooks: [
      { events: ["*"], phase: "request" },
      { events: ["*"], phase: "response" },
    ],
    mode: "audit",
    // No priorityHint - runs in parallel with other validators
    // Audit mode means violations logged but don't block
  },
];
Example: Chain Entries with Overrides

The following example shows how invoker-side overrides adjust execution policy without modifying interceptor servers:

// Chain entries with invoker overrides applied:
[
  {
    interceptor: {
      name: "pii-redactor",
      type: "mutation",
      hooks: [
        { events: ["tools/call", "llm/completion"], phase: "request" },
        { events: ["tools/call", "llm/completion"], phase: "response" },
      ],
      priorityHint: { request: -1000, response: 1000 },
      failOpen: false,
    },
    server: serverA,
    overrides: {
      // Override priority for this deployment
      priorityHint: { request: -2000, response: 2000 },
      // Set a per-interceptor timeout
      timeoutMs: 3000,
      // Narrow hooks: only intercept tools/call, not llm/completion
      hooks: [
        { events: ["tools/call"], phase: "request" },
        { events: ["tools/call"], phase: "response" },
      ],
    },
  },
  {
    interceptor: {
      name: "audit-logger",
      type: "validation",
      hooks: [
        { events: ["*"], phase: "request" },
        { events: ["*"], phase: "response" },
      ],
      mode: "audit",
      failOpen: true,
    },
    server: serverB,
    overrides: {
      // Promote from audit to active in production
      mode: "active",
    },
  },
  {
    interceptor: {
      name: "schema-validator",
      type: "validation",
      hooks: [{ events: ["tools/call"], phase: "request" }],
      failOpen: false,
    },
    server: serverC,
    overrides: {
      // Known fragile interceptor: fail-open in this deployment
      failOpen: true,
      timeoutMs: 2000,
    },
  },
];

In this example:

  • pii-redactor runs with higher priority and tighter timeout than the author declared, and only for tools/call (not llm/completion)
  • audit-logger is promoted from audit mode to active, making its violations blocking
  • schema-validator is configured fail-open with a short timeout because the invoker knows it can be unreliable
Full Request/Response Cycle
CLIENT REQUEST (Sending):
  Original: { email: "john@example.com", ssn: "123-45-6789" }
  → Mutate: pii-redactor (-1000), content-filter (-500), format-normalizer (100)
  → Result: { email: "[EMAIL]", ssn: "[SSN]" }
  → Validate & Observe: client-validator ✓, audit-logger ✓
  → Send across TRUST BOUNDARY

SERVER REQUEST (Receiving):
  Received: { email: "[EMAIL]", ssn: "[SSN]" }
  → Validate & Observe: schema-validator ✓, parameter-validator ✓, audit-logger ✓
  → Mutate: sanitizer (10)
  → Execute Tool → { result: { name: "John", ... } }

SERVER RESPONSE (Sending):
  Original: { name: "John", ... }
  → Mutate: pii-redactor (1000), content-filter (-500)
  → Validate & Observe: response-validator ✓, audit-logger ✓
  → Send across TRUST BOUNDARY

CLIENT RESPONSE (Receiving):
  Received: { name: "John", ... }
  → Validate & Observe: response-validator ✓, audit-logger ✓
  → Mutate: response-enhancer (10)
  → Deliver to Application
Error Handling

Interceptor chains MUST handle errors based on type:

// Mutation failure: Chain MUST halt immediately
{
  error: {
    code: -32603,
    message: "Interceptor mutation failed",
    data: { failedInterceptor: "content-filter", lastValidPayload: {...} }
  }
}
// Validation failure: All validations MUST complete, then reject if any has severity: "error"
{
  error: {
    code: -32602,
    message: "Interceptor validation failed",
    data: {
      validationErrors: [
        { interceptor: "schema-validator", severity: "error", message: "Required field missing" }
      ]
    }
  }
}

Interceptor Server Capabilities

During initialization, servers declare interceptor support:

{
  jsonrpc: "2.0",
  id: 1,
  result: {
    protocolVersion: "2025-06-18",
    capabilities: {
      extensions: {
        "io.modelcontextprotocol/interceptors": {
          // Events this server's interceptors can handle
          supportedEvents: InterceptionEvent[];
        }
      }
      // ...existing capabilities
    },
    serverInfo: {
      name: "example-server",
      version: "1.0.0"
    }
  }
}

Future Enhancements

Context Propagation and Interceptor State (Future)

Status: Reserved for future specification

Inspired by gRPC interceptors and OpenTelemetry context propagation, future versions may support enrichable interceptor state that flows through the entire chain. This would enable:

  • State Sharing: Early interceptor can enrich context for downstream interceptor
  • Cross-Cutting Data: Authentication, rate limiting, and audit data flows through chain
  • Trace Correlation: Distributed tracing baggage propagates automatically
  • Deadline Propagation: Timeout context flows through nested operations

Proposed Model:

// FUTURE: Enhanced context with state propagation
interface InterceptorChainContext {
  // Immutable request context (set by initiator, read-only)
  readonly request: {
    principal?: {
      type: "user" | "service" | "anonymous";
      id?: string;
      claims?: Record<string, unknown>;
    };
    traceId: string;
    spanId?: string;
    timestamp: string;
    sessionId?: string;
  };

  // Mutable interceptor state (enriched by interceptor during chain execution)
  // Each interceptor can read and write to this shared state
  interceptorState?: Record<string, unknown>;

  // Deadline for entire operation (inherited and propagated)
  deadline?: string; // ISO 8601 timestamp

  // Distributed tracing baggage (key-value pairs for trace correlation)
  // Follows W3C Baggage specification
  baggage?: Record<string, string>;
}

Interceptor State Enrichment:

Mutation and validation interceptors could optionally return context updates:

// FUTURE: MutationResult extended with context enrichment
interface MutationResult {
  interceptor: string;
  type: "mutation";
  phase: "request" | "response";
  durationMs?: number;
  info?: Record<string, unknown>;
  modified: boolean;
  payload: unknown;

  // Optional: Updates to propagate to downstream interceptors
  contextUpdates?: {
    interceptorState?: Record<string, unknown>;
    baggage?: Record<string, string>;
  };
}

Example Flow:

// Interceptor 1: Authentication enriches context
{
  interceptor: "authenticator",
  type: "mutation",
  modified: false,
  payload: { /* unchanged */ },
  contextUpdates: {
    interceptorState: {
      principal: {
        id: "user-123",
        roles: ["admin"],
        authMethod: "oauth2"
      }
    }
  }
}

// Interceptor 2: Rate limiter reads enriched context
// Has access to context.interceptorState.principal
{
  interceptor: "rate-limiter",
  type: "validation",
  valid: true,
  info: {
    rateLimitUsed: 45,
    rateLimitMax: 100,
    // Used principal.id from context for rate limiting
  },
  contextUpdates: {
    interceptorState: {
      rateLimit: { remaining: 55, resetAt: "2025-11-04T15:30:00Z" }
    }
  }
}

// Interceptor 3: Audit logger has full enriched context
// Logs with principal + rate limit info + original request
// Runs in audit mode (non-blocking)
{
  interceptor: "audit-logger",
  type: "validation",
  mode: "audit",
  valid: true,
  severity: "info",
  info: {
    logEntry: {
      user: "user-123",
      roles: ["admin"],
      rateLimitRemaining: 55,
      action: "tools/call",
      timestamp: "2025-11-04T15:29:00Z"
    }
  }
}

Benefits of State Propagation:

  1. Loose Coupling: Interceptor don't need direct dependencies
  2. Composability: New interceptor can leverage existing enriched state
  3. Audit and Logging: Full context available for audit interceptors to log and trace
  4. Performance: Avoid redundant operations (e.g., auth check once, reuse result)

Design Considerations:

  • Immutability: Request context is read-only to prevent tampering
  • Namespace Conflicts: Interceptor should use namespaced keys (e.g., auth.principal, rateLimit.info)
  • Size Limits: Context should have reasonable size limits to prevent abuse
  • Serialization: Context must be JSON-serializable for transport

This feature would be opt-in and backward compatible, with interceptor that don't support context enrichment working unchanged.

Rationale

Design Decisions

1. Separation of Validation and Mutation

We explicitly separate validation and mutation interceptor types rather than combining them because:

  • Clear intent: Users know immediately what an interceptor does
  • Simpler implementation: Each type has focused logic
  • Better error handling: Validation failures vs mutation failures have different semantics
  • Performance: Validations can run in parallel, mutations run sequentially
  • Clear execution model: Validation guards boundaries, mutations transform data

Audit Mode for Both Types

Instead of a separate observability type, both validators and mutators support audit mode:

  • Validators in audit mode: Log violations without blocking (non-blocking observability)
  • Mutators in audit mode: Compute transformations without applying them (shadow mutations for testing)

This approach is simpler and more flexible than a separate observability type

2. Validation as Trust Boundary Guard

Execution order is trust-boundary-aware:

  • Sending: Mutate → Validate → Send (prepare data, verify before crossing)
  • Receiving: Validate → Mutate → Process (security barrier, then process)

This ensures validation guards all boundary crossings, enabling zero-trust architecture and future cryptographic verification. Alternative (same order for all phases) rejected because validation must be first when receiving untrusted data.

3. Phase-Aware Priority Hints

Mutations declare priorityHint (default: 0) which can differ by phase. Benefits:

  • Flexibility: PII redaction runs early when sending (-50000), late when receiving (+50000)
  • Simplicity: Single number for common cases, object for phase-specific needs
  • Determinism: Alphabetical tie-breaking by name ensures consistency
  • Wide range: 32-bit integers align with Kubernetes standards

Alternative (global fixed ordering) rejected as too rigid for phase-aware operations like compression/decompression.

4. Separation of Capability and Policy

The chain separates interceptor capability (what the server advertises) from execution policy (what the invoker configures via overrides):

  • Deployment flexibility: The same interceptor can run in audit mode in staging and active mode in production without server changes
  • SLO adaptation: Platform teams adjust failOpen and timeoutMs per-deployment based on reliability requirements
  • Routing control: Hook narrowing lets invokers restrict which events reach an interceptor without server reconfiguration
  • Author defaults respected: Server-declared values remain meaningful as recommended configuration; overrides are opt-in

Alternative (policy only at server side) rejected because enterprise deployments need invoker-side control over execution behavior without coupling to interceptor authors' choices. Alternative (no server defaults) rejected because it forces every invoker to fully configure every interceptor.

5. Other Design Choices

  • Hook-Based Targeting: Interceptors declare specific hooks (vs. all traffic) for performance and security
  • Severity Levels (info/warn/error): Graduated response vs. binary pass/fail enables audit logging without blocking
  • Replace vs. Patch: Mutations replace entire payloads (vs. JSON Patch) for simplicity and atomicity
  • Method Names: interceptor/list and interceptor/invoke mirror MCP patterns (tools/list, etc.)
  • "info" Field: Used instead of "metadata" to avoid confusion with MCP's _meta protocol metadata
  • Cross-Boundary Support: Client and server interceptors enable defense-in-depth and zero-trust architecture

Inspired By:

  • Kubernetes Admission Controllers: Validation/mutation webhooks (MCP uses replace vs. patch for simplicity)
  • gRPC Interceptors: Client/server-side interception (MCP adds discoverability and dynamic invocation)
  • HTTP Middleware: Express.js, ASP.NET patterns (MCP is event-specific vs. path-based)
  • Envoy Filters: Network-level request/response filtering

Complements Existing MCP:

  • Adds validation/mutation capabilities to sampling, tools, prompts, and resources
  • Enables parameter validation, access control, and content filtering across all MCP features

Backward Compatibility

This SEP is fully backward compatible:

  1. Optional Capability: Interceptors are an OPTIONAL server capability. Servers without interceptor support MUST continue to work unchanged.

  2. No Protocol Changes: Existing JSON-RPC methods MUST NOT be modified. Interceptors add new methods (interceptors/list, interceptors/invoke) without changing existing ones.

  3. Client Compatibility: Clients that do not support interceptors MUST be able to communicate with interceptor-enabled servers. Interceptors MAY run transparently on the server side.

  4. Graceful Degradation: If a client calls interceptors/list on a server that does not support interceptors, the server MUST return a standard JSON-RPC error.

  5. Version Negotiation: The existing protocol version negotiation during initialization SHOULD be used to discover interceptor support.

Security Implications

Threat Model

1. Malicious Interceptor

A compromised interceptor could:

  • Exfiltrate sensitive data from requests/responses
  • Modify data to inject malicious content
  • Cause denial of service by rejecting all requests
  • Bypass security controls by approving invalid inputs

Mitigations:

  • Interceptor runs in the same trust domain as the server/client
  • Organizations must vet interceptor before deployment
  • Interceptor should be sandboxed where possible
  • Audit logging of all interceptor invocations
  • Cryptographic signing of interceptor for integrity

2. Interceptor Bypass

Attackers might try to bypass interceptor validation:

  • Send requests directly to underlying services
  • Exploit interceptor chain ordering

Mitigations:

  • Interceptor enforcement must be at the transport layer
  • Server-side interceptor is mandatory for requests entering the trust boundary
  • Interceptor chain order must be explicitly configured and immutable

3. Information Disclosure

Interceptor error messages might leak sensitive information:

  • Internal system details
  • Validation rules that could be reverse-engineered
  • PII in error messages

Mitigations:

  • Interceptor should use generic error messages
  • Detailed errors should only be logged, not returned to untrusted clients
  • Separate internal and external validation messages

4. Denial of Service

Interceptor could be exploited for DoS:

  • Computationally expensive validation
  • Infinite loops in mutation logic
  • Memory exhaustion from large payloads

Mitigations:

  • Timeouts for interceptor execution
  • Resource limits (CPU, memory)
  • Rate limiting on interceptor invocations
  • Circuit breakers for failing interceptor

5. Privilege Escalation

Interceptor mutation could be used to inject unauthorized operations:

  • Modifying request parameters to access unauthorized resources
  • Changing tool invocation targets
  • Injecting administrative commands

Mitigations:

  • Validation interceptor should verify request authorization at trust boundaries
  • Immutable fields should be enforced at protocol level
  • Audit all mutations that touch security-sensitive fields

Security Best Practices

  1. Principle of Least Privilege: Interceptors SHOULD only have access to the data they need to process

  2. Defense in Depth: Interceptors SHOULD be deployed at both client and server boundaries

  3. Audit Everything: Implementations SHOULD log all interceptor invocations, including payloads (with PII redaction)

  4. Fail Secure: Interceptors MUST default to failOpen: false (fail-closed). Implementations MUST reject requests when interceptors fail unless explicitly configured otherwise.

  5. Input Validation: Interceptor configuration and payloads MUST be validated before processing

  6. Cryptographic Integrity: Implementations SHOULD consider signing interceptor responses to prevent tampering

  7. Isolation: Untrusted interceptors SHOULD be run in sandboxed environments

Compliance Considerations

Interceptors enable compliance with:

  • GDPR: PII detection and redaction
  • HIPAA: PHI filtering in healthcare contexts
  • SOC 2: Audit logging and access control
  • PCI DSS: Sensitive data masking

However, organizations MUST ensure:

  • Interceptors do not create new compliance violations (e.g., storing PII in logs)
  • Audit trails are tamper-proof
  • Data retention policies apply to interceptor logs

Reference Implementation

The reference implementation will provide:

Multi-Language SDKs

SDK libraries for writing and running interceptors in various languages, including Python, TypeScript/JavaScript, Go and other community-contributed languages. These SDKs will provide base classes, helpers, and patterns for building validation and mutation interceptors, with built-in support for audit mode operation.

Common Interceptor Sidecar

A universal interceptor runtime that enables teams to deploy and manage interceptors without modifying individual MCP servers. The sidecar acts as an MCP proxy that loads custom interceptors (both local and remote) via configuration and injects interceptor invocations transparently.

Example Configuration:

# interceptor-config.yaml
interceptors:
  # Local interceptor: runs in-process
  - name: pii-redactor
    type: mutation
    transport: local
    command: ./interceptors/pii-redactor.py
    args: []
    config:
      patterns: ["email", "ssn", "phone"]

  # Remote interceptor: calls external service
  - name: security-scanner
    type: validation
    transport: remote
    endpoint: https://security.example.com/scan
    timeout: 5s
    headers:
      Authorization: "Bearer ${SECURITY_TOKEN}"

  # Local TypeScript interceptor
  - name: rate-limiter
    type: validation
    transport: local
    command: ./interceptors/rate-limiter.js
    args: []
    config:
      requests_per_minute: 100

routing:
  # Apply interceptors to specific lifecycle event types
  tools.call:
    request: [rate-limiter, security-scanner]
    response: [pii-redactor]

  llm.completion:
    request: [security-scanner]
    response: [pii-redactor]

This approach enables platform teams to deploy guardrails across all services from a central control plane without code changes to existing MCP servers.

Acknowledgments

This proposal draws inspiration from:

SEP-2631 File Objects and Transfer

Draft · Standards Track · Created 2026-04-20

Imported from upstream. Proposed as modelcontextprotocol#2631 by @caseychow-oai on 2026-04-22, 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 extends SEP-2356 with out-of-band file transfer and generated file outputs. SEP-2356 defines x-mcp-file, an inline JSON Schema extension keyword for declaring file-valued fields, and uses URI strings, including RFC 2397 data: URIs, as the baseline input value while keeping URL-mode elicitation as the complementary path for server-controlled large-file flows. This SEP adds file-transfer URIs, capability negotiation for transfer modes, and control-plane methods for preparing uploads and resolving downloads. It also defines content integrity metadata for transferred files.

Under this proposal, clients can still satisfy SEP-2356 file inputs with data: URIs. When inline transfer is undesirable, clients can instead prepare an upload, receive a file URI, upload bytes out of band, and pass that file URI string in the same file-valued tool argument or elicitation response field. Servers can also return file URI metadata in tool results or resource reads so generated artifacts can be downloaded without forcing bytes through JSON-RPC.

Motivation

MCP currently has partial building blocks for file-like workflows, but no clean, standardized end-to-end story for "the user gives a file to a tool" or "a tool returns a file to the user":

  • resources/read and ContentBlock support server-to-client retrieval patterns, but they do not define an out-of-band file transfer path for large or sensitive content.
  • SEP-2356 defines declarative file inputs and inline data: URI values, but not upload negotiation, generated file outputs, or download resolution.
  • Tool arguments are arbitrary JSON, which has led implementations toward ad hoc file parameter conventions, hosted-storage handles, or inline base64.

This gap creates several ecosystem problems:

  • File upload contracts differ between clients, gateways, and servers.
  • Inline base64 becomes the de facto fallback, even for large or sensitive files.
  • User experience depends on harness-specific argument rewriting rather than protocol semantics.
  • Server authors cannot reliably declare "this tool takes a file" in a way that works across local and hosted environments.

The design target for this SEP is a minimal interoperable baseline:

  • A server can use SEP-2356 declarations to identify file-valued tool arguments and elicitation fields.
  • A client can supply those files either as inline data: URIs or as negotiated file URI strings.
  • A server can return a file in a result without forcing bytes into the model-visible contract.

Goals

File Input/Output Declaration and Representation

  • Reuse SEP-2356 x-mcp-file declarations for file-valued tool and elicitation inputs.
  • Extend the SEP-2356 URI-string input contract with out-of-band file URI values.
  • Standardize the representation of file-valued outputs so generated or transformed artifacts can be downloaded, opened, saved, or reused predictably.
  • Keep file identity and metadata in the JSON-RPC control plane while allowing file bytes to move through a separate transfer path.

File Transfer Mechanism Negotiation

  • Standardize file upload and download in a way that supports both SEP-2356 inline data: URI values and negotiated out-of-band HTTP upload, including multipart form uploads.
  • Allow both inline data: URI transfer and out-of-band HTTP transfer as interoperable options, with per-input transfer mode control when a server needs to require one.
  • Verify optional content integrity metadata for uploaded or downloaded files when it is provided.

Non-Functional

  • Preserve compatibility with stateless MCP directions under discussion, especially work to remove mandatory initialization/session coupling and to support multi round-trip request flows without assuming long-lived server-side session state. This goal is aligned with SEP-2575 and SEP-2322.
  • Support file exchange in hosted, browser-based, local, and gateway-mediated environments.
  • Preserve compatibility with existing MCP primitives such as tools/call, elicitation/create, resources/read, and ResourceLink where they are useful.
  • Avoid requiring any specific backing store, filesystem model, upload service, or deployment topology.

Non-Goals

  • Making inline base64 transport the primary protocol contract for files.
  • Standardizing one universal storage backend, object store, or artifact service.
  • Solving every artifact lifecycle concern in v1, such as retention, version history, sharing, or cross-session persistence semantics.

Specification

1. Design Principles

This SEP defines the following principles:

  1. Generic MCP file support MUST work across hosted, browser-based, local, and gateway-mediated environments.
  2. File-valued inputs MUST preserve SEP-2356's URI-string value contract.
  3. Clients MAY source files from local filesystems, browser uploads, app-owned storage, cloud drives, or any other user-mediated surface.
  4. File collection SHOULD happen before the main request is sent when practical, while preserving URL-mode elicitation as a fallback when inline or negotiated upload is not appropriate.
  5. File-valued inputs SHOULD be declared through SEP-2356 x-mcp-file annotations on URI string schema properties.
  6. A usable file URI MUST identify one immutable byte sequence for its usable lifetime. Changed bytes require a new file URI.

2. Capabilities

Clients that support out-of-band file transfer declare a new files capability during initialization:

{
  "capabilities": {
    "files": {
      "upload": true,
      "download": true,
      "transports": ["https"]
    }
  }
}

Capability fields:

  • upload: the client can upload file bytes out-of-band and use file URI values for SEP-2356 file-valued inputs.
  • download: the client can download file-valued outputs out-of-band.
  • transports: supported out-of-band transport families. This SEP initially defines https, including multipart form uploads.

3. Existing and New Shapes

This proposal intentionally reuses existing MCP envelopes where they already define the right protocol surface:

  • tools/call continues to carry tool arguments in params.arguments.
  • CallToolResult.content continues to use ContentBlock[].
  • structuredContent remains the structured tool-result channel.
  • resources/read continues to return resource contents keyed by resource uri.
  • elicitation/create form mode continues to use requestedSchema and form response content.

SEP-2356 defines this input declaration shape, which this SEP reuses:

  • x-mcp-file: an inline JSON Schema extension keyword whose value is a FileInputDescriptor.

This SEP adds new file-transfer and output shapes and threads them through existing surfaces:

  • file URI strings: an out-of-band value mode for SEP-2356 x-mcp-file fields.
  • FileValue: metadata for generated or returned files identified by file URI.
  • FileDigest: content integrity metadata for a complete file byte sequence.
  • AuthorizedFile: a shared parent result shape for files/authorize* methods that carries a FileValue plus an optional eager download authorization.
  • FileContent: a new ContentBlock variant that wraps a FileValue.
  • FileResourceContents: a new resources/read resource-content variant for file-backed resources.
  • FileTransferDescriptor: the upload/download descriptor for the out-of-band data plane.
  • files/authorizeUpload and files/authorizeDownload: control-plane methods for resolving upload and download transfer descriptors.

4. File Input Values and File Outputs

SEP-2356 file inputs remain URI strings. A file-valued tool argument or elicitation response field contains either:

  • a data: URI as defined by SEP-2356; or
  • a file URI prepared through files/authorizeUpload.

Inline input continues to use SEP-2356's RFC 2397 data: URI form:

"data:text/plain;base64,SGVsbG8gd29ybGQh"

Out-of-band input uses a file URI string:

"mcp-file://server/file_01HXYZ"

For generated files and file content blocks, this SEP introduces FileValue: file URI plus optional display and integrity metadata.

{
  "uri": "mcp-file://server/file_01HYZA",
  "name": "report.pdf",
  "mimeType": "application/pdf",
  "size": 248123,
  "digest": {
    "algorithm": "sha-256",
    "value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU"
  }
}

files/authorize* methods return an AuthorizedFile parent shape that carries the stable FileValue in parallel with any eager download authorization:

{
  "file": {
    "uri": "mcp-file://server/file_01HYZA",
    "name": "report.pdf",
    "mimeType": "application/pdf",
    "size": 248123,
    "digest": {
      "algorithm": "sha-256",
      "value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU"
    }
  },
  "download": {
    "transport": "https",
    "method": "GET",
    "url": "https://download.example.com/...",
    "expiresAt": "2026-04-20T18:45:00Z"
  }
}

The uri identifies a file handle, but does not imply the file is available via resources/read.

URI Namespaces

This SEP uses URIs for both resources and files, but they have different resolution contracts.

A resource URI identifies resource contents and is resolved through resources/read. A file URI identifies a file transfer handle and is resolved through files/authorizeDownload, or through the upload descriptor returned by files/authorizeUpload.

Implementations SHOULD use distinct URI schemes or authorities for resource URIs and file URIs. Examples in this SEP use mcp-resource: for resource URIs and mcp-file: for file URIs.

Clients MUST NOT assume that a file URI can be passed to resources/read. Servers MUST NOT require clients to treat file URIs as resources.

Rules:

  • Inline input transfer MUST use SEP-2356 data: URIs.
  • Out-of-band input transfer MUST use file URI strings prepared through files/authorizeUpload.
  • Clients MAY decide whether to send a file as a data: URI or via out-of-band upload when the field's transferModes is omitted.
  • Clients MUST use one of the listed modes when transferModes is present.
  • Clients MUST use a data: URI when transferModes is ["inline"].
  • Clients MUST use files/authorizeUpload and send the returned file URI when transferModes is ["upload"].
  • Servers that returned a file URI from files/authorizeUpload MUST accept that URI in an x-mcp-file field where they would otherwise accept a data: URI, subject to the same declared accept and maxSize constraints.
  • Servers MUST validate and either accept or reject file URI values using their normal request validation rules.

In pseudocode:

type FileInputValue = string; // data: URI or file URI

interface FileValue {
  uri: string;
  name?: string;
  mimeType?: string;
  size?: number;
  digest?: FileDigest;
}

interface FileDigest {
  algorithm: string;
  value: string; // base64url without padding
}

interface AuthorizedFile {
  file: FileValue;
  download?: FileTransferDescriptor;
}

Implementations that produce or verify digests MUST support sha-256. A digest describes the complete immutable byte sequence identified by the file URI.

5. Declaring File-Valued Inputs

Tools and elicitation forms declare file-valued fields using SEP-2356's x-mcp-file keyword on URI string schema properties. The keyword identifies which schema properties should be treated as file inputs while preserving the value shape as a URI string.

A tool with a single file input can be declared as:

{
  "name": "analyze_document",
  "inputSchema": {
    "type": "object",
    "properties": {
      "document": {
        "type": "string",
        "format": "uri",
        "description": "The document to analyze.",
        "x-mcp-file": {
          "accept": ["application/pdf"],
          "maxSize": 10485760,
          "transferModes": ["inline", "upload"]
        }
      }
    },
    "required": ["document"]
  }
}

For multiple files:

{
  "name": "compare_documents",
  "inputSchema": {
    "type": "object",
    "properties": {
      "documents": {
        "type": "array",
        "items": {
          "type": "string",
          "format": "uri",
          "x-mcp-file": {
            "accept": ["application/pdf", "text/plain"],
            "maxSize": 10485760,
            "transferModes": ["upload"]
          }
        },
        "minItems": 2
      }
    },
    "required": ["documents"]
  }
}

Elicitation requests use the same descriptor shape:

{
  "method": "elicitation/create",
  "params": {
    "mode": "form",
    "message": "Select a document to analyze.",
    "requestedSchema": {
      "type": "object",
      "properties": {
        "document": {
          "type": "string",
          "format": "uri",
          "title": "Document",
          "x-mcp-file": {
            "accept": ["application/pdf"],
            "maxSize": 10485760,
            "transferModes": ["inline", "upload"]
          }
        }
      },
      "required": ["document"]
    }
  }
}

This SEP supports elicitation by allowing the eventual value for an x-mcp-file field to be either a SEP-2356 data: URI or, when out-of-band transfer is used, a file URI prepared through files/authorizeUpload. It does not add a new elicitation mode or change the surrounding elicitation control flow.

x-mcp-file MUST appear only where SEP-2356 allows it: on a URI string schema property. On the tool surface, array items can carry x-mcp-file because Tool.inputSchema is general JSON Schema. On the elicitation surface, file inputs are single URI string fields unless the elicitation schema is separately extended to support arrays.

File input descriptors MAY include:

  • accept: a list of MIME types or dot-prefixed file extensions. As in SEP-2356, extension entries are picker hints only; server-side validation compares the media type portion of the submitted URI value.
  • maxSize: maximum accepted size in bytes for each individual file. For inline data: URI values this is the decoded byte size; for file URI values this is the uploaded file byte size enforced during upload negotiation and subsequent request validation.
  • transferModes: optional list of allowed transfer modes for the field. If omitted, the client can choose any supported mode. "inline" allows a data: URI. "upload" allows files/authorizeUpload and a file URI.

As in the latest SEP-2356 draft, filenames are not carried in a name= parameter on the data: URI. Servers that need the original filename SHOULD declare a separate string field for it.

Clients that do not support any allowed transfer mode MUST reject tools or elicitation requests that require file-valued fields unless another interoperable non-file path is available.

6. Upload Negotiation

When a client chooses out-of-band transfer for a file in tools/call or elicitation, it calls files/authorizeUpload. The client SHOULD include the expected digest when it already knows it:

{
  "jsonrpc": "2.0",
  "id": 10,
  "method": "files/authorizeUpload",
  "params": {
    "name": "report.pdf",
    "mimeType": "application/pdf",
    "size": 248123,
    "digest": {
      "algorithm": "sha-256",
      "value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU"
    }
  }
}

The server responds with an upload authorization:

{
  "jsonrpc": "2.0",
  "id": 10,
  "result": {
    "file": {
      "uri": "mcp-file://server/file_01HXYZ",
      "name": "report.pdf",
      "mimeType": "application/pdf",
      "size": 248123,
      "digest": {
        "algorithm": "sha-256",
        "value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU"
      }
    },
    "upload": {
      "transport": "https",
      "method": "POST",
      "url": "https://upload.example.com/...",
      "headers": {
        "Content-Type": "multipart/form-data"
      },
      "multipart": {
        "fileField": "file",
        "fields": {
          "token": "abc123"
        }
      },
      "expiresAt": "2026-04-20T18:30:00Z"
    }
  }
}

The client uploads bytes out-of-band using the provided descriptor, then passes the returned file URI string in tools/call or the elicitation result. When a digest was provided during authorization, the server MUST reject a completed upload whose bytes do not match it. If the server returns a digest in the file metadata, that digest is authoritative for the resulting file URI.

If inline transfer is allowed and used instead, the client MAY skip files/authorizeUpload and send a SEP-2356 data: URI string. If the field declares transferModes: ["upload"], the client MUST NOT use this inline shortcut.

Tool Call Upload Lifecycle

The full out-of-band upload lifecycle for a tool call is:

Upload endpointServerClientUserUpload endpointServerClientUserauthorizeUpload params may include an expected complete-file digestmultipart body includes the file bytes under upload.multipart.fileFieldtools/call arguments.document is "mcp-file://server/file_01HXYZ"Select report.pdf for analyze_document.documentfiles/authorizeUpload { name, mimeType, size, digest? }{ file: FileValue, upload: HTTPS multipart descriptor, download?: HTTPS GET descriptor }POST multipart/form-data { fileField: "file", file: report.pdf, token }200 OKtools/call analyze_document { document: "mcp-file://server/file_01HXYZ" }CallToolResult content or structuredContent

7. Tool Invocation with Files

After upload preparation, clients pass file URI strings in SEP-2356 file-valued tools/call arguments:

{
  "jsonrpc": "2.0",
  "id": 11,
  "method": "tools/call",
  "params": {
    "name": "analyze_document",
    "arguments": {
      "document": "mcp-file://server/file_01HXYZ"
    }
  }
}

Servers MUST treat file arguments as URI strings whose file URI values are resolved through the upload contract, not as local filesystem paths or storage-specific handles.

8. File Outputs

Tool results may return FileValue objects in either structuredContent or a new content item of type file:

{
  "type": "file",
  "file": {
    "uri": "mcp-file://server/file_01HYZA",
    "name": "annotated-report.pdf",
    "mimeType": "application/pdf",
    "size": 252001,
    "digest": {
      "algorithm": "sha-256",
      "value": "g_o9GvJ2c3BIHqS_oQYV-a8bW4FSYd9_gF9Eav8F8BA"
    }
  }
}

Clients MUST verify the received byte count and digest when those values are present on a file they download.

Clients authorize generated files for download through files/authorizeDownload:

{
  "jsonrpc": "2.0",
  "id": 12,
  "method": "files/authorizeDownload",
  "params": {
    "uri": "mcp-file://server/file_01HYZA"
  }
}

The server responds with an AuthorizedFile that includes the stable file reference and a download descriptor in parallel:

{
  "jsonrpc": "2.0",
  "id": 12,
  "result": {
    "file": {
      "uri": "mcp-file://server/file_01HYZA",
      "name": "annotated-report.pdf",
      "mimeType": "application/pdf",
      "size": 252001,
      "digest": {
        "algorithm": "sha-256",
        "value": "g_o9GvJ2c3BIHqS_oQYV-a8bW4FSYd9_gF9Eav8F8BA"
      }
    },
    "download": {
      "transport": "https",
      "method": "GET",
      "url": "https://download.example.com/...",
      "expiresAt": "2026-04-20T18:45:00Z"
    }
  }
}

This method is the standard way to resolve generated files and the canonical refresh path for eager download authorizations. Servers MAY also return ResourceLink objects for resource-oriented workflows, but file output interoperability defined by this SEP MUST NOT require resources/read. The optional resource-read integration in the next section is only for resources whose contents are file-backed; it is not required for tool-result file outputs.

File URI lifetime is server-defined. This SEP does not define how long a file URI remains resolvable, whether a URI can be resolved more than once, or whether file URIs are scoped to a request, task, session, user, or server. Servers MUST reject expired, unauthorized, or unknown file URIs with an appropriate JSON-RPC error. Clients MUST treat file URIs and eager download authorizations as revocable and be prepared for files/authorizeDownload to fail even if the URI or a previous download descriptor was previously returned by the server.

9. Resource Reads

resources/read keeps its existing TextResourceContents and BlobResourceContents shapes. This SEP adds an optional FileResourceContents variant for resources whose bytes should be transferred out-of-band:

{
  "uri": "mcp-resource://reports/monthly.pdf",
  "mimeType": "application/pdf",
  "content": {
    "uri": "mcp-file://server/file_01HYZA"
  }
}

Clients resolve file-backed resource contents through files/authorizeDownload in the same way they resolve generated file outputs from tool calls. This avoids requiring large or sensitive resource bytes to be returned inline through JSON-RPC.

10. Error Handling

Implementations SHOULD use standard JSON-RPC errors with the following guidance:

  • -32601 when files/authorizeUpload or files/authorizeDownload is not supported.
  • -32602 when a file URI is malformed or violates declared constraints such as accept or maxSize.
  • -32603 for internal failures resolving upload or download descriptors.

Servers SHOULD distinguish digest mismatches from other upload validation failures when they reject an authorized upload whose bytes do not match the declared digest.

Servers SHOULD include machine-readable details when rejecting a file, such as:

{
  "reason": "maxSizeExceeded",
  "maxSize": 10485760,
  "actualSize": 24812345
}

Rationale

Why Reuse x-mcp-file

SEP-2356 moved file input declaration into the schema through x-mcp-file, an x-mcp-* extension keyword on URI string fields. This SEP follows that model rather than introducing a second declaration channel.

File transfer does not need to change which argument is file-valued. It only broadens the URI values that may satisfy that field. A SEP-2356-only client sends a data: URI; a client that also supports this SEP may send a server-issued file URI after files/authorizeUpload.

Keeping declaration in x-mcp-file also avoids drift between parallel declaration channels. The file affordance, file selection constraints, and value schema remain visible at the same point in the input schema, while this SEP focuses on transfer mechanics.

Why Amend maxSize

SEP-2356 introduced maxSize for the decoded size of inline data: values. Once an x-mcp-file field can also be satisfied by a prepared file URI, the same limit should remain the server's declared per-file acceptance limit rather than becoming an inline-only hint.

For data: URI values, implementations compare maxSize against decoded bytes. For file URI values, servers enforce the same limit during upload negotiation and when validating the eventual tools/call argument or elicitation response.

Why Add transferModes

Letting the client choose the transfer mode is useful for many inputs, but not all of them. Some servers need to require out-of-band upload to avoid large inline payloads, accidental transcript or log exposure, or harness behavior that rewrites file inputs into base64. Other servers may prefer inline data: URIs for small, lightweight files where an upload negotiation round trip would be unnecessary.

transferModes keeps that policy with the file input declaration. Omitting it preserves client choice; listing one or more modes lets a server constrain the value shape it can safely and efficiently handle while leaving room for future transfer modes.

This remains compatible with SEP-2356's host-side rules for inline values, including the case where a host chooses to forward a model-supplied data: URI verbatim under its existing approval policy.

Why URI Strings, Not FileValue, for Inputs

Using URI strings for inputs preserves SEP-2356 compatibility. A server that declares a file input through SEP-2356 receives a URI string whether the client chose inline data: transfer or out-of-band upload. This avoids a split where the same file-valued argument is a string for SEP-2356 clients but an object for SEP-2631 clients.

For out-of-band uploads, metadata is exchanged during files/authorizeUpload. The server returns a stable FileValue there so the client has the file reference and display metadata, and can optionally include an eager download authorization in parallel. The eventual tools/call or elicitation response only needs to carry the URI string.

Why Separate File and Resource URI Namespaces

Resource URIs and file URIs have different resolution contracts. Resource URIs are resolved through resources/read; file URIs are transfer handles resolved through files/authorizeDownload or the upload descriptor returned by files/authorizeUpload.

Keeping those namespaces distinct prevents clients from accidentally treating a short-lived transfer capability as a model-readable resource, and prevents servers from requiring file-transfer implementations to expose resources only to move bytes.

Why File Values for Outputs

Generated files often need display metadata in addition to a URI. A FileValue gives the protocol a stable, model-visible output unit that can survive different backing implementations:

  • local filesystems;
  • hosted upload buckets;
  • application-managed artifacts;
  • transient gateway handles;
  • generated artifacts with display names and MIME types.

Keeping eager download authorization in a shared AuthorizedFile parent for the files/authorize* methods preserves that separation. FileValue stays a stable, transport-agnostic reference that tools and resources can return directly, while the RPC results can expose upload and download authorizations in parallel sibling fields.

Why Separate Control Plane from Data Plane

The main design trap is forcing both file identity and file bytes through the same JSON payload. Splitting these concerns keeps MCP aligned with JSON-RPC control flow while letting implementations choose practical transport mechanisms for bytes.

Why Not Default to Elicitation

File-specific elicitation and URL-mode flows are still useful as fallback UX, but they should not be the default path because in the common case the client can bind the file before tools/call, avoiding an unnecessary extra round trip.

This SEP therefore complements, rather than replaces, SEP-2356's recommendation to use URL-mode elicitation when the server needs a large-file or server-controlled upload flow.

Why Recommend Host-Managed Model Exposure

This SEP defines interoperable wire representations for moving files, not a requirement that product surfaces paste whole files into model-visible context. In many implementations, the better user experience is for the host or harness to expose the file to the model as an attachment, reference, or other managed affordance while keeping the raw bytes out of the prompt transcript.

That is especially important for inline data: URIs. They remain part of the protocol for compatibility and small-file cases, but they are not the recommended default model-facing representation for a generic file. Treating file inputs as host-managed artifacts reduces accidental base64 dumps, transcript bloat, and avoidable sensitive-data exposure while still allowing the host to forward inline values verbatim when SEP-2356's rules or the product's approval policy call for it.

Why Standardize Download Resolution

ResourceLink is useful, but it is not enough by itself for generic file output. At a surface level, a ResourceLink and a file-valued output can look quite similar: both can carry a URI plus display metadata such as name, MIME type, or size. The important difference is the resolution contract. A ResourceLink points at something the client can read through resources/read; a file-valued output points at a file transfer handle the client resolves through files/authorizeDownload.

That distinction matters for interoperability. Tool authors need a predictable answer to "how does this generated file become downloadable or storable?" without requiring clients to treat short-lived transfer capabilities as model-readable resources or requiring servers to expose file outputs through resources/read. A dedicated files/authorizeDownload method makes that path explicit.

Backward Compatibility

This proposal is additive.

  • Existing resources/read text and blob resource-content shapes continue to work unchanged.
  • Existing ResourceLink workflows continue to work unchanged.
  • Existing tools that accept ad hoc string or object inputs for files may continue to do so, though they would not automatically gain interoperability from this SEP.

If adopted, implementations may gradually migrate from custom file argument conventions to the standard SEP-2356 declaration model and file URI transfer model defined here.

Security Implications

This SEP introduces important security considerations:

  • Upload and download descriptors are bearer-style capabilities and MUST be scoped, time-limited, and transport-protected.
  • Clients MUST not follow untrusted upload or download descriptors over insecure transports unless explicitly negotiated by a future SEP.
  • Servers MUST validate MIME type, size, and file content according to their own policy rather than trusting client metadata.
  • Clients SHOULD surface the source and destination context for file transfers so users understand where bytes are going.
  • Hosts and harnesses SHOULD expose file inputs to models through references, metadata, or other product affordances rather than embedding the full file bytes directly into model-visible context. In particular, implementations SHOULD NOT treat raw base64 or data: URI payloads as the default model-facing representation of a file when a file handle or host-managed attachment affordance is available.
  • Hosts that forward a model-supplied inline data: URI under SEP-2356 rules MUST NOT rewrite it into a local-file upload or substitute other bytes for it.
  • Inline transfer increases the risk of oversized payloads and accidental sensitive-data exposure in logs or transcripts; implementations SHOULD keep inline limits bounded.

Performance Implications

The main performance impact is positive:

  • large files no longer need to be encoded into inline data: URIs inside JSON-RPC messages;
  • intermediaries and servers avoid parsing oversized request bodies for file content;
  • clients can use storage-native upload and download paths.

This SEP does add one control-plane round trip for upload or download negotiation, but that tradeoff is preferable to making every file transfer inline.

Testing Plan

Conforming implementations should test at least:

  • upload preparation followed by successful tools/call with a file URI string;
  • generated file output followed by successful files/authorizeDownload;
  • rejection of malformed file URI values;
  • rejection of inline files that violate server validation or policy;
  • user-driven file selection from non-filesystem sources such as browser upload surfaces.

Alternatives Considered

1. Use only inline data: URIs

Rejected as the whole transfer story because it couples control-plane semantics to raw-byte transport, scales poorly, and encourages large model-visible payloads. This SEP keeps SEP-2356 data: URIs as the baseline representation while adding file URIs for out-of-band transfer.

Partially viable for server-originated outputs, and this SEP does integrate file-backed resource contents with resources/read. However, these primitives are not sufficient for client-to-server ingress. Uploads need an explicit negotiation and authorization story.

3. Leave file parameters entirely implementation-defined

Rejected because it preserves the current interoperability gap.

Open Questions

  • Should files/authorizeUpload require support for both raw-body and multipart HTTP uploads in v1, or allow servers to advertise only one?
  • Should generated files be representable both as file content items and as structured content references, or should MCP pick only one normative result shape?
  • Should future work define a first-class client-owned artifact store distinct from existing resource-oriented workflows?
  • PR #2356: an existing MCP proposal for declarative file inputs in tools and elicitation. This SEP builds on its x-mcp-file declaration model and URI-string input value contract by adding negotiated file URI transfer and generated file outputs.
  • OpenAI openai/fileParams: prior art for declaratively identifying which tool parameters are file-valued. Unlike openai/fileParams, SEP-2356's x-mcp-file keyword is intended to be the actual MCP file-input declaration rather than an app-specific parameter rewriting hint.

SEP-2632 Structured Content for Progress Notifications

Draft · Standards Track · Created 2026-04-22

  • Status: Draft
  • Type: Standards Track
  • Created: 2026-04-22
  • Author(s): @stevehaertel - Steve Haertel
  • PR: TBD

Imported from upstream. Proposed as modelcontextprotocol#2632 by @stevehaertel on 2026-04-22, 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 the Reference Implementation section links a discussion thread, not an implementation. 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 extending MCP progress notifications with an optional structured content field in addition to the existing human-readable message field. Today, notifications/progress supports numeric progress updates and a short text message, which is sufficient for simple status reporting but inadequate for interoperable, machine-readable progress events in modern tool, workflow, and multi-agent systems.

The proposed content field would allow implementations to attach a compact structured payload to a progress notification. A minimal interoperable shape would include event, text, actor, and data. The data object may carry event-specific fields, including common tool-oriented fields such as toolName, input, and output.

This proposal preserves backward compatibility by retaining message for simple clients while enabling richer clients and orchestration runtimes to render and reason about progress updates without parsing free-form strings. The intent is to improve interoperability for general-purpose progress reporting across MCP implementations, not to standardize any one application's UI model.

Motivation

The current progress notification format is intentionally lightweight. It allows a sender to report:

  • progressToken
  • progress
  • total
  • message

This is enough for simple loading indicators and basic text updates, but it becomes limiting when a progress notification needs to communicate more than a single sentence.

This motivation was informed in part by a Langflow enhancement demonstration:

That work makes use of the existing message-based progress mechanism but it's easy to imagine possibilities to provide a richer, more transparent user experience during multi-step and delegated execution, and it highlights why a more structured protocol-level representation would be valuable.

In practice, progress updates often represent structured events rather than just text. Examples include:

  • a tool call beginning
  • a tool result being returned
  • an agent step starting or completing
  • a workflow stage transitioning
  • a warning or partial result being emitted

Today, implementations that need richer progress semantics must either:

  1. encode structured information into the free-form message string, which is not interoperable and requires clients to parse human-oriented text, or
  2. emit richer data through implementation-specific side channels, which fragments the ecosystem.

This is especially limiting in systems involving tools, orchestration runtimes, and agent-to-agent delegation, where progress updates may benefit from identifying:

  • what kind of event occurred
  • who emitted it
  • what tool was involved
  • what input or output is relevant

A structured progress payload would make the protocol more generally useful without changing the semantics of existing progress notifications.

Specification

Summary

Extend ProgressNotificationParams with an optional content field.

Existing fields remain unchanged:

  • progressToken
  • progress
  • total
  • message

The new field is:

  • content — an optional structured object describing the progress event

Proposed Shape

The content field SHOULD be a structured object with the following minimal interoperable shape:

interface ProgressContent {
  event?: string;
  text?: string;
  actor?: {
    kind?: string;
    name?: string;
  };
  data?: Record<string, unknown>;
}

Field Semantics

content.event

A machine-readable string describing the kind of progress event being reported.

Examples MAY include:

  • progress_update
  • agent_step
  • tool_call
  • tool_result

This SEP does not require a closed vocabulary of event names. Implementations MAY define additional event values. Future standardization MAY define a recommended vocabulary if needed.

content.text

Optional human-readable text associated with the event.

This may duplicate or elaborate on the top-level message field. If both message and content.text are present, they SHOULD be semantically aligned.

content.actor

Optional metadata describing the actor that emitted or is responsible for the progress event.

Examples include:

  • an agent
  • a tool
  • a server component
  • a workflow executor

The actor object is intentionally minimal in this proposal and includes only:

  • kind
  • name

Implementations MAY omit this field when actor identity is not relevant.

content.data

Optional event-specific structured payload.

For tool-oriented events, the following keys are especially useful and MAY make use of:

  • toolName
  • input
  • output

These fields are siblings within data, not nested within one another.

For example:

  • a tool_call event MAY include toolName and input
  • a tool_result event MAY include toolName and output

Other event types MAY include other implementation-specific keys.

Top-Level message Compatibility

The existing top-level message field remains valid and unchanged.

  • Simple clients MAY continue to ignore content and render only message
  • Richer clients MAY use content for structured handling and rendering
  • Senders MAY continue to provide message when a human-readable summary is useful, even when content is present

Example: Tool Call Progress

{
  "jsonrpc": "2.0",
  "method": "notifications/progress",
  "params": {
    "progressToken": "abc123",
    "progress": 50,
    "total": 100,
    "message": "Calling retrieval tool",
    "content": {
      "event": "tool_call",
      "text": "Calling retrieval tool",
      "actor": {
        "kind": "agent",
        "name": "catalog-assistant"
      },
      "data": {
        "toolName": "get_metadata",
        "input": {
          "productId": "ABC"
        }
      }
    }
  }
}

Example: Tool Result Progress

{
  "jsonrpc": "2.0",
  "method": "notifications/progress",
  "params": {
    "progressToken": "abc123",
    "progress": 75,
    "total": 100,
    "message": "Retrieved metadata",
    "content": {
      "event": "tool_result",
      "text": "Retrieved metadata",
      "actor": {
        "kind": "agent",
        "name": "catalog-assistant"
      },
      "data": {
        "toolName": "get_metadata",
        "output": {
          "productId": "ABC",
          "status": "success"
        }
      }
    }
  }
}

Schema and Documentation Updates

If accepted, this SEP would require updates to:

  • schema/draft/schema.ts
  • generated schema artifacts
  • docs/specification/draft/basic/utilities/progress.mdx
  • examples for ProgressNotification and ProgressNotificationParams

Rationale

Why Add content Instead of Expanding message

message is intentionally human-readable. It is useful for simple rendering but poorly suited for machine-readable semantics. Adding content preserves the simplicity of message while allowing structured progress information to be represented directly.

Why Use a Small Structured Object

This proposal intentionally keeps the shape compact:

  • event
  • text
  • actor
  • data

A small contract improves the chance of broad adoption while still supporting the most common general-purpose use cases.

Why Not Standardize Hierarchy Fields Now

This SEP does not propose fields such as parent identifiers, nesting identifiers, labels, or ordering semantics.

Such fields may be useful in some systems, but standardizing them now would overfit the proposal to specific orchestration and UI models. A smaller proposal is more likely to serve general-purpose progress reporting across the MCP ecosystem.

Why toolName, input, and output

These fields map well to common tool and agent execution patterns and are understandable across many implementations.

They are proposed as recommended examples within data, not as exhaustive or mandatory keys. This gives implementations a common starting point without preventing other structured event data.

Why Not Use Only Arbitrary metadata

A generic metadata or data field alone would provide flexibility, but without a minimal interoperable shape it would be difficult for clients to interpret progress events consistently.

By defining event, text, actor, and data, this proposal gives implementations a shared contract while preserving extensibility.

Backward Compatibility

This change is backward-compatible.

  • Existing clients that understand notifications/progress but do not recognize content can ignore it
  • Existing servers and clients may continue using only message
  • No existing field is removed or redefined

This proposal only adds an optional field and does not change the meaning of current progress notifications.

Reference Implementation

A prototype demonstration related to this proposal exists in Langflow and is described here:

That demonstration uses the current, legacy progress mechanism based only on the top-level message field. Even with that limitation, it is an excellent example of why this enhancement would be useful: it shows how progress notifications can provide a richer, more transparent experience for users by surfacing intermediate tool and agent activity during execution.

At the same time, the demonstration highlights the limitations of relying on a single free-form message string for representing richer progress semantics. This SEP is intended to generalize that need into a protocol-level enhancement that works across MCP implementations, rather than defining a Langflow-specific solution.

A formal MCP reference implementation has not yet been added to this repository.

Security Implications

This proposal does not introduce a new transport or execution capability. It only allows more structured data to be attached to an existing notification type.

However, implementers should consider the following:

  • structured progress content may expose tool inputs or outputs that contain sensitive data
  • clients should avoid rendering or logging structured progress content without applying existing privacy and redaction policies
  • servers should avoid sending unnecessary sensitive material in content.data
  • senders should avoid including excessively large inputs, outputs, or other structured payloads in progress updates
  • implementations SHOULD consider rate limiting, truncation, summarization, or size limits for structured progress content to avoid excessive load on clients, servers, transports, logging pipelines, or intermediary infrastructure

These concerns already exist for free-form message strings and other MCP payloads, but structured content may make such data easier to capture, persist, and transmit at higher volume.

Alternatives Considered

1. Keep message Only

Rejected because it forces implementations to encode machine-readable state into free-form text, which reduces interoperability.

2. Add a Generic metadata Field Only

Rejected because it is too underspecified. Different implementations would likely use incompatible shapes, limiting interoperability.

3. Standardize a Much Richer Event Model

Rejected for now because it would likely overfit the proposal to specific UI and orchestration systems. A smaller shape is more appropriate as an initial step.

4. Use Arrays of Content Blocks

Rejected for now because progress notifications typically represent a single event, and a single structured object is simpler. This can be revisited in the future if stronger alignment with other MCP content models is needed.

References

  • Current progress utility documentation: /docs/specification/draft/basic/utilities/progress.mdx
  • Draft schema definition: /schema/draft/schema.ts
  • SEP Guidelines: /docs/community/sep-guidelines.mdx

SEP-2633 Standard Client-Side Configuration Format - mcp.json

Draft · Standards Track · Created 2026-04-21

Imported from upstream. Proposed as modelcontextprotocol#2633 by @BobDickinson on 2026-04-22, 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 the Reference Implementation section links a design document, not an implementation. 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

The MCP ecosystem has server.json (the MCP Registry package specification), and soon the related ServerCard, for describing how servers can be configured. But there's no standard for the other side: how a client will connect to its servers.

mcp.json is a proposal for a minimal, client-side configuration format that any MCP client can adopt, and that will provide interoperability of MCP server configurations across clients.

Motivation

Today every MCP client invents its own format for server configuration. Clients use different file names, and even different file types (JSON, JSONC, and TOML). They use different top-level keys (including "servers" and "mcpServers"). They use different values for the type field. They use different mechanisms and encodings for secret interpolation. The list goes on. This makes it hard to share MCP server configurations across clients.

As just one example of why this is an issue, consider that many clients allow MCP servers to be configured at the project level. In such a case, if developers of that project used different clients, they would be unable to share a common definition of project-level MCP servers.

Consider this example of the same GitHub MCP server configuration in two popular clients: VS Code and Claude Code.

VS Code: .vscode/mcp.json (JSONC file)

{
  "servers": {
    // Comments don't break me
    "github": {
      "type": "http",
      "url": "https://api.githubcopilot.com/mcp/",
      "headers": {
        "Authorization": "Bearer ${input:github_mcp_pat}", // <- trailing comma
      },
    },
  },
  "inputs": [
    {
      "type": "promptString",
      "id": "github_mcp_pat",
      "description": "GitHub Personal Access Token",
      "password": true,
    },
  ],
}

Claude: .mcp.json (JSON file)

{
  "mcpServers": {
    "github": {
      "type": "http",
      "url": "https://api.githubcopilot.com/mcp",
      "headers": {
        "authorization": "Bearer ${GITHUB_MCP_PAT}"
      }
    }
  }
}

These two clients use different file names, different file types (JSONC vs JSON), different top-level keys, and different mechanisms of injecting secrets.

While these two clients (and many others) use the type value "http" to represent streamable HTTP, there are many exceptions: RooCode/Cline uses "streamable-http". Goose uses "streamable_http". LangChain used "streamable_http" until December of 2025 when they widened it to also allow "streamable-http" or just "http". Also, many clients infer the type value from the presence of "command" or "url" attributes (presumably modern clients infer the type to be streamable http when url is present, but it's not clear that can be relied upon).

This disparity also complicates life for server publishers trying to help with installation guidance. Consider the GitHub MCP server: https://github.com/github/github-mcp-server

In the documentation for that server there are 11 separate server configuration examples to show how to install that one server into different clients. There are VSCode instructions on the main page, then seven additional pages for installing into other clients, including one for installing it into other IDEs supporting Copilot, of which there are four variants (bringing the total to 11). Granted, server.json and ServerCards are a better solution to this, but even with those solutions, JSON configuration examples will likely still persist.

Specification

We will establish a standard format for mcp.json as defined below.

Relationship to server.json

Format Purpose Configurability
server.json Server package specification for registries Highly configurable — variables, templates, user-adjustable parameters
mcp.json Client-side server configuration Fully resolved — only auth secrets remain as interpolatable variables
  • server.json = "Here's how this server can be configured"
  • mcp.json = "Here's exactly how this client will connect to its servers"

Each entry in an mcp.json is what you get after resolving a server.json template with concrete values.

File Name, Type, and Structure

The file will be named mcp.json and will be a JSON file (and explicitly not a JSONC file). MCP servers will be under the top-level key mcpServers, and will consist of a map of server names to server configurations:

{
  "mcpServers": {
    "server-name": { ... },
    "another-server": { ... }
  }
}

Server names must match ^[a-zA-Z0-9_\[\]-]+$ (alphanumeric, hyphens, underscores, brackets).

Server Configuration Fields

Field Type Required Description
title string No Human-readable display name (max 100 chars)
description string No What the server provides (max 500 chars)
type string Yes Transport type: "stdio", "sse", or "streamable-http"
command string Yes (stdio) Executable command for stdio servers
args string[] No (stdio) Command-line arguments
env object No (stdio) Environment variables (string values)
url string Yes (remote) Endpoint URL for sse/streamable-http servers
headers object No (remote) HTTP headers for remote servers (string values)
oauth object No (remote) OAuth configuration for servers using OAuth authorization
Transport-Specific Requirements

stdio servers (type: "stdio"):

  • command is required
  • args is allowed
  • env is allowed
  • url and headers are not allowed

Remote servers (type: "sse" or type: "streamable-http"):

  • url is required
  • headers is allowed
  • oauth is allowed (see OAuth Configuration)
  • command, args, and env are not allowed

Transport Types

stdio — Local Process

Runs a local process that communicates via stdin/stdout:

{
  "github": {
    "title": "GitHub",
    "description": "Create and manage issues, PRs, branches, and files.",
    "type": "stdio",
    "command": "npx",
    "args": ["-y", "@modelcontextprotocol/server-github@0.6.2"],
    "env": {
      "GITHUB_PERSONAL_ACCESS_TOKEN": "${env:GITHUB_PERSONAL_ACCESS_TOKEN}"
    }
  }
}
sse — Server-Sent Events

For remote servers using the SSE transport:

{
  "monitoring": {
    "title": "Monitoring",
    "type": "sse",
    "url": "https://mcp.monitoring.example.com/sse",
    "headers": {
      "Authorization": "Bearer ${env:MONITORING_TOKEN}"
    }
  }
}
streamable-http — HTTP Streaming

For remote servers using HTTP streaming:

{
  "analytics": {
    "title": "Analytics",
    "type": "streamable-http",
    "url": "https://mcp.analytics.example.com/mcp",
    "headers": {
      "X-API-Key": "${env:ANALYTICS_API_KEY}"
    }
  }
}

OAuth Configuration

Remote servers can use OAuth for authorization. The oauth object configures how the MCP client initiates the OAuth flow:

Field Type Required Description
clientId string No OAuth client ID. If omitted, the client uses Dynamic Client Registration (DCR) or discovery.
scopes string[] No OAuth scopes to request in the authorization request. Passed as the scope parameter (RFC 6749 §3.3).
redirectUri string No Redirect URI for the OAuth callback. For CLI/desktop clients this is typically http://localhost:{port}/callback (RFC 8252).

All oauth fields are optional. Servers that support automatic discovery (RFC 8414, RFC 9728) and Dynamic Client Registration may need no configuration at all — just a url:

{
  "linear": {
    "title": "Linear",
    "type": "streamable-http",
    "url": "https://mcp.linear.app/mcp"
  }
}

Servers that require a pre-registered client ID (no DCR support) specify it explicitly:

{
  "slack": {
    "title": "Slack",
    "type": "streamable-http",
    "url": "https://mcp.slack.com/mcp",
    "oauth": {
      "clientId": "1601185624273.8899143856786",
      "redirectUri": "http://localhost:3118/callback"
    }
  }
}

The scopes field allows a single server to be configured with different access levels under different names. Each entry gets its own OAuth session and consent grant:

{
  "bigquery-readonly": {
    "title": "BigQuery (Read-Only)",
    "description": "Read-only analytical queries against BigQuery.",
    "type": "streamable-http",
    "url": "https://mcp.bigquery.example.com/mcp",
    "oauth": {
      "clientId": "bigquery-mcp-client",
      "scopes": ["https://www.googleapis.com/auth/bigquery.readonly"]
    }
  },
  "bigquery-readwrite": {
    "title": "BigQuery (Read-Write)",
    "description": "Full read-write access to BigQuery.",
    "type": "streamable-http",
    "url": "https://mcp.bigquery.example.com/mcp",
    "oauth": {
      "clientId": "bigquery-mcp-client",
      "scopes": [
        "https://www.googleapis.com/auth/bigquery.readonly",
        "https://www.googleapis.com/auth/bigquery"
      ]
    }
  }
}

This enables shareable configurations: a recipient copies the file, authenticates with their own identity, and each entry is scoped correctly via the OAuth consent flow.

Validation rules:

  • oauth is forbidden for stdio entries
  • oauth and headers with an Authorization key should not both be present on the same entry — use one auth mechanism or the other

Version Pinning

MCP server entries using stdio SHOULD pin packages to specific versions:

  • npm: @scope/package@0.6.2 (not @latest or @^1.0.0)
  • PyPI: package==0.5.0 (not package or package>=0.5)
  • OCI: image:1.0.2 (not image:latest)

Unpinned versions cause security issues and non-deterministic behavior, and make debugging difficult.

Secret Value Interpolation

All string values support secret value interpolation. The MCP client is responsible for resolving these values at runtime.

Interpolated values are enclosed in ${...} wrapping and can occur anywhere in a string value, including multiple times in the same string value. The values are encoded as URIs per RFC 3986 with a limited set of defined schemes representing value sources. For environment variables the scheme is "env" and the token would be represented as ${env:ENV_VAR_NAME}.

Interpolated values can be resolved by pipe chaining multiple URIs, where the first element to resolve to a non-empty value is used. For example: ${env:ENV_VAR_NAME | value:Foo}.

The use of schemes to represent values other than environment variables allows clients to reference secrets stored in other systems or by other means, while still being shared by multiple clients.

For example, a OnePassword URI in the form: op://<vault-name>/<item-name>/[section-name/]<field-name> as traditionally used in apps supporting OnePassword, could be used directly and shared by different clients as long as they supported OnePassword and had access to the specified vault. This would be true of any supported secret manager or technology.

As another example, VS Code and related products use a scheme of "input" to specify that the client should collect the value from the user, store it securely, and inject it at runtime. While this does not support sharing of the secret value across clients, it does at least support interoperability as each client understands it is responsible for collecting, securing, and injecting the value. It should be noted that shared secrets is preferred to this method, but "input" is still supported due to its wide use.

Initial Schemes
Scheme Description Example
env Environment var env:ENV_VAR_NAME
value Literal value value:Foo
input Collected value input:input_name
op OnePassword URI op://myVault/theItem/secret
... More to come

Interpolation is primarily intended for authentication secrets:

  • API keys: "${env:OPENAI_API_KEY}"
  • Bearer tokens: "Bearer ${env:AUTH_TOKEN}"
  • Database credentials: "postgresql://${env:PG_USER}:${env:PG_PASS}@host/db"

Non-secret values should use literals. The mcp.json file is meant to be a fully-formed configuration — using interpolation for non-secrets undermines that purpose.

Rationale

In designing this format, the authors conducted a survey of many popular clients, including: Claude Code, VS Code (and related products), Cursor, Goose, Kiro, Codex, RooCode/Cline, and LangChain, in part to determine if there was some broad standardization that could be adopted to work with most or many clients. As stated above, these clients differ in almost all aspects, with no two being fully compatible. For example, even for clients who supported compatible secret interpolation, they often didn't support them in the same set of fields, and only some of them supported Posix-style param expansion for defaults.

For secret interpolation in particular, the desire to support different sources of secret values led to the decision to make the scheme (including "env") explicit.

Since there was no one format that would be broadly compatible with many clients, we decided to focus on creating the most fully specified and unambiguous representation that supported the desired goals and functionality, exposing all client vendors to a comparable (but ideally, small) amount of pain.

Backward Compatibility

While technically not a backward compatibility concern of MCP itself, since mcp.json has not been part of the spec, most clients use some variation of mcp.json as specified here to manage server configurations, and few, if any, of them will be compatible with the format specified here.

Security Implications

It should be stated plainly that the mcp.json file presents a significant attack surface. It will frequently be in client or agent writable directories. If clients simply run (or make available) the defined servers they find in an mcp.json they will subject their users to a wide variety of significant attacks, including remote code execution (for stdio servers) and environment/secret exfiltration (for http servers).

In the past, the MCP protocol has been criticized for the mcp.json attack surface (with numerous reported CVEs). But, even though widely adopted, mcp.json was not part of the MCP spec and therefore this entire area has largely been considered a client problem, not an MCP problem. If we standardize mcp.json, it now becomes an MCP problem.

This related SEP details issues with "one-click" installation of servers by clients: SEP-1024: MCP Client Security Requirements for Local Server Installation.

The same consent issues apply to servers discovered in an mcp.json. When a client loads an mcp.json it should require consent for each server discovered and it should store that consent securely. If any part of a server configuration changes, consent must be re-established. If new servers show up on a subsequent load of an mcp.json, the client should identify them and seek consent.

For proper management of interpolated secrets, any secrets represented in the server configuration must be clearly identified in the consent interface, and the consent to use a given secret should be specific to the server for which consent was given.

Reference Implementation

TODO We need to publish a JSON schema that can be used to describe and validate mcp.json as defined in this document.

Alternatives Considered

We considered directly adopting the VS Code format as it supported the secret value interpolation with multiple providers/schemes, but it was the least compatible format in almost every other way.

We also considered directly adopting the Claude Code format as it is the most widely used and the most compatible, but it lacked the multi-provider secret interpolation support and its POSIX-style param expansion wouldn't have supported multiple values. And while its shape was most consistent with other clients, it still would have broken almost all of them in some way.

Open Questions

Should value interpolation apply to non-secret configuration values? What are the use-cases?

We need the ability to reference the registry entry or ServerCard used to configure the server originally (where it supplied the configuration shape), in part so that clients can display server configurations with greater UX fidelty, and so that they can reconfigure servers after initial configuration.

Do we need more exhaustive or complete examples of configurations?

Should we support cwd in stdio configs?

Should our Security "shoulds" be "MUSTS"?

Acknowledgments

This topic was discussed at the MCP Maintainers meeting held before the MCP Dev Summit on March 31, 2026 in NYC, where it was agreed by consensus that we should pursue a standard SEP. Many maintainers contributed to that discussion and their comments and concerns were incorporated into this specification.

This specification was inspired by mcp.json — A Proposed Client-Side Configuration Format.

SEP-2640 Skills Extension

Final · Extensions Track · Created 2026-04-23

  • Status: Final
  • Type: Extensions Track
  • Created: 2026-04-23
  • Author(s): Peter Alexander (@pja-ant), Ola Hungerford (@olaservo), Sambhav Kothari (@sambhav), Aditya Kumar (@aditya-scio), on behalf of the Skills Over MCP Working Group
  • Sponsor: @pja-ant
  • Extension Identifier: io.modelcontextprotocol/skills
  • PR: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640
This SEP was developed by the [Skills Over MCP Working Group](https://modelcontextprotocol.io/community/skills-over-mcp/charter). Design history and experimental findings are maintained in the [ext-skills repository](https://github.com/modelcontextprotocol/ext-skills).

Abstract

This SEP defines a convention for serving Agent Skills over MCP using the existing Resources primitive. A skill is a directory of files (minimally a SKILL.md) that provides structured workflow instructions to an agent. This extension specifies that each file in a skill directory is exposed as an MCP resource, conventionally under the skill:// URI scheme. Skills are addressed by URI and may be read directly; a skills/list method enumerates the skills a server serves (servers whose skill catalogs are large, generated, or otherwise unenumerable MAY return an empty or partial listing), and a skills/get method returns any single skill's entry by URI. The skill format itself (directory structure, YAML frontmatter, naming rules, and the progressive disclosure model that governs how hosts stage content into context) is delegated entirely to the Agent Skills specification; this SEP defines only the transport binding.

The extension defines three protocol methods. Every server declaring the extension implements skills/list, which enumerates the skills a server serves, and skills/get, which returns the entry for a single skill by URI, including skills absent from the listing. The optional resources/directory/read lists the direct children of a directory resource, giving agents scoped navigation of a skill's supporting files. Everything else rides on existing protocol surface, so hosts that already treat MCP resources as a virtual filesystem can consume MCP-served skills identically to local filesystem skills. The specification is accompanied by implementation guidelines for host-provided resource-reading tools and SDK-level convenience wrappers.

Motivation

Native skills support in host applications demonstrates strong demand for rich, progressively disclosed workflow instructions. MCP does not currently offer a conventional way to ship this content alongside the tools it describes, which leads to:

  • Fragmented distribution. A server and the skill that teaches an agent to use it are versioned, discovered, and installed separately. Users installing a server from a registry have no signal that a companion skill exists. (problem statement)
  • Instruction size limits. Server instructions are delivered as the instructions field of the server/discover result and are practically bounded in size. Complex workflows, such as the 875-line mcpGraph skill, do not fit this model. (experimental findings)
  • Inconsistent ad-hoc solutions. Absent a convention, several independent implementations have each invented their own skill:// URI structure, with diverging semantics for authority, path, and sub-resource addressing.

Specification

Dependencies

This extension has no dependencies beyond the base MCP Resources primitive. In protocol versions 2026-07-28 and later, skills/list results additionally carry the base protocol's list-caching attributes (SEP-2549).

Skill Format

A skill served over MCP MUST conform to the Agent Skills specification. In particular:

  • A skill is a directory. Its skill name is the value of the name field in its SKILL.md frontmatter.
  • Every skill MUST contain a SKILL.md file at its root.
  • SKILL.md MUST begin with YAML frontmatter containing at minimum the name and description fields as defined by the Agent Skills specification.
  • A skill MAY contain additional files and subdirectories (references, scripts, examples, assets).

This extension does not redefine, constrain, or extend the skill format. Future revisions of the Agent Skills specification apply automatically. In the event that the Agent Skills specification changes in a backwards incompatible way, clients MUST honor any backwards compatibility mechanisms provided by the Agent Skills specification and SHOULD continue to support the Agent Skills specification as it existed prior to any incompatible change.

Resource Mapping

Each file within a skill directory is exposed as an MCP resource. Servers SHOULD use the skill:// URI scheme, under which the resource URI has the form:

skill://<skill-path>/<file-path>

where:

  • <skill-path> is a /-separated path of one or more segments locating the skill directory within the server's skill namespace. It MAY be a single segment (git-workflow) or nested to arbitrary depth (acme/billing/refunds).
  • <file-path> is the file's path relative to the skill directory root, using / as the separator.

The resource for the skill's required SKILL.md is therefore always addressable as skill://<skill-path>/SKILL.md, and the skill's root directory is skill://<skill-path> (the /SKILL.md suffix removed, no trailing slash), matching Directory Listing.

The final segment of <skill-path> MUST equal the skill's name as declared in its SKILL.md frontmatter. This mirrors the Agent Skills specification's requirement that name match the parent directory name. Preceding segments, if any, are a server-chosen organizational prefix. Servers MAY organize skills hierarchically by domain, team, version, or any other axis. In skill://acme/billing/refunds/SKILL.md, the prefix is acme/billing and the skill's name is refunds; in skill://git-workflow/SKILL.md there is no prefix and the name is git-workflow. This means the skill name is always recoverable from the URI alone, without reading frontmatter.

Further constraints:

  • A SKILL.md MAY appear in a descendant directory of a skill, so skills can nest. See Nested skills.
  • The final <skill-path> segment, being the skill name, MUST satisfy the Agent Skills specification's naming rules. The first <skill-path> segment occupies the authority component and SHOULD be a valid reg-name per RFC 3986; any other prefix segments SHOULD be valid URI path segments; no further constraints are imposed on them.

Per RFC 3986, the first segment of <skill-path> occupies the authority component. This carries no special semantics under this convention and clients MUST NOT attempt DNS or network resolution of it.

A server MAY serve skills under another scheme native to its domain (e.g., github://owner/repo/skills/refunds/SKILL.md). No scheme is privileged: the structural constraints above (<skill-path> ending in the skill name, SKILL.md explicit in the URI) apply regardless of scheme, and skills/list enumerates a server's skills whatever scheme they use.

Skill identity does not depend on the scheme. A host learns that a resource is a skill in one of two ways: from a skills/list entry, the authoritative record of the skills a server publishes; or from an explicit reference (the server's instructions field, another skill, or the user), which skills/get confirms, the server answering for a skill it serves and erroring otherwise. This holds for every scheme, skill:// included. A host MUST NOT conclude that a resource is a skill merely because its URI carries a particular scheme.

Examples
Skill path File Resource URI
git-workflow SKILL.md skill://git-workflow/SKILL.md
pdf-processing references/FORMS.md skill://pdf-processing/references/FORMS.md
pdf-processing scripts/extract.py skill://pdf-processing/scripts/extract.py
acme/billing/refunds SKILL.md skill://acme/billing/refunds/SKILL.md
acme/billing/refunds examples/email.md skill://acme/billing/refunds/examples/email.md
Resource Metadata

For each skill://<skill-path>/SKILL.md resource:

  • mimeType SHOULD be text/markdown.
  • name SHOULD be set from the name field of the SKILL.md YAML frontmatter. By the path constraint above, this will equal the final segment of <skill-path>.
  • description SHOULD be set from the description field of the SKILL.md YAML frontmatter.

Servers MAY expose additional frontmatter fields via the resource's _meta object. When _meta keys are used for skill resources, implementations SHOULD use the io.modelcontextprotocol.skills/ reverse-domain prefix. Other files in the skill use the mimeType appropriate to their content.

Nested skills

A skill directory MAY contain further skills in descendant directories. A nested skill is subject to the same rules as any other skill (its directory name is its name, and the enclosing skill's path becomes part of its organizational prefix), with the following semantics:

  • Nested content is supporting content. From the enclosing skill's perspective, a nested skill's directory and files are ordinary supporting files, and reading them is ordinary reading. A nested SKILL.md read this way is ordinary markdown: hosts MUST NOT act on its frontmatter.
  • Activation requires fresh consent. Approval is per skill: approving a skill approves that skill alone and says nothing about skills nested within it. Activating a nested skill (loading it as a skill in its own right, whether through the host's skill-loading machinery or by giving effect to its frontmatter) requires fresh, explicit user consent; approval of the enclosing skill does not substitute for it. Once activated, a nested skill is an ordinary skill: its frontmatter takes effect under the same rules as any other MCP-served skill, including the approval gate on allowed-tools.
  • Publication is flat. A nested skill is published like any other: through its own skills/list entry, or by explicit reference. The listing remains flat: an entry for a nested skill is an ordinary entry whose uri happens to share a path prefix with the enclosing skill's, and nothing in the listing marks nesting.

Discovery

A server is not required to make its skills enumerable. A skill's URI is directly readable via resources/read whether or not it appears in any listing, and hosts MUST support loading a skill given only its URI (see Hosts: End-to-End Integration). This is the baseline: if a model has the URI, whether from server instructions, another skill, or the user, it can read the skill.

On top of that baseline, three mechanisms are defined. Two are discovery: enumeration via skills/list, which every server declaring this extension implements, and an optional pointer from server instructions. The third is retrieval: however a host arrives at a skill's URI, skills/get returns that skill's entry (its metadata and digests), including for skills no listing mentions.

Enumeration via skills/list

A server declaring the io.modelcontextprotocol/skills extension MUST implement the skills/list method, which returns the skills it serves. The result MAY be empty.

The request carries an optional pagination cursor:

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "skills/list",
  "params": {}
}

The result carries the skill entries:

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "resultType": "complete",
    "skills": [
      {
        "uri": "skill://git-workflow/SKILL.md",
        "frontmatter": {
          "name": "git-workflow",
          "description": "Follow this team's Git conventions for branching and commits"
        },
        "resources": [
          {
            "uri": "skill://git-workflow/SKILL.md",
            "digest": "sha256:a1b2c3d4...",
            "size": 2314
          }
        ]
      },
      {
        "uri": "skill://acme/billing/refunds/SKILL.md",
        "frontmatter": {
          "name": "refunds",
          "description": "Process customer refund requests per company policy",
          "license": "Apache-2.0"
        },
        "resources": [
          {
            "uri": "skill://acme/billing/refunds/SKILL.md",
            "digest": "sha256:b2c3d4e5...",
            "size": 3871
          },
          {
            "uri": "skill://acme/billing/refunds/examples/email.md",
            "digest": "sha256:c3d4e5f6...",
            "size": 962
          }
        ]
      },
      {
        "uri": "skill://pdf-processing/SKILL.md",
        "frontmatter": {
          "name": "pdf-processing",
          "description": "Extract, fill, and assemble PDF documents",
          "metadata": { "version": "2.1.0" }
        },
        "resources": [
          {
            "uri": "skill://pdf-processing/SKILL.md",
            "digest": "sha256:d5e6f7a8...",
            "size": 5120
          },
          {
            "uri": "skill://pdf-processing/references/FORMS.md",
            "digest": "sha256:e6f7a8b9...",
            "size": 18433
          },
          {
            "uri": "skill://pdf-processing/scripts/extract.py",
            "digest": "sha256:f7a8b9c0...",
            "size": 4096
          },
          {
            "uri": "skill://pdf-processing/templates/invoice.md",
            "digest": "sha256:a8b9c0d1...",
            "size": 1210
          },
          {
            "uri": "skill://pdf-processing/templates/purchase-order.md",
            "digest": "sha256:b9c0d1e2...",
            "size": 1388
          },
          {
            "uri": "skill://pdf-processing/templates/regional/eu-invoice.md",
            "digest": "sha256:c0d1e2f3...",
            "size": 1472
          }
        ]
      }
    ]
  }
}

Result fields:

Field Required Description
skills Yes Array of skill entries.
skills[].frontmatter Yes Verbatim copy of the skill's SKILL.md YAML frontmatter, rendered as JSON. See Frontmatter.
skills[].uri Yes Resource URI of the skill's SKILL.md. See Skill URIs.
skills[].resources Yes The skill's files: an array enumerating them with digests and sizes, or the string "dynamic". See Resources.
skills[].resources[].uri Yes Resource URI of the file.
skills[].resources[].digest Yes SHA-256 digest of the file. See Integrity.
skills[].resources[].size Yes Length in bytes of the file's raw content. See Limits.

A skill whose content is generated dynamically carries "resources": "dynamic" in place of the array. An entry with no resources at all is invalid.

Pagination mirrors the base protocol's list methods: the request accepts an optional cursor, and when the result includes nextCursor the client passes it back to retrieve the next page. An entry is atomic: a skill's resources set is never split across pages.

In protocol versions 2026-07-28 and later, the result also carries the base protocol's list-caching attributes, ttlMs and cacheScope, as defined for tools/list and resources/list (SEP-2549), with the same semantics: a freshness hint for the listing and a cache-scope marker, not an integrity property. Integrity and verification governs content regardless of how fresh a cached listing is.

A server whose skill catalog is large, generated on demand, or otherwise unenumerable MAY return an empty or partial listing. Hosts MUST NOT treat an empty or partial listing as proof that a server has no skills. The method serves entries for a server's skills whatever URI scheme they use. Enumeration is uniform across schemes.

Names

A skill's name is a label, not an identifier. A skill is identified by its uri within a server, and by the pair of server identity and uri across servers (Skill URIs). Within a server's listing, names SHOULD be unique, but they are not guaranteed to be: two skills at different paths may share a final segment (acme/billing/refunds and acme/support/refunds are both named refunds), and a nested skill may share its name with a top-level one. Hosts MUST NOT assume name uniqueness. When two entries in one listing collide on name, hosts MUST disambiguate them, for example by their distinguishing path segments, rather than silently discarding or preferring one. When skills from different origins collide on name, hosts MUST resolve the name within a per-origin namespace, identifying servers by a host-assigned label; an MCP-served skill MUST NOT silently shadow, or be silently substituted for, a same-named skill from any other origin, whether another server's or the host's own filesystem skills. See Security Implications.

Frontmatter

frontmatter is the skill's SKILL.md YAML frontmatter rendered verbatim as a JSON object. It contains every field the author wrote, not a curated subset. Because the Agent Skills specification requires name and description, those fields are always present; everything else (license, metadata, fields added by future revisions of the Agent Skills specification) passes through unchanged. A host can therefore build its skill registry (names, descriptions, and whatever other metadata it understands) from the listing alone, without fetching each SKILL.md.

The frontmatter object MUST be identical in content to the frontmatter of the SKILL.md it describes. The final <skill-path> segment of the entry's uri MUST equal frontmatter.name, per Resource Mapping.

Within the frontmatter metadata object, keys prefixed with io.modelcontextprotocol/ are reserved for metadata defined by MCP extensions. This extension currently defines no such keys. Implementations SHOULD ignore keys under this prefix that they do not recognize.

Skill URIs

uri is the full resource URI of the skill's SKILL.md, readable via resources/read. Supporting files are individually addressable as sibling resources under the same skill path, per Resource Mapping. A skill is always retrieved as individually addressable resources; this extension defines no packed or bundled retrieval form. See Appendix: Deferred Features.

A skill URI is scoped to the server that serves it. Nothing prevents two connected servers from both serving skill://refunds/SKILL.md, and those are two unrelated skills. The identity of an MCP-served skill is therefore the pair of the host's identity for the originating server and the skill's uri. Hosts MUST preserve both halves wherever a skill is recorded or addressed, including the registry, persisted approvals, the cache, and any tool or path through which the model reaches the skill, and MUST NOT key any of these on the uri alone. In particular, any path at which a host materializes skill content, whether a cache directory or a virtual mount, MUST encode the server identity as well as the uri, so that same-URI skills from different servers land at distinct paths and the originating server is recoverable from the path; this is also what lets the host honor the durable-origin requirement in Security Implications.

Resources

resources is REQUIRED on every skill entry and takes one of two forms: an array enumerating the skill's files (SKILL.md and every supporting file) as {uri, digest, size} triples, or the string "dynamic". The array is the unit of content that a host verifies and that a user's approval binds to:

  • When present, resources MUST be complete: it lists every file of the skill, each exactly once, including an entry matching the skill's top-level uri. That entry carries the digest and size of SKILL.md itself.
  • Each uri MUST be the skill's SKILL.md or a file within the skill's directory.
  • Each entry MUST carry size: the length in bytes of the file's raw content (the same bytes the digest covers). size lets a host budget a skill before fetching anything: it can enforce the Limits from the entry alone, decide whether a file is worth retrieving, and detect a truncated or padded read before hashing it. A read whose byte length differs from the entry's size is a verification failure equivalent to a digest mismatch (Integrity and verification), whether or not the host goes on to compute the digest.
  • Completeness extends to nested skills: from the enclosing skill's perspective their files are supporting files (Nested skills), so the enclosing skill's resources lists them too, and the same file may appear in both the enclosing and the nested skill's entries. A change to a nested skill is therefore a change to the enclosing skill's set.
  • When a skill's content is generated dynamically, such that stable digests cannot be published, the server MUST set "resources": "dynamic" instead of an array. The marker is explicit so that a host can tell a deliberately unverifiable skill from a malformed entry: an entry with no resources at all, or with any value other than an array or "dynamic", is invalid, and hosts MUST NOT load it. A skill whose resources is "dynamic" offers no content integrity and cannot be content-bound (Security Implications). Hosts MAY decline to load such skills, and server authors SHOULD expect that some hosts will.
Integrity and verification

Digests are SHA-256 hashes of an artifact's raw bytes, formatted as sha256:{hex} where {hex} is 64 lowercase hexadecimal characters. Each entry in a skill's resources carries the digest of the file at its uri.

When a host retrieves a file listed in a skill's resources, it MUST verify the content against that entry's digest. A mismatch means the content is not what the listing promised. It may be corrupted, tampered with, or simply stale because the skill was updated after the listing was fetched. Whatever the cause, hosts MUST NOT use the unverified content; to recover from staleness, call skills/get for that skill (or skills/list to refresh the catalog) and proceed from the current resources set, which, being different, revokes any content-bound approval (Security Implications). A host is acting on a skill from the moment it loads the skill's SKILL.md into the model's context until, at the earliest, that SKILL.md leaves context; hosts MAY hold the window open longer, never shorter. For the whole of that window the host holds the entry from which it loaded the skill. Because resources is complete, an unlisted file is a change to the skill: while acting on a skill, a host MUST resolve reads of the skill's files only to URIs listed in that entry's resources, and MUST treat a read of an unlisted file within the skill as a verification failure equivalent to a digest mismatch. Hosts MUST NOT retrieve a skill's files ahead of need, whether on connection, on listing, or at approval. A SKILL.md is fetched when the skill is loaded, and a supporting file when it is read. A server may publish many skills with many files each, and every host that connects retrieving all of them would impose load proportional to the catalog rather than to use. Hosts SHOULD instead cache what they do retrieve, and digests make that cache cheap to validate: a cached file whose digest matches the current entry can be served without fetching it again, and one whose digest does not match must be fetched again. A cached copy is only as trustworthy as the host's certainty that its bytes have not changed since they were verified; the requirements on a disk cache are in Security Implications. Lazy retrieval is compatible with content-bound approval, which binds to the entry's resources set rather than to retrieved bytes; a file fetched long after approval is verified against that set when it is read.

Digests are unsigned and supplied by the same server that supplies the content. A match proves the two are consistent, not that either is trustworthy. Any intermediary on the path, such as a gateway, can rewrite both the listing and the content together. Hosts MUST NOT treat a digest match as a security boundary.

After fetching a SKILL.md for which the host holds an entry, from either skills/list or skills/get (digest-verified when the entry's resources is an array, and unverifiable when it is "dynamic"), hosts MUST parse its YAML frontmatter and compare it field-by-field against the entry's frontmatter. Any discrepancy MUST be treated as a verification failure equivalent to a digest mismatch, and the skill MUST NOT be loaded. This enforces the Frontmatter identity requirement on the host side, so that what a user approves from the listing is what the model actually receives.

Limits

This extension fixes two per-skill limits so that servers know what every conforming host will accept and hosts know what they must be prepared to handle:

Limit Value Counted over
Resources per skill 512 entries The entries of the skill's resources, SKILL.md included
Total file size per skill 16 MiB (16,777,216 bytes) The sum of size over the skill's resources

Hosts MUST support skills up to and including these limits, and MAY support larger ones. Servers SHOULD NOT serve a skill that exceeds either limit; a skill that does is not guaranteed to be loadable by any conforming host. Because resources is complete, both limits are checkable from the entry alone, by counting entries and summing size, before the host retrieves a single file, and a host that declines a skill on this basis SHOULD tell the user why rather than fail silently on a later read.

For a skill whose resources is "dynamic", the entry offers nothing to count. A host that chooses to load such a skill applies the total-size limit to what it actually retrieves and MAY stop loading the skill once that limit is reached.

These limits bound a host's exposure to a single skill. They say nothing about how many skills a server may serve or a host must accept; a listing may be arbitrarily large, which is one reason hosts retrieve files only on demand (Integrity and verification).

Retrieval via skills/get

A server declaring the io.modelcontextprotocol/skills extension MUST also implement the skills/get method, which returns the entry for a single skill named by its URI:

{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "skills/get",
  "params": {
    "uri": "skill://pdf-processing/SKILL.md"
  }
}
{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "resultType": "complete",
    "skill": {
      "uri": "skill://pdf-processing/SKILL.md",
      "frontmatter": {
        "name": "pdf-processing",
        "description": "Extract, fill, and assemble PDF documents",
        "metadata": { "version": "2.1.0" }
      },
      "resources": [
        {
          "uri": "skill://pdf-processing/SKILL.md",
          "digest": "sha256:d5e6f7a8...",
          "size": 5120
        },
        {
          "uri": "skill://pdf-processing/references/FORMS.md",
          "digest": "sha256:e6f7a8b9...",
          "size": 18433
        },
        {
          "uri": "skill://pdf-processing/scripts/extract.py",
          "digest": "sha256:f7a8b9c0...",
          "size": 4096
        },
        {
          "uri": "skill://pdf-processing/templates/invoice.md",
          "digest": "sha256:a8b9c0d1...",
          "size": 1210
        },
        {
          "uri": "skill://pdf-processing/templates/purchase-order.md",
          "digest": "sha256:b9c0d1e2...",
          "size": 1388
        },
        {
          "uri": "skill://pdf-processing/templates/regional/eu-invoice.md",
          "digest": "sha256:c0d1e2f3...",
          "size": 1472
        }
      ]
    }
  }
}

params.uri MUST be the URI of a skill's SKILL.md. The skill object is a skill entry, identical in shape and meaning to an entry of skills/list, with the same uri, frontmatter, and resources fields under the same rules.

Semantics:

  • If the URI does not identify a skill the server serves, the server MUST return error -32602 (Invalid params), the same code resources/read uses for unknown resources.
  • A server MUST answer for every skill it serves, whether or not that skill appears in its skills/list result. A skill absent from a partial listing is still retrievable by URI.
  • The result is a point-in-time snapshot, exactly as a listing entry is. Re-calling the method is how a host refreshes one skill's digests without re-enumerating the catalog.
  • A skill whose content is generated dynamically carries "resources": "dynamic", per Resources, whether it is reached through skills/list or skills/get.
  • The result carries no pagination cursor: a single entry is not a list. The entry is a snapshot of the skill as the server holds it at that moment; whether the result should also carry the base protocol's caching attributes (ttlMs and cacheScope, per SEP-2549), as resources/read results do, is left open.

The method complements the baseline: a URI alone is enough to read a skill, and skills/get turns that same URI into the skill's metadata and digests, so a skill that never appeared in a listing can still be verified and content-bound (Security Implications).

Pointer from Server Instructions

A server MAY direct the agent to specific skill URIs from its instructions field. This requires no discovery machinery on the host; the URI is simply present in the model's context and readable via resources/read.

Capability Declaration

Per SEP-2133 extension negotiation, servers declare support for this extension in the extensions field of their capabilities:

{
  "capabilities": {
    "extensions": {
      "io.modelcontextprotocol/skills": {
        "directoryRead": true
      }
    }
  }
}

One extension-specific setting is defined:

Setting Type Default Meaning
directoryRead boolean false The server implements resources/directory/read.

An empty object indicates support for the extension with no optional features. Declaring the extension itself commits the server to skills/list and skills/get; clients MUST NOT call resources/directory/read against a server that has not declared directoryRead: true. A server declaring this extension MUST also declare the resources capability.

Reading

Skill files are read via the standard resources/read method. No skill-specific read semantics are defined.

In particular, reading a SKILL.md via resources/read does not by itself activate the skill. resources/read is transport: it returns bytes, whoever asked for them: a generic resource-reading tool, a resource browser, a user inspecting the server. A skill is activated only by the host's own skill-loading path, the one that verifies the content against the skill's entry (Integrity and verification), applies any required user approval (Security Implications), and opens the window in which the host is acting on the skill. Hosts MUST NOT treat a resources/read of a SKILL.md that arrives by any other route as a load: it grants no approval, opens no window, and confers no standing on the skill's supporting files. Content obtained that way is ordinary resource content, and a host that returns it to the model SHOULD do so as it would any other resource read, not as a loaded skill. A host that wishes such a read to load the skill routes it through the skill-loading path instead.

Internal references within a skill (e.g., SKILL.md linking to references/GUIDE.md) are relative paths, as in the filesystem form of the Agent Skills specification. A client resolves a relative reference against the skill's root, so references/GUIDE.md in skill://acme/billing/refunds/SKILL.md resolves to skill://acme/billing/refunds/references/GUIDE.md, exactly as a filesystem path would resolve. The skill's root is the directory containing SKILL.md, not the scheme root. When skills nest, each SKILL.md's references resolve against its own directory: a relative reference in a nested skill's SKILL.md resolves against the nested skill's root, regardless of how the file was reached.

Directory Listing

A skill's instructions frequently reference a directory rather than a file: "pick the appropriate template from templates/", "run the matching script in scripts/". To act on this, the agent must learn what the directory contains. resources/list cannot answer that scoped question: it enumerates the server's entire resource space, not a subtree, and the servers this SEP most wants to accommodate, large, generated, or unenumerable catalogs (see Why May the Listing Be Empty or Partial?), may not implement meaningful global listing at all.

This extension therefore defines one new method, resources/directory/read, gated behind the directoryRead setting of the capability declaration.

Directory resources

A directory resource is a resource whose mimeType is inode/directory. In a skill namespace served as individual files, every directory level is a directory resource: the skill root (skill://pdf-processing) and each subdirectory (skill://pdf-processing/templates). Directory URIs are written without a trailing slash. Directory resources need not appear in resources/list; they are addressable whether listed or not.

resources/directory/read

The request carries the directory's URI and an optional pagination cursor. The result carries the resource metadata of the directory's direct children, the same Resource objects that resources/list returns, with the same nextCursor pagination contract.

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "resources/directory/read",
  "params": {
    "uri": "skill://pdf-processing/templates"
  }
}
{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "resultType": "complete",
    "resources": [
      {
        "uri": "skill://pdf-processing/templates/invoice.md",
        "name": "invoice.md",
        "mimeType": "text/markdown"
      },
      {
        "uri": "skill://pdf-processing/templates/purchase-order.md",
        "name": "purchase-order.md",
        "mimeType": "text/markdown"
      },
      {
        "uri": "skill://pdf-processing/templates/regional",
        "name": "regional",
        "mimeType": "inode/directory"
      }
    ]
  }
}

Semantics:

  • The method applies only to directory resources. If the URI does not exist, or exists but is not a directory resource, the server MUST return error -32602 (Invalid params), the same code resources/read uses for unknown resources.
  • The result contains every direct child of the directory: files with their ordinary resource metadata, subdirectories listed as directory resources (mimeType: "inode/directory"). The listing is not recursive; clients descend by calling the method again on a child directory.
  • An empty directory yields an empty resources array.
  • Pagination mirrors resources/list: when the result includes nextCursor, the client passes it back as cursor to retrieve the next page.

A server that declares directoryRead MUST support the method for every directory within the skill namespaces it serves as individual files. The method itself is not skill-specific: a server MAY support it on any directory resource it serves, under any scheme.

Directory reads and the held entry

For a skill whose entry carries resources, the host already holds a complete manifest of the skill's files (Resources); a directory read tells it nothing about that skill's contents that the entry did not. Directory reading earns its place elsewhere: for dynamically generated skills, whose resources is "dynamic"; for resource trees that are not skills at all; and for obtaining the server's current view of a directory without first refreshing the entry. When a host acting on a skill with a manifest wants to know what templates/ contains, it MAY answer from the entry alone.

The two views can disagree. If the server adds a file to a skill after the host obtained its entry, a directory read may list that file while the held manifest does not. This is the stale-snapshot case that Integrity and verification already governs, and the recovery path is the one specified there: while acting on the skill under the held entry, the host MUST NOT read the newly listed child (an unlisted file is a verification failure, exactly as a digest mismatch is) and MUST NOT surface it to the model as a file of the skill. To reach it, the host refreshes the entry with skills/get, at which point the resources set has changed and any persisted content-bound approval is revoked and must be obtained again (Security Implications). Only under the refreshed entry is the new file readable. Hosts SHOULD expect this sequence and present it as a skill that has changed and needs re-approval, rather than as a read error. Conversely, a child present in the manifest but absent from a directory read is a file the server no longer serves; a read of it will fail, and the same refresh applies.

This extension defines no shared version or cache token that would let a host determine whether a directory result and an entry describe the same snapshot of the server. The manifest is authoritative for what the host may read under its current approval; a directory read is a live observation that may run ahead of or behind it. Hosts MUST NOT treat the directory result as extending the manifest.

For a dynamically generated skill, whose resources is "dynamic", none of this changes the skill's standing: it offers no content integrity and cannot be content-bound, and a host MAY decline to load it (Resources). A directory read is how such a skill's files are discovered at all, but it does not supply the integrity the entry lacks.

Implementation Guidelines

The following are recommendations for interoperable implementations. They are not part of the normative specification.

Hosts: End-to-End Integration

This section sketches one way a host might wire MCP-served skills into an existing skills implementation. It is illustrative, not prescriptive. Hosts are free to structure tools, naming, and routing however suits their architecture. The goal is that an MCP-served skill flows through the same loading and reading mechanics as a filesystem skill while remaining origin-tagged, per Security Implications.

Registry. At startup and on connection change, the host assembles a single internal skill registry from every origin it supports: filesystem skill directories, and skills/list results from each connected MCP server that declares the io.modelcontextprotocol/skills extension. Each registry entry records the skill's name and description (from the entry's frontmatter) and its origin: for a filesystem skill, the local directory, and for an MCP skill, the server identity and the SKILL.md resource URI. Assembling the registry reads only the listing: the host MUST NOT fetch SKILL.md or any supporting file at this stage (Integrity and verification). The entry's frontmatter carries everything the registry needs. The registry is keyed by skill identity, origin and SKILL.md URI together (Skill URIs), never by name. Because names collide within and across origins (Names), the registry qualifies colliding names for display rather than dropping either entry; since the name is not the key, a collision never changes how a skill is loaded.

Context. The host surfaces the name and description of each enabled registry entry in the model's context, the same list the model already sees for filesystem skills, now with MCP-served entries mixed in, together with the entry's identity: the host's label for the originating server and the SKILL.md URI. The name and description tell the model what a skill is for; the identity is what the model passes to load it. The host's UI presents the same merged list for user inspection and per-skill enable/disable, with provenance shown so users can see which server a skill came from.

Loading. The host exposes a single skill-loading tool to the model, keyed by skill identity, the originating server and the SKILL.md URI:

{
  "name": "read_skill",
  "description": "Load a skill's SKILL.md into context.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "server": {
        "type": "string",
        "description": "Name of the connected MCP server"
      },
      "uri": { "type": "string", "description": "The skill's SKILL.md URI" }
    },
    "required": ["server", "uri"]
  }
}

read_skill is the host's skill-loading path in this sketch, the only route by which a skill is activated (Reading); a read_resource call against the same SKILL.md URI returns its content but does not load the skill. When the model calls read_skill, the host looks up the pair in its registry and routes on origin: a filesystem skill is read from disk; an MCP skill is fetched via resources/read against the originating server, at that moment and not before, unless a verified copy is already in the host's cache. The mechanics are the same either way. Keying the tool by identity rather than by name is what lets a URI travel: a skill URI handed to the model by the user, by server instructions, or by another skill's SKILL.md is exactly what read_skill takes, with no need to map it back to a display name that the host may have qualified to resolve a collision (Names). A host that already exposes a name-keyed loading tool for filesystem skills extends it to accept server and uri rather than introducing a parallel one; for a filesystem skill it may reserve a server value for the local origin and pass the SKILL.md path as uri.

Supporting files. Once a SKILL.md is in context, the model may encounter relative references to supporting files (references/GUIDE.md, scripts/extract.py). For filesystem skills the model reads these with the host's ordinary file-read tool; for MCP skills there is no local file. The host therefore also exposes a general-purpose resource-reading tool:

{
  "name": "read_resource",
  "description": "Read an MCP resource from a connected server.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "server": {
        "type": "string",
        "description": "Name of the connected MCP server"
      },
      "uri": { "type": "string", "description": "The resource URI" }
    },
    "required": ["server", "uri"]
  }
}

The host arranges for the model to know, when it loads an MCP-served SKILL.md, which server it came from and what its base URI is, for example by stating both in the read_skill tool result, so the model can resolve references/GUIDE.md to skill://<skill-path>/references/GUIDE.md and issue read_resource against the right server. A host may instead fold this into its file-read tool by mounting each server's skill:// namespace into a virtual path, with one mount root per server so that the path encodes the server identity (Skill URIs), and translating reads under that path into resources/read calls, in which case no separate read_resource tool is needed and the model treats every supporting file as a local path. A virtual mount resolves reads on access; it MUST NOT be populated by fetching the skill's files in advance. Either way the resolution rule is the same: relative references resolve against the skill's root directory, exactly as on a filesystem. When the skill's entry carries a resources array, the host verifies each such read against it, per Integrity and verification.

Directory navigation. Skill instructions may point the model at a directory rather than a file ("choose the right template from templates/"). When the originating server declares directoryRead, the host SHOULD surface this capability to the model: a read_resource call whose target is a directory resource can be routed to resources/directory/read and return the child listing, and the virtual-mount approach maps it onto the host's existing directory-listing tool: an ls of a mounted path becomes a resources/directory/read call.

Unenumerated skills. Because a listing may be empty or partial, a host should also accept skill URIs it has never seen listed, handed to the model by the user, by server instructions, or by another skill. Calling skills/get on such a URI yields the same entry a listing would have carried, so an unlisted skill enters the registry, gets verified, and is approved on the same terms as a listed one; a server that does not serve the URI as a skill answers with an error. No special tool surface is needed: read_skill already takes a server and a URI, so a read_skill call naming a pair the registry has not seen is the trigger for skills/get.

Both tool signatures above include server because two connected servers may both serve skill://refunds/SKILL.md (Skill URIs). That is one disambiguation strategy; a host may instead rewrite URIs with a per-server prefix, scope by session, or anything else appropriate to its architecture. The tool is general-purpose, reads any MCP resource, and is useful beyond skills.

SDKs: Convenience Wrappers

SDK maintainers SHOULD provide affordances that wrap the underlying resource operations in skill-specific terms. For example:

Server-side: declare a skill from a directory, at a given path:

@server.skill("git-workflow")                 # → skill://git-workflow/SKILL.md
def git_workflow():
    return Path("./skills/git-workflow")

@server.skill("acme/billing/refunds")         # → skill://acme/billing/refunds/SKILL.md
def refunds():
    return Path("./skills/refunds")

The SDK handles: reading SKILL.md frontmatter to populate resource metadata, serving file content on resources/read, and answering skills/get (and, where the server's skill set is bounded, skills/list), computing entry digests and sizes from the registered files, and warning when a registered skill exceeds the Limits.

Client-side: enumerate and fetch skills:

skills = await client.list_skills()               # calls skills/list, paginating; may be empty
entry = await client.get_skill(
    "skill://acme/billing/refunds/SKILL.md")      # calls skills/get, listed or not
content = await client.read_skill_uri(
    "skill://acme/billing/refunds/SKILL.md")      # wraps resources/read, works regardless of enumeration
entries = await client.read_directory(
    "skill://pdf-processing/templates")           # wraps resources/directory/read

These wrappers are thin, each a single underlying protocol call with a fixed URI pattern, but they give server authors an ergonomic way to declare skills and give client authors a discoverable entry point.

Rationale

The design rationale for this SEP (why skills map to Resources rather than a new primitive, the URI structure, listing semantics, skills/get, the choice of a method over an index resource, format delegation to agentskills.io, directory reads, verbatim frontmatter, and per-file digests) is maintained as a standalone document in the Working Group repository: rationale.md.

One point of that design bears restating here because it shapes how the two methods relate. A skills/list entry is intentionally a complete manifest of the skill, its verbatim frontmatter and its full resources set with digests, rather than a summary to be filled in by a follow-up call. A host that pages through the listing therefore has, in that one pass, everything it needs to build its registry, present the skill for approval, bind the approval to content, and verify every file it later reads; there is no second round-trip per skill, which matters most for exactly the hosts that connect to many servers or servers with many skills. skills/get exists for the cases the listing does not serve: refreshing a single skill's entry, typically after a digest mismatch, without re-enumerating the catalog, and obtaining an entry for a skill that a partial listing omitted. It is never a step a host must take to complete a listed entry.

Backward Compatibility

This extension introduces three protocol methods. skills/list and skills/get are implemented by every server declaring the extension, so a client only issues those calls after seeing the declaration, and a client that predates the extension never issues them. resources/directory/read is additionally gated behind the directoryRead capability setting, so a server that does not declare it never receives the call. The extension introduces no other methods, message types, or schema changes. A server that does not implement this extension simply exposes no skill:// resources; existing clients are unaffected. A client that does not implement this extension sees skill:// resources as ordinary resources, which they are.

Existing implementations using other skill:// URI structures will need to adjust to conform. See the Working Group's related-work survey for a catalog. Notably, FastMCP's widely-used SkillsProvider diverges on URI structure, discovery (per-skill _manifest vs. central index), and metadata mapping; coordinating that migration is a near-term Working Group priority. These are mechanical changes, not semantic ones.

Security Implications

Skill content is instructional text delivered to a model, which makes it a prompt-injection surface (background in open-questions.md §10). This extension imposes the following requirements:

  • Skill content is untrusted input. Hosts MUST treat MCP-served skill content as untrusted model input, subject to the same prompt-injection defenses applied to any server-provided text. A server being connected does not make its skill content authoritative.
  • Origin MUST be visible to the model. Hosts MUST tag MCP-served skill content with its originating server identity at the point it enters model context and MUST NOT present an MCP-served skill to the model as indistinguishable from a local filesystem skill. The model, not the host, decides whether to follow a skill's instructions. Withholding origin from it makes the untrusted-input requirement above unenforceable at the layer that acts on it.
  • Skills introduce host-side surfaces that tools do not. Unlike a remote tool call, an MCP-served skill can place server-authored bytes on the host filesystem and direct the model to execute them with host-side tools. Hosts MUST treat MCP-served skills as a higher-risk surface than remote tool invocation.
  • No implicit local execution. Hosts MUST NOT allow MCP-served skill content to cause host-side code execution without explicit per-skill user approval. This covers (a) declarative fields the host parses (hooks, frontmatter scripts) and (b) instructions in the skill body that direct the model to invoke any host code-execution tool, whether to run a script bundled in the skill or to run an arbitrary command the skill specifies. Hosts MUST ignore or approval-gate (a), and MUST apply the same approval gate to code-execution tool calls issued while the model is acting on an MCP-served skill.
  • Origin-scoped resource reads. A model-callable resource-read surface (such as the read_resource pattern in Hosts: End-to-End Integration) is a cross-server confused-deputy vector when driven by untrusted skill content. Hosts MUST bind such reads to the skill's originating server: a skill served by server A MUST NOT cause a resources/read against server B. Hosts MUST identify servers by a host-assigned label, not the server's self-reported serverInfo.name. Any cross-origin read MUST be gated behind explicit per-call user approval naming both servers.
  • Name collisions are an impersonation surface. Skill names are not unique across origins, and a malicious server can publish a skill under the name of a popular one, whether another server's or the user's own local skill, counting on the host resolving its way. Hosts MUST resolve skill names within a per-origin namespace, identifying servers by a host-assigned label, not the self-reported serverInfo.name; MUST NOT let an MCP-served skill silently shadow, replace, or intercept invocations of a same-named skill from any other origin, including the host's filesystem skills; and SHOULD surface collisions to the user. A name binds to whatever bytes its origin currently serves and carries no authorship or endorsement. Intermediaries MAY attach provenance or verification annotations via _meta under their own reverse-domain prefix, not the io.modelcontextprotocol.skills/ prefix reserved for this extension (Resource Metadata); this extension assigns such annotations no semantics.
  • No implicit permission grants. Hosts MUST NOT honor frontmatter fields that widen the model's tool or filesystem permissions when the skill arrives over MCP. In particular, the Agent Skills allowed-tools field, which a filesystem-sourced skill uses to declare the tools available while it runs, MUST be ignored for MCP-origin skills unless the user has explicitly approved that grant for that skill. A remote server populating allowed-tools is requesting elevated access on the host, not declaring a property of its own environment. Approval of a skill never extends to the frontmatter of any other SKILL.md within its file space: a nested skill's allowed-tools has no effect unless that nested skill is itself activated under its own approval (Nested skills).
  • Skills are data, not directives. Hosts MUST NOT treat skill resources as higher-authority than other context. Explicit user policy governs whether a skill is loaded at all.
  • Nested skill consent. Approval is per skill: approving a skill does not approve skills nested within it. Activating a nested SKILL.md requires fresh, explicit user consent, per Nested skills. Silently promoting a file of an approved skill to an active skill would let a server ride new instructions and permission requests in on a prior approval.
  • Provenance and inspection. Hosts SHOULD indicate which server a skill originates from when presenting it, SHOULD let users inspect a skill's content before it is loaded into model context, and MAY gate loading behind per-skill or per-server user approval.
  • Digests are not a security boundary. Listing digests are unsigned and come from the same server as the content. They confirm consistency between the listing and what was fetched, as described in Integrity and verification, but they cannot establish trust in the content, defend against the server itself, or detect an intermediary that rewrites both together.
  • Content-bound approval. When a host persists any per-skill user approval, it MUST be bound to the entry's resources set (every uri and digest) observed at the moment of approval. If a subsequent entry for that skill, from skills/list or skills/get, advertises a different set, whether a file was rotated, added, or removed, the host MUST treat the prior approval as revoked and re-prompt before loading or executing. A host need not poll for changes. While it is acting on the skill (Integrity and verification), content that has moved fails verification when read; and if it does fetch a fresh entry, the rule above revokes the approval. Neither path lets moved content through under the old approval. A skill whose resources is "dynamic" cannot be content-bound: hosts MAY decline to load it, and MUST NOT treat a persisted approval as covering whatever content the server currently serves. Digest verification (Integrity and verification) defends the approval after it is granted. It cannot establish that the content was trustworthy when the user approved it, because the server authors both the listing and the body.
  • Caching, cache integrity, cache isolation, and durable origin. Hosts SHOULD cache verified skill content locally, populated on demand as files are read rather than in bulk (Integrity and verification). A cache is a second copy of content that was verified once; the verification does not carry over to bytes that may have changed since. Hosts that cache skill content on disk MUST therefore do one of the following for every file served from the cache: keep the cache where nothing but the host can write to it (not the model, not scripts or tools the model runs, not other users of the machine) and never modify a cached file in place; or recompute the file's SHA-256 digest from the cached bytes on each access and compare it against the entry's digest, treating a mismatch exactly as a mismatch on a fresh read. Comparing a stored digest label, or a modification time, is not verification. Hosts that cache MCP-served skill content on disk MUST also do so in a location excluded from every filesystem-skill discovery path, and MUST treat content loaded from that location as having arrived over MCP for all purposes of the no-implicit-local-execution rule above, including after host restart and after the originating server is disconnected. Cached bytes do not graduate to filesystem-skill trust by residing locally. Hosts SHOULD remove a server's cached skill content when the user removes that server.

Reference Implementation

Per SEP-2133, an Extensions Track SEP requires at least one reference implementation in an official SDK prior to review.

SDK implementations:

Conformance tests:

  • Client and server scenarios with the SEP traceability file (sep-2640.yaml): conformance#330

Prototype host implementations (reading skill:// resources, surfacing skills alongside filesystem skills):

Prototype server implementation:

Appendix: Deferred Features

Features recorded here appeared in earlier revisions of this SEP and were removed before review concluded. They are not part of this extension. Each is kept on record with the objections that removed it, so that any future proposal to reintroduce one starts from those objections rather than rediscovering them.

Archive Distribution

An earlier revision let a skill entry advertise pre-packed archives of the entire skill directory (gzip-compressed tar and ZIP) as an alternative retrieval form alongside the skill's uri. A host could fetch a multi-file skill in a single resources/read rather than one per file, and an archive could carry UNIX file metadata (executable bits, symlinks) that individually served resources cannot represent.

The Core Maintainers removed archives during review, for two reasons:

  • Unpacking is an attack surface disproportionate to the benefit. Safely extracting an archive supplied by a remote server means defending against decompression bombs, path traversal, links resolving outside the skill directory, case- and Unicode-normalization collisions that silently overwrite SKILL.md, setuid and setgid bits, and non-regular file entries such as device nodes. Every host would have to implement that checklist correctly, and a host that got any item wrong would be exploitable by any server it connects to. Serving a skill as individually addressable resources has no comparable surface.
  • Two ways to serve one skill is a compatibility hazard. Archives were a second encoding of content the protocol could already express. Hosts would have to support both forms to be certain of reading any skill, and a skill offered only as an archive would be unreadable to a host that implemented individual-file reads alone. A single retrieval form keeps the compatibility floor flat: any conforming host can read any conforming skill.

The cost of removal is the one archives were introduced to address: a skill with many supporting files takes one round trip per file, and executable bits and symlinks have no representation. Because hosts retrieve files only as they are needed (Integrity and verification), that cost scales with the files a session actually uses rather than with the size of the skill. Should archives be reconsidered, the questions to settle first are how to bound host-side unpacking risk, perhaps by restricting the format to a profile admitting no symlinks, no non-regular entries, and a declared uncompressed size; and how to keep an archive strictly an optimization, never the sole way to retrieve a skill, so that the compatibility floor stays flat. An archive form would now also be required to unpack to exactly the file set enumerated in the entry's resources (Resources).

References

SEP-2643 Structured Authorization Denials

Draft · Extensions Track · Created 2026-04-20

Imported from upstream. Proposed as modelcontextprotocol#2643 by @monmohan on 2026-04-24, 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 defines an authorization denial envelope for the Model Context Protocol. When an MCP server refuses a request on authorization grounds today, the client has no reliable way to tell what kind of failure it is or what to do next. The only mechanism that carries that information is the HTTP WWW-Authenticate challenge, and the stdio transport does not have it.

The envelope is a single object with the same shape on every transport, carried in the _meta field of the result that expresses the denial. It tells the client whether there is a way forward, and when there is, it carries remediation hints describing what the client should do. New hint types can be defined by later SEPs without changing the envelope. Where an HTTP WWW-Authenticate challenge already describes the remediation in full, that challenge can be the entire denial, and a server that answers with a challenge alone sends no envelope.

Sometimes the client's credential does not allow the operation, and the fix is to obtain a new one using OAuth scopes or Rich Authorization Requests (RFC 9396). Sometimes the credential is valid but a person has to approve the action first, and the fix is for the server to record that approval. This SEP covers both, including approvals that outlive the response.

This SEP is fully backward compatible. A client that does not recognize the _meta key ignores it, so existing OAuth clients and libraries do not need to change.

Motivation

  1. Stdio has no defined authorization challenge mechanism: The MCP specification defines authorization procedures for the HTTP transport, including OAuth 2.1 flows, WWW-Authenticate challenges, and Protected Resource Metadata discovery. These mechanisms explicitly exclude stdio, which the specification directs to rely on credentials retrieved from the environment instead. As a result, a stdio MCP server that needs to reject a request on authorization grounds has no standard mechanism to communicate that the failure is an authorization problem, to indicate the nature of the required remediation, or to carry structured data the client can act on. Additionally, current guidance is that stdio servers that require OAuth remediation steps must handle the OAuth flow themselves and act as the OAuth client. This obscures the identity of the MCP client from other participants in the system.

  2. Support for signaling server-side state remediation: Some authorization denials are not about the client's credential. The MCP server may deny a request because it has insufficient information to contact an external system, needs confirmation from the user, or another piece of server-side state must be established before the request can succeed. The decision may come from something other than the user at the client, such as a reviewer, a policy engine or an external ticketing system, and it may not arrive until long after the server has replied. OAuth authorization challenges such as insufficient_scope or invalid_token are shaped around credential changes and do not cover either case. MCP has URL-mode elicitation as a primitive for out-of-band user interaction, and the Tasks extension as a primitive for work that outlives the response, but neither is an authorization denial signaling mechanism.

  3. Support for structured remediation data at denial: In some cases, the server can safely share data with the client that helps with remediation. For example, OAuth 2.0 defines Rich Authorization Requests (RFC 9396) for conveying structured authorization requirements, but only as part of the authorization request flow. An IETF OAuth Working Group draft, draft-ietf-oauth-rar-metadata-remediation, proposes a mechanism for HTTP by defining a new WWW-Authenticate error code, insufficient_authorization, with the structured data placed in the authorization_remediation header error parameter, which contains a base64url-encoded JSON object. The draft is HTTP-specific, and this SEP proposes to adopt the same error-remediation pattern at the JSON-RPC layer so it applies across MCP transports.

  4. A denial does not indicate whether it can be remediated: A client that receives an authorization denial cannot determine whether remediation is possible. It therefore either reattempts authorization for a denial that no credential or approval can satisfy, or treats a remediable denial as final. A remediation may also exist that the server cannot safely disclose or express. OAuth authorization challenges cannot resolve this, because every error value they define names something the client is expected to obtain, and none of them indicates that nothing can be obtained.

Specification

Extension Identifier

This extension is identified as: io.modelcontextprotocol/structured-authorization-denials.

Authorization Denial Envelope

An MCP server that denies a request on authorization grounds SHOULD include an authorization denial envelope in the _meta field of the result, under the key io.modelcontextprotocol/authorization. The envelope is a single object. Its shape does not vary with the transport, nor with the result that carries it.

The shape of the envelope is:

{
  "reason": "insufficient_authorization",
  "remediation": "<available|unavailable|undisclosed>",
  "authorizationContextId": "<correlation-handle>",
  "remediationHints": [
    {
      "type": "<remediation-type>"
      /* additional members determined by the remediation type */
    }
  ]
}

The fields of the envelope are defined as follows:

  • reason (string, REQUIRED). Indicates that the failure is an authorization denial and classifies its general nature. This SEP defines a single value, insufficient_authorization. Additional values MAY be defined by future SEPs.
  • remediation (string, REQUIRED). Whether the denial can be acted on. Exactly one of:
    • available. A remediation exists and the client MAY pursue it.
    • unavailable. No remediation exists. The client MUST NOT retry the request unchanged and MUST NOT begin a reauthorization flow.
    • undisclosed. A remediation may exist and the server does not describe it. The client MUST NOT retry the request unchanged and MUST NOT infer that no remediation exists.
  • authorizationContextId (string, OPTIONAL). A server-issued correlation handle. When the server includes this field, the client MUST echo it on retry as described in "Retry Echo via _meta". The handle is not authorization material, and the server MUST NOT rely on it for authorization decisions.
  • remediationHints (array of objects, OPTIONAL). Structured remediation hints. Each hint is an object with a type field (string, REQUIRED) naming the remediation mechanism, and zero or more additional members whose names and shapes are determined by the value of type. Clients MUST ignore remediation hints whose type they do not recognize. This member MUST NOT be present when remediation is unavailable or undisclosed. A remediation of available does not require it, because a transport-level challenge or the response shape itself may carry the remediation.

The presence of the envelope means the requested operation has not been performed. A server MUST NOT attach the envelope to a result in which the operation partially executed. A failure occurring after the operation has begun is reported as an ordinary failure of the operation, without the envelope.

Where the envelope attaches

The envelope attaches to whichever result the server uses to express the denial, so its location depends on the case.

# Case Response remediation Envelope
1 The user must complete an interaction at a URL InputRequiredResult available In _meta, with a url hint
2 Remediation is proceeding in the background, and the client has declared Tasks support CreateTaskResult, and each tasks/get result while the task is non-terminal available In _meta, with a task hint
3 A new credential is needed and the server does not carry the remedy in a transport challenge CallToolResult with isError: true available In _meta, with an authorization_remediation hint
4 The denial is final, or the server does not describe a remediation CallToolResult with isError: true unavailable or undisclosed In _meta, with no hints
5 On HTTP, the server carries the remedy in a transport challenge HTTP status line and WWW-Authenticate, optionally with a CallToolResult with isError: true available Optional. In _meta, with or without an authorization_remediation hint

In case 5, where the server also sends the envelope, the transport challenge is authoritative for the remedy and the envelope adds the classification. Where it sends the challenge alone there is no result and therefore no envelope. Every challenge below means reason: insufficient_authorization and remediation: available, and differs only in which parameter carries the remedy.

Challenge Remedy carried in WWW-Authenticate What the client does
error="insufficient_scope" (RFC 6750) scope Obtains a token for the named scopes and retries
error="insufficient_authorization" (draft-ietf-oauth-rar-metadata-remediation) authorization_remediation, base64url RAR Decodes it and sends the authorization_details to the authorization server, then retries
error="transaction_authorization_required" (draft-rosomakho-oauth-txn-challenge-00) transaction_challenge, a JWS Posts the JWS to the authorization server's transaction authorization endpoint and retries with the issued token

No WWW-Authenticate parameter expresses remediation: unavailable or undisclosed. A bare 403 carrying none of these parameters therefore cannot be classified from the challenge alone. A challenge can express only a token remedy, so a remediation requiring a page visit has no case 5 form even on HTTP.

Retry Echo via _meta

When a server includes an authorizationContextId in the authorization denial envelope, the client MUST echo the value, verbatim, in the _meta block of the retry request under the key io.modelcontextprotocol/authorization-context-id.

Example retry after a denial carrying authzctx_7c5d1d79:

{
  "jsonrpc": "2.0",
  "id": 18,
  "method": "tools/call",
  "params": {
    "name": "files.list_folder",
    "arguments": { "folderId": "fld_12345" },
    "_meta": {
      "io.modelcontextprotocol/authorization-context-id": "authzctx_7c5d1d79"
    }
  }
}

The authorizationContextId ties a denial to the retry that follows it, and gives the server a stable value for audit and telemetry. It does not locate server-side state. Where a remediation requires the server to resume work it began at denial time, that state travels with the response that expressed the denial, such as requestState on an InputRequiredResult.

To preserve backward compatibility with clients that do not implement this SEP, servers MUST NOT reject a retry whose handle is absent or unresolvable. A retry that omits the handle is processed normally, and the server loses the correlation between the denial and the retry. The authorizationContextId MUST NOT be treated as sufficient authorization. On retry the server performs a fresh authoritative evaluation using the presented credential, current server-side state, the requested operation and any other relevant context.

Use Case 1 — Server-side state remediation

This use case covers denials where the client's credential is valid and does not need to change. Remediation establishes server-side state, such as an approval or a selection of resources, through an interaction that takes place outside the request.

This SEP defines two such interactions. The server indicates which one applies with a remediationHints entry of type url or task. In the first, the interaction is with the user at the client, who opens a URL and completes it while the request is in flight. In the second, the interaction is with someone else, such as a reviewer, a policy engine or a ticketing system, and the decision can outlive the response.

Interaction 1 — Completed by the user at a URL

A representative example is a file picker in a cloud storage service, where the user's OAuth token is unchanged and the server records an approval tied to the user.

The server expresses the denial as an InputRequiredResult whose inputRequests map carries an elicitation/create request in url mode. The envelope MUST include a remediationHints entry of type url. That hint carries no pointer to the elicitation, because the inputRequests map is where the client reads the URL.

Elicitation modes are negotiated, and a server must not request a mode the client does not support, so this interaction is available only when the denied request declared elicitation.url in its client capabilities.

Example denial:

{
  "jsonrpc": "2.0",
  "id": 17,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "folder_approval": {
        "method": "elicitation/create",
        "params": {
          "mode": "url",
          "url": "https://mcp.example.com/approve-folder-access",
          "message": "Open this page to approve access to the requested folder."
        }
      }
    },
    "requestState": "eyJhcHByb3ZhbCI6ImZsZF8xMjM0NSJ9",
    "_meta": {
      "io.modelcontextprotocol/authorization": {
        "reason": "insufficient_authorization",
        "remediation": "available",
        "authorizationContextId": "authzctx_7c5d1d79",
        "remediationHints": [{ "type": "url" }]
      }
    }
  }
}

After the user completes the approval, the client retries the original request. It returns the elicitation outcome in inputResponses and passes requestState back unchanged as described in Multi Round-Trip Requests. It also echoes the authorizationContextId as described in "Retry Echo via _meta":

{
  "jsonrpc": "2.0",
  "id": 18,
  "method": "tools/call",
  "params": {
    "name": "files.list_folder",
    "arguments": { "folderId": "fld_12345" },
    "inputResponses": {
      "folder_approval": { "action": "accept" }
    },
    "requestState": "eyJhcHByb3ZhbCI6ImZsZF8xMjM0NSJ9",
    "_meta": {
      "io.modelcontextprotocol/authorization-context-id": "authzctx_7c5d1d79"
    }
  }
}
Interaction 2 — Decided out of band

The decision is reached outside the request and can outlive the response. A server that denies a request on authorization grounds and can remedy it only through such a decision MUST NOT carry out the request while the decision is pending. It returns a task that holds the request instead. The result is a CreateTaskResult, and its envelope MUST include a remediationHints entry of type task. The Tasks extension requires that a server never return a task to a client that has not declared it, so this interaction is available only when the denied request declared io.modelcontextprotocol/tasks in its client capabilities.

{
  "jsonrpc": "2.0",
  "id": 55,
  "result": {
    "resultType": "task",
    "taskId": "task_3f91b2",
    "status": "working",
    "ttlMs": 604800000,
    "pollIntervalMs": 5000,
    "_meta": {
      "io.modelcontextprotocol/authorization": {
        "reason": "insufficient_authorization",
        "remediation": "available",
        "authorizationContextId": "authzctx_9e4c02b1",
        "remediationHints": [{ "type": "task" }]
      }
    }
  }
}

While the task is not in a terminal state, the server MUST repeat the envelope on every tasks/get result. A client that resumes polling after a restart holds only the taskId it read from its own storage, and the response that created the task is gone, so without the repetition it cannot tell that the task is waiting on an authorization decision.

{
  "jsonrpc": "2.0",
  "id": 61,
  "result": {
    "taskId": "task_3f91b2",
    "status": "working",
    "_meta": {
      "io.modelcontextprotocol/authorization": {
        "reason": "insufficient_authorization",
        "remediation": "available",
        "authorizationContextId": "authzctx_9e4c02b1",
        "remediationHints": [{ "type": "task" }]
      }
    }
  }
}

The task holds the original request, so the client does not retry it and there is nothing to echo. When the decision allows the request, the server evaluates authorization afresh and then carries it out, and the task reaches completed with the outcome in its result field. When the decision refuses it, the task also reaches completed, because a refusal is an outcome of the call rather than a failure of the task, and the result field carries the denial.

{
  "jsonrpc": "2.0",
  "id": 74,
  "result": {
    "taskId": "task_3f91b2",
    "status": "completed",
    "result": {
      "resultType": "complete",
      "isError": true,
      "content": [
        { "type": "text", "text": "The access request was not approved." }
      ],
      "_meta": {
        "io.modelcontextprotocol/authorization": {
          "reason": "insufficient_authorization",
          "remediation": "unavailable",
          "authorizationContextId": "authzctx_9e4c02b1"
        }
      }
    }
  }
}

This SEP defines how an authorization denial is expressed while a decision is pending and once it has been made. How the server manages the pending decision, including how it records what was requested and how it guarantees the request runs no more than once, is outside its scope.

A server knows which interactions are open to it, because a request carries the client's capabilities. When the denied request declared neither elicitation.url nor io.modelcontextprotocol/tasks, the server has no interaction to offer. It returns a CallToolResult with isError: true carrying an envelope whose remediation is undisclosed, and describes the remedy in content. The server MUST NOT use unavailable in this case, because the remedy exists and a client that declared either capability would be offered it.

Use Case 2 — New credential issuance with broader authorization

This use case covers denials where the client's existing credential carries insufficient authorization for the requested operation and remediation requires obtaining a new credential. This new credential issuance may involve additional OAuth scopes, a Rich Authorization Requests authorization_details object (RFC 9396), or any other mechanism that adds the required authorization to the new credential. In contrast with Use Case 1, the failure cannot be resolved by server-side state changes alone.

On HTTP, where the WWW-Authenticate challenge describes the remediation in full, a server can answer with the challenge alone. It then returns no MCP result, so there is no envelope, and the client derives reason and remediation from the challenge as described in "Where the envelope attaches".

Where the server expresses the denial as a result, which is always the case on stdio because the MCP specification defines no authorization challenge there, the envelope MAY carry a remediationHints entry of type authorization_remediation with the following members:

  • authorization_details (array, REQUIRED). An array of actionable authorization details objects, in the format RFC 9396 specifies for the authorization_details request parameter, built from the failed request. A new credential granted for these authorization details satisfies what the denied request required.
  • authorization_reference (string, RECOMMENDED). An opaque value generated by the server that allows the client to select an existing credential associated with equivalent authorization details, without interpreting those details. Matching only works if the value is stable across semantically equivalent authorization details. OAuth 2.0 RAR Metadata and Error Remediation describes one approach to generating such values. Clients MUST treat the value as opaque. The server MUST NOT include this member when credentials issued for the provided authorization_details are intended for single use.

A client that has obtained a remediation, whether from the transport challenge or from an authorization_remediation hint, MAY retry with a credential it already holds that matches the authorization_reference, and otherwise uses the authorization_details in a new OAuth authorization request. On HTTP transports, a client that does not recognize the remediation SHOULD fall back to resource-metadata and authorization-server-metadata discovery.

Example 1 — Scope challenge

Consider an MCP server where the read scope covers read-only tools (files.list, files.get) and the write scope is required for modifying tools (files.update). The client's access token holds only the read scope, and its call to files.update is denied:

HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
    scope="read write",
    resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"

The client performs OAuth reauthorization using the transport challenge, obtains a new access token whose scope includes both read and write, and retries the original request with the new token in the Authorization header:

POST /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer at_new
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 18,
  "method": "tools/call",
  "params": {
    "name": "files.update",
    "arguments": { "fileId": "fl_98765", "content": "..." }
  }
}
Example 2 — Rich Authorization Requests remediation

When a denied request requires authorization beyond what OAuth scopes can express, the WWW-Authenticate challenge can carry the structured remediation requirements directly, so the client can construct its next authorization request without further metadata discovery. This uses the error-remediation pattern defined in OAuth 2.0 RAR Metadata and Error Remediation.

Consider an MCP server that exposes a payments.initiate tool where initiating a payment requires authorization details bound to the payee and amount. The client's current access token does not carry such authorization details, and the call is denied:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="insufficient_authorization",
    resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/payments",
    error_description="Additional authorization is required",
    authorization_remediation=eyJhdXRob3JpemF0aW9uX2RldGFpbHMiOlt7InR5cGUiOiJwYXltZW50X2luaXRpYXRpb24i...
Cache-Control: no-store

The client may retry the failing call using an existing valid credential if it matches a provided authorization_reference. Otherwise the client uses the authorization_details from WWW-Authenticate header's authorization_remediation parameter, to construct an OAuth authorization request, per RFC 9396#section-2:

GET /authorize?
    response_type=code&
    client_id=client-123&
    redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&
    scope=payments%3Aapprove&
    authorization_details=%5B%7B...payment_initiation...%7D%5D HTTP/1.1
Host: as.example.com

After obtaining a new access token, the client retries the original request:

POST /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer at_new
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 32,
  "method": "tools/call",
  "params": {
    "name": "payments.initiate",
    "arguments": {
      "amount": "123.50",
      "currency": "EUR",
      "creditorName": "Merchant A",
      "creditorAccount": "DE02100100109307118603",
      "remittanceInformationUnstructured": "Ref Number Merchant"
    }
  }
}
Example 3 — Remediation over stdio

On stdio there is no status line and no headers, so the result is the entire denial. An MCP server that exposes payments.initiate denies the call because the authorization it holds does not cover the payment, and carries the requirement in the envelope:

{
  "jsonrpc": "2.0",
  "id": 41,
  "result": {
    "resultType": "complete",
    "isError": true,
    "content": [
      {
        "type": "text",
        "text": "Current authorization is insufficient for this payment."
      }
    ],
    "_meta": {
      "io.modelcontextprotocol/authorization": {
        "reason": "insufficient_authorization",
        "remediation": "available",
        "authorizationContextId": "authzctx_c17a3f5e",
        "remediationHints": [
          {
            "type": "authorization_remediation",
            "authorization_details": [
              {
                "type": "payment_initiation",
                "actions": ["initiate", "status", "cancel"],
                "instructedAmount": {
                  "currency": "EUR",
                  "amount": "123.50"
                },
                "creditorName": "Merchant A",
                "creditorAccount": {
                  "iban": "DE02100100109307118603"
                },
                "remittanceInformationUnstructured": "Ref Number Merchant"
              }
            ]
          }
        ]
      }
    }
  }
}

No authorization_reference is present, because the credential issued for these authorization details is intended for a single payment.

The client constructs an OAuth authorization request from the authorization_details as in Example 2, obtains a credential, and makes it available to the server through the same channel that provisioned the server's original credential. It then retries the original request, echoing the authorizationContextId:

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "payments.initiate",
    "arguments": {
      "amount": "123.50",
      "currency": "EUR",
      "creditorName": "Merchant A",
      "creditorAccount": "DE02100100109307118603",
      "remittanceInformationUnstructured": "Ref Number Merchant"
    },
    "_meta": {
      "io.modelcontextprotocol/authorization-context-id": "authzctx_c17a3f5e"
    }
  }
}

Denials with no remediation to describe

A denial that carries no hints is the shape remediation exists to disambiguate, because nothing else in the result distinguishes a final denial from one whose remedy the server will not describe:

{
  "jsonrpc": "2.0",
  "id": 71,
  "result": {
    "resultType": "complete",
    "isError": true,
    "content": [
      { "type": "text", "text": "You do not have access to this workspace." }
    ],
    "_meta": {
      "io.modelcontextprotocol/authorization": {
        "reason": "insufficient_authorization",
        "remediation": "unavailable",
        "authorizationContextId": "authzctx_2b7e9c"
      }
    }
  }
}

A server that holds a remedy it will not describe sends the same result with remediation set to undisclosed. The authorizationContextId is still useful on both, because a caller who asks why the request failed needs something to quote.

Rationale

This SEP standardizes the smallest set of facts that an MCP client and server need to communicate after an authorization denial: what classification the failure has, how the retry is correlated, and what remediation posture the server can describe. Any further extensions, whether a new remediation type or a new transport binding, can be added additively for future needs without changing the envelope itself. For example, a future remediationHints type could describe a Client Initiated Backchannel Authentication (CIBA) flow, in which per-operation approvals occur on a separate channel such as a push notification rather than interrupting the foreground session. A new type could equally give an existing challenge a carrier it lacks. The challenge table above carries an OAuth transaction authorization challenge only in a WWW-Authenticate parameter, and a future type could carry the same signed challenge in the envelope, so that remediation becomes expressible on a transport that defines no challenge layer.

The SEP composes with existing MCP primitives rather than inventing parallel shapes. Use Case 1 reuses url-mode elicitation where the user completes the interaction while the request is in flight, and the Tasks extension where the decision outlives the response, and in both the envelope rides in the result's _meta. Clients that already understand those primitives continue to work unchanged, and clients that additionally understand the envelope receive transport-agnostic classification and a correlation handle.

A denial's structured requirement can reach the client through a transport mechanism that carries it alongside the failure signal, or through the envelope. MCP's stdio transport has no challenge layer, so there the envelope is the only carrier available. Defining the requirement's shape at the JSON-RPC layer means one denial shape works identically on every MCP transport, and the extension itself depends on no transport-level mechanism.

Backward Compatibility

All changes introduced by this SEP are additive. The envelope is carried in the result's _meta under a key that a client which does not implement this SEP will not recognize, and unrecognized _meta members are ignored, so such a client processes the result exactly as it does today.

Use Case 1 uses url-mode elicitation and the Tasks extension exactly as they are defined, and adds only the _meta key, so a client that implements those primitives needs no change to handle the denial.

Transport-level authorization mechanisms are not modified, and existing OAuth libraries and resource-server configurations require no changes.

A client that does not implement this SEP does not echo authorizationContextId on retry. Per the Retry Echo requirement that servers not reject solely on handle absence, such a client continues to be evaluated on the credential it presents.

Security Implications

URL safe handling and phishing mitigation

The MCP elicitation specification's safe URL handling and phishing requirements apply to any URL carried in a denial whose envelope has a remediationHints entry of type url.

Correlation handle is not authorization material

The authorizationContextId field is a correlation handle, not authorization material. Possession of it grants nothing, because on retry the server evaluates authorization afresh using the presented credential, current server-side state and the requested operation.

Information disclosure through the correlation handle

The handle gives the server a stable value for audit and telemetry, so it reaches logs and traces that outlive the exchange and that are read by parties other than the client. Servers SHOULD generate the authorizationContextId as an opaque value, and SHOULD NOT encode in it the operation, the resource, the policy that denied the request, or any information about the user.

Information disclosure through remediation hints

Remediation hints describe what a client should do in response to a denial. They MUST NOT carry information that would itself require authorization to read, such as the identity of another user whose approval would be required or the contents of a resource the client cannot access.

Integrity of remediation hints

A remediation hint tells the client what to do next, so an intermediary that can modify the response can substitute a remediation URL or alter the authorization_details a client will send to its authorization server. A client MUST NOT act on a remediation hint, or on the URL of a url-mode elicitation accompanying a denial, unless it has assurance that the response originated from the MCP server it addressed, and an authenticated connection to an intermediary is not that assurance. This SEP mandates no mechanism for establishing it. Deployments on HTTP MAY use HTTP Message Signatures (RFC 9421) with Digest Fields (RFC 9530).

Client-side modification of authorization_details

The authorization_details a server places in a denial reach the authorization server by way of the client, which can change them along the way. An MCP server MUST NOT treat a token as authorizing an operation because it carries authorization details resembling the ones the denial asked for, and MUST validate the token against the operation actually requested on retry. Servers SHOULD NOT place anything in authorization_details that they would be unwilling for the client to see or alter.

Reference Implementation

TBD

Acknowledgments

This proposal was developed through discussion in the MCP Fine-Grained Authorization Working Group and refined across review cycles before being presented as a SEP, with input from:

  • Nate Barbettini (MCP FGA Working Group lead)
  • Yaron Zehavi (OAuth RAR/Payment Industry expertise; author of OAuth 2.0 RAR Metadata and Error Remediation, adopted by the IETF OAuth Working Group)
  • Justin Richer (OAuth/RAR/GNAP expertise)
  • Max Gerber (Design Reviews)
  • Karl McGuinness (author of SEP-2848)
  • Mats Sundvall (Review feedback)

SEP-2663 Tasks Extension

Final · Extensions Track · Created 2026-04-27

Abstract

This SEP defines an extension that allows a server to respond to a tools/call request with an asynchronous task handle instead of a final result, allowing the client to retrieve the eventual result by polling. The extension introduces three methods: tasks/get, tasks/update, and tasks/cancel; a polymorphic-result discriminator (resultType: "task"); and a Task shape that carries a task status, in-progress server-to-client requests, and a final result or error. Task creation is server-directed: the client signals support by including the extension in its per-request capabilities, and the server decides on a per-request basis whether to materialize a task.

Tasks will become a foundational building block of MCP and are expected to be supported in future protocol versions. The experimental tasks feature in the 2025-11-25 specification served as a stopgap until the protocol's extension mechanism was available. Now that extensions have been formalized, moving tasks to an official extension gives the feature time to incubate and evolve based on additional real-world implementation feedback, without being constrained by the core specification's release cadence. Once the extension has stabilized and achieved broad adoption, it is intended to be promoted into the core protocol.

This proposal removes the version of tasks specified in the 2025-11-25 release from the core protocol and moves it to an Extension. It also proposes updates to Tasks shaped by implementation feedback since that release, and by several changes to the base protocol included in the 2026-06-30 specification:

Motivation

The experimental tasks feature served as an alternate execution mode for tool calls, elicitation, and sampling, allowing receivers to return a poll handle instead of blocking until a final result was ready. Implementation experience surfaced several challenges:

  1. The handshake is fragile. Tasks today expose method-level capabilities (tasks.requests.tools.call declares that tools/call MAY be task-augmented) alongside a tool-level execution.taskSupport field that declares whether a particular tool will accept the augmentation. Clients express their own support for tasks by passing a task parameter on their requests, but MUST NOT include it if the method/tool does not support tasks. A client that wants to opt into tasks must therefore prime its state with a tools/list call before issuing any task-augmented request, and cannot blindly attach a task parameter to every request to handle tools isomorphically. This is confusing, implicit, and easy to get wrong.

  2. tasks/result is a blocking trap. In the current flow, a client that observes input_required is required to call tasks/result prematurely so that the server has an SSE stream on which to side-channel elicitation or sampling requests. tasks/result then blocks until the entire operation completes. This forces long-lived persistent connections that many clients and servers do not want to implement, and it conflicts with SEP-2260, which disallows unsolicited server-to-client requests outright. Under SEP-2260, the SSE semantics that justified the blocking behavior no longer apply.

  3. tasks/list scoping cannot be defined. To avoid clients cancelling or retrieving results for tasks they shouldn't have access to, all tasks should be bound to some sort of "authorization context," the implementation of which is left to individual servers according to their existing bespoke permission models. However, in many cases, it is not possible to perform this binding, in which case the task ID becomes the only line of defense against contamination. In this scenario, it is unsafe for a server to support tasks/list at all. While it was possible for tasks to instead be bound to a session, SEP-2567 removes sessions from the protocol. There is no other natural scope a server can define unilaterally — task IDs can be unguessable handles that a server can recognize one at a time, but servers cannot reliably correlate two unrelated handles to the same caller without additional state.

Beyond implementation challenges, tasks face another structural issue: Client-hosted tasks are no longer expressible. SEP-1686 permitted clients to host tasks for elicitation and sampling, in part to avoid coupling tasks to tool calls. SEP-2260 makes any unsolicited server-to-client request invalid; every server-to-client polling request under client-hosted tasks would be unsolicited by definition.

This proposal intends to solve the above issues by redesigning certain aspects of the feature and moving tasks out to an official extension. Redefining tasks as an official extension gives the feature more time to incubate and evolve independently of the core specification, promoting adoption. As part of the redesign, this proposal consolidates the polling lifecycle into tasks/get and a new tasks/update to remove the blocking tasks/result method. The redesign allows servers to return tasks unsolicited (in response to ordinary, non-task-flagged requests) to eliminate the per-request opt-in and the tools/list warmup, relying instead on the extension capability as the single handshake point. Finally, this proposal removes client-hosted elicitation and sampling tasks in compliance with SEP-2260.

Specification

The MCP Tasks extension allows certain requests to be augmented with tasks. Tasks are durable state machines that carry information about the underlying execution state of the request they augment, and are intended for client polling and deferred result retrieval. Each task is uniquely identifiable by a server-generated task ID.

Tasks are useful for representing expensive computations and batch processing requests, and map naturally onto external job APIs.

Extension Identifier

This extension is identified as: io.modelcontextprotocol/tasks.

Capability Negotiation

The client and server declare support for the tasks extension in their respective capabilities objects (using updated form from SEP-2575: Make MCP Stateless):

// Client to server, in per-request capabilities
{
  // Other request parameters...
  "params": {
    "_meta": {
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": {
          "io.modelcontextprotocol/tasks": {},
        },
      },
    },
  },
}
// Server to client, in response to server/discover
{
  "result": {
    // Other response parameters...
    "capabilities": {
      "extensions": {
        "io.modelcontextprotocol/tasks": {},
      },
    },
  },
}

No extension-specific settings are currently defined; an empty object indicates support.

A server that has negotiated this extension MAY return CreateTaskResult in lieu of a standard result (e.g. CallToolResult) in response to any supported request at its own discretion and on a per-request basis. The server is the sole decider; clients do not signal task preference on the request itself. The client declaring the extension capability does not suggest that it requires a CreateTaskResult in response to that request.

A server MUST NOT return CreateTaskResult to a client that did not include the extension capability on its request, regardless of prior declarations. A client that has negotiated this extension MUST be prepared to handle either CallToolResult or CreateTaskResult in response to any supported request it issues. A client that receives CreateTaskResult in response to an unsupported request type MUST interpret this as an invalid response to the request.

If a server is unable to service a request to a client that does not declare this extension capability without returning CreateTaskResult, the server MUST return an error with the code -32021 (Missing Required Client Capability), indicating the required extension in the error response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    // MISSING_REQUIRED_CLIENT_CAPABILITY
    "code": -32021,
    // Message provided for example purposes only. The content of this example message is non-normative.
    "message": "Missing required client capability",
    "data": {
      "requiredCapabilities": {
        "extensions": {
          "io.modelcontextprotocol/tasks": {}
        }
      }
    }
  }
}

Supported Methods

The following methods currently support task-augmented execution:

  • tools/call

This specification may be extended to support tasks over other request types in the future; implementations SHOULD be designed to accommodate additional request types in future revisions of this specification.

Polymorphic Results

A request that is eligible for task-augmentation may return one of two distinct result shapes — the request's standard result, or a CreateTaskResult. The discriminator is the resultType field on the result object, introduced by SEP-2322:

// "task" is introduced by this extension.
type ResultType = "complete" | "input_required" | "task" | string;

Servers MUST set resultType to "task" when returning a CreateTaskResult so that clients can distinguish it from a standard result. Servers MUST NOT set resultType to "task" on result types other than CreateTaskResult.

Client implementors are advised that existing code returning a fixed shape (e.g., a tools/call method returning CallToolResult) need not change their public contract — they can transparently drive the polling flow internally and surface only the final, completed result. New implementation surfaces MAY expose the task lifecycle directly for applications able to leverage it.

Tasks

A Task carries operational metadata about ongoing work.

interface Task {
  /** Stable identifier for this task. */
  taskId: string;

  /** Current task status. */
  status: "working" | "input_required" | "completed" | "cancelled" | "failed";

  /**
   * Optional message describing the current task state.
   * This can provide context for any status, for example (non-normative):
   * - Progress descriptions for "working"
   * - Work blocked on "input_required"
   * - Reasons for "cancelled" status
   * - Summaries for "completed" status
   * - Additional information for "failed" status (e.g., error details, what went wrong)
   *
   * This MAY be exposed to the end-user or model.
   */
  statusMessage?: string;

  /** ISO 8601 timestamp when the task was created. */
  createdAt: string;

  /** ISO 8601 timestamp when the task was last updated. */
  lastUpdatedAt: string;

  /**
   * Time-to-live duration from creation in integer milliseconds, null for unlimited.
   * The server may discard the task after the TTL elapses. This value MAY change
   * over the lifetime of a task.
   */
  ttlMs: number | null;

  /**
   * Suggested polling interval in integer milliseconds. Clients SHOULD honor
   * this value to avoid overwhelming the server. This value MAY change over
   * the lifetime of a task.
   */
  pollIntervalMs?: number;
}
Task Status

Tasks can be in one of the following states:

  • working: The request is currently being processed.
  • input_required: The server needs input from the client before the task can proceed. The tasks/get response will include outstanding requests in the inputRequests field. The client MUST inspect this field and SHOULD provide responses via the inputResponses field in subsequent tasks/update requests.
  • completed: The request completed successfully and results are available in the result field. This includes tool calls that returned results with isError: true.
  • failed: The request failed due to a JSON-RPC error during execution. The task will include the error field with the JSON-RPC error details. This status MUST NOT be used for non-JSON-RPC errors.
  • cancelled: The request was cancelled before completion.

Derived shapes of Task inline status-specific payload fields and are used by tasks/get responses and notifications/tasks notifications:

/**
 * A task that is in a normal working state.
 * Used by tasks/get and notifications/tasks.
 */
export interface WorkingTask extends Task {
  status: "working";
}

/**
 * A task that is waiting for input from the client.
 * Used by tasks/get and notifications/tasks.
 */
export interface InputRequiredTask extends Task {
  status: "input_required";
  /**
   * Server-to-client requests that need to be fulfilled during task execution.
   * Keys are arbitrary identifiers for matching requests to responses.
   */
  inputRequests: InputRequests;
}

/**
 * A task that has completed successfully.
 * Used by tasks/get and notifications/tasks.
 */
export interface CompletedTask extends Task {
  status: "completed";
  /**
   * The final result of the task.
   * The structure matches the result type of the original request.
   * For example, a CallToolRequest task would return the CallToolResult structure.
   */
  result: JSONObject;
}

/**
 * A task that has failed due to a JSON-RPC error.
 * Used by tasks/get and notifications/tasks.
 */
export interface FailedTask extends Task {
  status: "failed";
  /**
   * The JSON-RPC error that caused the task to fail.
   */
  error: JSONObject;
}

/**
 * A task that has been cancelled.
 * Used by tasks/get and notifications/tasks.
 */
export interface CancelledTask extends Task {
  status: "cancelled";
}

/**
 * A union type representing a task with optional inlined result/error/inputRequests fields.
 * This type is used by tasks/get and notifications/tasks to provide complete task state
 * including terminal results or pending input requests.
 */
export type DetailedTask =
  WorkingTask | InputRequiredTask | CompletedTask | FailedTask | CancelledTask;

Task Creation

A server returns CreateTaskResult in lieu of the standard result shape for a request to indicate that request will be processed asynchronously.

// resultType: "task"
type CreateTaskResult = Result & Task;

Example Request (CallToolRequest):

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "city": "New York"
    }
  }
}

Example Response (CreateTaskResult):

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "task",
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "status": "working",
    "statusMessage": "The operation is now in progress.",
    "createdAt": "2025-11-25T10:30:00Z",
    "lastUpdatedAt": "2025-11-25T10:40:00Z",
    "ttlMs": 60000,
    "pollIntervalMs": 5000
  }
}

The embedded task is the seed state for the task, typically (though not necessarily) with status: "working". The client uses task.taskId for all subsequent tasks/get, tasks/update, and tasks/cancel calls.

A server MUST NOT return CreateTaskResult until the task is durably created — that is, until a tasks/get for the returned taskId would resolve. In eventually-consistent environments, the server MUST wait for consistency before responding. This requirement eliminates the need for clients to speculatively poll for task creation.

Server implementations that use multi round-trip requests in conjunction with task creation (for example, a tool that requires elicitation over InputRequiredResult before creating a task) SHOULD resolve all MRTR exchanges synchronously before responding with a CreateTaskResult.

Task Polling

Clients poll for task completion by sending tasks/get requests.

Clients SHOULD respect the pollIntervalMs provided in responses when determining polling frequency. The pollIntervalMs MAY change over the lifetime of a task. Servers MAY rate-limit clients polling more frequently than the recorded pollIntervalMs.

Clients SHOULD continue polling until the task reaches a terminal status or until invoking tasks/cancel. Clients SHOULD persist task IDs to durable storage so that polling can resume after a crash or restart.

Request
interface GetTaskRequest extends JSONRPCRequest {
  method: "tasks/get";
  params: {
    /** Identifier of the task to query. */
    taskId: string;
  };
}
Response

Upon receiving a tasks/get request, the server MUST check the status of the task and respond accordingly:

  1. If the status is working, the server MUST return a a Task object with status working.
  2. If the status is input_required, the server MUST return a Task object with status input_required and an inputRequests field defined in Multi Round-Trip Requests. The inputRequests field MUST contain all outstanding requests from the server to the client that need to be fulfilled before the task can proceed.
  3. If the status is completed, the server MUST return a Task object with status completed and a result field containing the final result of the task.
  4. If the status is cancelled, the server MUST return a Task object with status cancelled.
  5. If the status is failed, the server MUST return a Task object with status failed and the error that occurred during execution.
type GetTaskResult = Result & DetailedTask;

The response carries the appropriate response variant for the task's current status (see Task Status). The resultType field MUST be set to "complete" on this object as it is the standard result shape for the tasks/get request.

If the task has a non-null ttlMs, clients MAY treat the TTL as a backstop: if the task's observable status has not reflected the update after createdAt plus ttlMs has elapsed, the client MAY consider the task to no longer be usable. Conversely, servers MAY mark a task as failed at any point after the TTL elapses, and subsequently delete it at any time. The value of ttlMs MAY change over the lifetime of a task.

Task Update Requests

When a task requires input from the client (indicated by the input_required status), the server includes outstanding requests in the inputRequests field of the tasks/get response (see Multi Round-Trip Requests). The client provides responses via the inputResponses field in one or more subsequent tasks/update requests.

When a client observes a tasks/get response (or notifications/tasks notification) with status: "input_required", the client SHOULD fulfill the outstanding requests in inputRequests by sending one or more tasks/update requests with corresponding inputResponses. After sending a tasks/update, the client SHOULD continue observing the task's status via polling (tasks/get) or notifications (notifications/tasks) until it reaches a terminal state.

Clients MUST treat each entry in inputRequests as they would the equivalent standalone server-to-client request — for example, an elicitation request surfaced via inputRequests is subject to the same trust model and user-facing behavior as a direct elicitation/create request. Clients SHOULD deduplicate inputRequests keys across consecutive polls to avoid presenting the same request to the user or model more than once.

Each request key in inputRequests MUST be unique over the lifetime of a single task. A server MUST NOT reuse a key for a subsequent server-to-client request after a response for that key has been delivered, and MUST NOT use the same key to refer to two distinct requests over a task's lifetime. This guarantees that inputResponses keyed by the same identifier always refer to the request the client expects, eliminates ambiguity for clients deduplicating across polls, and lets servers ignore inputResponses for unknown or already-satisfied requests.

Request
interface UpdateTaskRequest extends JSONRPCRequest {
  method: "tasks/update";
  params: {
    /** Identifier of the task to update. */
    taskId: string;

    /**
     * Responses to outstanding inputRequests previously surfaced by the
     * server. Shape per MRTR. Each key MUST correspond to a currently-
     * outstanding inputRequest key.
     */
    inputResponses: InputResponses;
  };
}
Response
type UpdateTaskResult = Result; // empty acknowledgement

On success, the server MUST acknowledge the request with an empty result. The acknowledgement is eventually consistent: the server MAY accept the responses and return the ack before the task's observable status (via tasks/get or notifications/tasks) reflects them. Servers SHOULD return a JSON-RPC error if the taskId does not correspond to a known task. Clients SHOULD track inputRequests keys to avoid responding to requests more than once.

A server SHOULD ignore any inputResponses responses mapped to a key that is not currently outstanding for the task — including keys that were never issued, keys that have already been answered, and keys whose corresponding request has been superseded. A server MAY accept a partial set of responses (a strict subset of currently-outstanding keys);

The resultType field MUST be set to "complete" on UpdateTaskResult as it is the standard result shape for the tasks/update request.

Task Cancellation

A client sends a tasks/cancel request to signal its intent to cancel an in-progress task. The notifications/cancelled notification MUST NOT be used for task cancellation.

Request
interface CancelTaskRequest extends JSONRPCRequest {
  method: "tasks/cancel";
  params: {
    taskId: string;
  };
}
Response
type CancelTaskResult = Result; // empty acknowledgement

The server MUST acknowledge the request with an empty result. Servers SHOULD return a JSON-RPC error if the taskId does not correspond to a known task. Cancellation processing is eventually consistent — the task's observable status MAY remain working (or some other non-terminal status) after the ack, and MAY ultimately reach a terminal status other than cancelled if the work finished before cancellation could take effect.

Cancellation is cooperative: The request signals intent, and the server decides whether and when to honor it. A server is not obligated to actually stop the work; it is only obligated to acknowledge the request. Eventual transition to cancelled is not guaranteed.

Clients MAY delete all state associated with the task as soon as they send a cancellation (e.g., it no longer needs to retain the list of inputRequests keys that it has already responded to). The client does not need to poll tasks/get again to wait for the task to reach the cancelled state.

The resultType field MUST be set to "complete" on CancelTaskResult as it is the standard result shape for the tasks/cancel request.

Task Status Notifications

Servers MAY push status updates via notifications/tasks notifications in addition to servicing client polls:

export type TaskStatusNotificationParams = NotificationParams & Task;

export interface TaskStatusNotification extends JSONRPCNotification {
  method: "notifications/tasks";
  params: TaskStatusNotificationParams;
}

To begin listening for task status notifications, clients send a subscriptions/listen request to the server including a list of task IDs the client is interested in (see SEP-2575):

export interface SubscriptionsListenRequest extends Request {
  method: "subscriptions/listen";
  params: {
    // Other existing fields...
    notifications: {
      taskIds?: string[];
      // Other existing fields...
    };
  };
}

In its acknowledgement notification, the server includes the list of task IDs it has agreed to send task status notifications for, if any:

export interface SubscriptionsAcknowledgedNotification extends Notification {
  method: "notifications/subscriptions/acknowledged";
  params: {
    notifications: {
      /**
       * Subscribe to notifications/tasks for specific task IDs.
       */
      taskIds?: string[];
      // Other existing fields...
    };
  };
}

If a client requests task status notifications but does not declare the io.modelcontextprotocol/tasks extension capability, the server MUST return a JSON-RPC error specifying the missing capabilities:

{
  "jsonrpc": "2.0",
  "id": 12,
  "error": {
    // MISSING_REQUIRED_CLIENT_CAPABILITY
    "code": -32021,
    // Message provided for example purposes only. The content of this example message is non-normative.
    "message": "Missing required client capability",
    "data": {
      "requiredCapabilities": {
        "extensions": {
          "io.modelcontextprotocol/tasks": {}
        }
      }
    }
  }
}

Each notification carries a complete DetailedTask for the current status, identical to what tasks/get would have returned at that moment.

Notification:

{
  "jsonrpc": "2.0",
  "method": "notifications/tasks",
  "params": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "status": "completed",
    "createdAt": "2025-11-25T10:30:00Z",
    "lastUpdatedAt": "2025-11-25T10:50:00Z",
    "ttlMs": 60000,
    "pollIntervalMs": 5000,
    "result": {
      "content": [
        {
          "type": "text",
          "text": "Operation completed successfully."
        }
      ],
      "isError": false
    }
  }
}

The notification includes the full task object, allowing clients to access the complete task state and final results without polling the tasks/get method. Clients MAY continue polling tasks/get in addition to subscribing to task status notifications, but need not do so.

notifications/progress and notifications/message notifications MUST NOT be sent on the subscriptions/listen stream for a task, and are not supported on tasks in general in this specification.

Streamable HTTP: Routing Headers

When tasks/get, tasks/update, or tasks/cancel is sent over the Streamable HTTP transport, the client MUST set the Mcp-Name header (defined by SEP-2243) to the value of params.taskId. This allows transport intermediaries and load balancers to route subsequent requests for the same task to the server instance holding its state, which is typically required for correctness. The Mcp-Method header is set to the JSON-RPC method name per SEP-2243.

Example Message Flow

Consider a simple tool call, hello_world, requiring an elicitation for the user to provide their name. The tool itself takes no arguments.

To invoke this tool, the client makes a CallToolRequest as follows:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "hello_world",
    "arguments": {},
    "_meta": {
      // Other metadata...
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": {
          "io.modelcontextprotocol/tasks": {},
        },
      },
    },
  },
}

The server determines (via bespoke logic) that it wants to create a task to represent this work, and it immediately returns a CreateTaskResult:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "resultType": "task",
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "status": "working",
    "createdAt": "2025-11-25T10:30:00Z",
    "lastUpdatedAt": "2025-11-25T10:50:00Z",
    "ttlMs": 3600000,
    "pollIntervalMs": 5000
  }
}

Once the client receives the CreateTaskResult, it begins polling tasks/get:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tasks/get",
  "params": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
  }
}

On each request while the task is in a "working" status, the server returns a regular task response:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "resultType": "complete",
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "status": "working",
    "createdAt": "2025-11-25T10:30:00Z",
    "lastUpdatedAt": "2025-11-25T10:50:00Z",
    "ttlMs": 3600000,
    "pollIntervalMs": 5000
  }
}

Eventually, the server reaches the point at which it needs to send an elicitation to the user. It sets the task status to "input_required" to signal this. On the next tasks/get request from the client, the server sends the elicitation payload via the inputRequests field. Note that while task inputRequests share structural similarities with SEP-2322 multi round-trip requests, they are a distinct mechanism: task inputRequests are surfaced via tasks/get and fulfilled via tasks/update, not via retries of the original method. A server that needs client input before returning a CreateTaskResult (e.g. to decide whether to proceed) uses the multi round-trip request flow on the original request; a server that needs client input during task execution uses the inputRequests/inputResponses mechanism described here.

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tasks/get",
  "params": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
  }
}
{
  "id": 4,
  "jsonrpc": "2.0",
  "result": {
    "resultType": "complete",
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "status": "input_required",
    "createdAt": "2025-11-25T10:30:00Z",
    "lastUpdatedAt": "2025-11-25T10:50:00Z",
    "ttlMs": 3600000,
    "pollIntervalMs": 5000,
    "inputRequests": {
      "name": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "Please enter your name.",
          "requestedSchema": {
            "type": "object",
            "properties": {
              "name": { "type": "string" }
            },
            "required": ["name"]
          }
        }
      }
    }
  }
}

For thoroughness, let's consider a case where the client happens to poll tasks/get again before the user has fulfilled the elicitation request. As inputRequests is effectively a point-in-time snapshot of all outstanding server-to-client requests associated with the task, the server includes the same request again, despite the client having already seen this information (the client is advised to deduplicate inputRequests with the same key for UX purposes):

{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tasks/get",
  "params": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
  }
}
{
  "id": 5,
  "jsonrpc": "2.0",
  "result": {
    "resultType": "complete",
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "status": "input_required",
    "createdAt": "2025-11-25T10:30:00Z",
    "lastUpdatedAt": "2025-11-25T10:50:00Z",
    "ttlMs": 3600000,
    "pollIntervalMs": 5000,
    "inputRequests": {
      "name": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "Please enter your name.",
          "requestedSchema": {
            "type": "object",
            "properties": {
              "name": { "type": "string" }
            },
            "required": ["name"]
          }
        }
      }
    }
  }
}

The user enters their name, and the client makes a tasks/update request with the satisfied information:

{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "tasks/update",
  "params": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "inputResponses": {
      "name": {
        "action": "accept",
        "content": {
          "input": "Luca"
        }
      }
    }
  }
}

The server acknowledges the request:

{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "resultType": "complete"
  }
}

Asynchronously, the server processes it and moves the task back into the working status:

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tasks/get",
  "params": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
  }
}
{
  "id": 7,
  "jsonrpc": "2.0",
  "result": {
    "resultType": "complete",
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "status": "working",
    "createdAt": "2025-11-25T10:30:00Z",
    "lastUpdatedAt": "2025-11-25T10:50:00Z",
    "ttlMs": 3600000,
    "pollIntervalMs": 5000
  }
}

Eventually, the server completes the request, so it stores the final CallToolResult and moves the task into the "completed" status. On the next tasks/get request, the server sends the final tool result inlined into the task object:

{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "tasks/get",
  "params": {
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
  }
}
{
  "jsonrpc": "2.0",
  "id": 8,
  "result": {
    "resultType": "complete",
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "status": "completed",
    "createdAt": "2025-11-25T10:30:00Z",
    "lastUpdatedAt": "2025-11-25T10:50:00Z",
    "ttlMs": 3600000,
    "pollIntervalMs": 5000,
    "result": {
      "content": [
        {
          "type": "text",
          "text": "Hello, Luca!"
        }
      ],
      "isError": false
    }
  }
}

Error Handling

Tasks use two error reporting mechanisms:

  1. Protocol Errors: Standard JSON-RPC errors for protocol-level issues
  2. Task Execution Errors: Errors in the underlying request execution, reported through task status
Protocol Errors

Servers MUST return standard JSON-RPC errors for the following protocol error cases:

  • Invalid or nonexistent taskId: -32602 (Invalid params)
    • Servers MUST return this error for tasks/get.
    • Servers SHOULD return this error for tasks/update and tasks/cancel.
  • Internal errors: -32603 (Internal error)
  • Missing required client capabilities: -32021 (Missing Required Client Capability)
    • Servers MUST return this error for non-declaring clients requesting task notifications on subscriptions/listen.
    • Servers MUST return this error for non-declaring clients issuing tasks/get, tasks/update, and tasks/cancel requests.

Servers SHOULD provide informative error messages to describe the cause of errors.

Example: Task not found

{
  "jsonrpc": "2.0",
  "id": 70,
  "error": {
    "code": -32602,
    "message": "Failed to retrieve task: Task not found"
  }
}

Example: Task expired

{
  "jsonrpc": "2.0",
  "id": 71,
  "error": {
    "code": -32602,
    "message": "Failed to retrieve task: Task has expired"
  }
}

Servers are not required to retain tasks indefinitely. It is compliant behavior for a server to return an error stating the task cannot be found if it has purged an expired task.

Task Execution Errors

When the underlying request encounters a JSON-RPC protocol error during execution, the task moves to the failed status. The tasks/get response SHOULD include a statusMessage field with diagnostic information about the failure, and MUST include the error field with the JSON-RPC error.

The failed status MUST NOT be used to represent non-JSON-RPC errors, such as a tool result that completed with isError: true. Errors within the context of a protocol method result MUST use the completed status with the error details in the result field. This maintains a strong separation between protocol-level faults (which use the failed status) and other faults.

Example: Task with JSON-RPC execution error

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "resultType": "task",
    "taskId": "786512e2-9e0d-44bd-8f29-789f820fe840",
    "status": "failed",
    "createdAt": "2025-11-25T10:30:00Z",
    "lastUpdatedAt": "2025-11-25T10:40:00Z",
    "ttlMs": 3600000,
    "statusMessage": "Tool execution failed: API rate limit exceeded",
    "error": {
      "code": -32603,
      "message": "API rate limit exceeded"
    }
  }
}

Example: Tool call completed with tool error (isError: true)

For tool calls that complete successfully at the protocol level but return an tool-level error (indicated by isError: true in the tool result), the task reaches completed status with the tool result in the result field:

{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "resultType": "task",
    "taskId": "786512e2-9e0d-44bd-8f29-789f820fe840",
    "status": "completed",
    "createdAt": "2025-11-25T10:30:00Z",
    "lastUpdatedAt": "2025-11-25T10:40:00Z",
    "ttlMs": 3600000,
    "result": {
      "content": [
        {
          "type": "text",
          "text": "Failed to process request: invalid input"
        }
      ],
      "isError": true
    }
  }
}

The tasks/get endpoint returns exactly what the underlying request would have returned:

  • If the underlying request resulted in a JSON-RPC error, the task uses failed status and the error field MUST contain that JSON-RPC error.
  • If the request completed with a result (even if isError: true for tool results), the task uses completed status and the result field MUST contain that result.

Reservations

  • The tasks/ method prefix and notifications/tasks/ notification prefix are reserved for this extension.
  • The result-discriminator value "task" for resultType is reserved for this extension.
  • The label io.modelcontextprotocol/tasks is reserved for this extension.

Rationale

Unsolicited Tasks vs. Immediate Results

An alternative proposal would have handled the immediate result case individually, and with slightly different preconditions: If tasks are supported, and the client supports immediate task results, then servers may return a regular result in response to a task-augmented request. That version of immediate results looked like a better option at the time, as it implied no breaking changes on top of the initial tasks specification.

However, as we look to move away from stateful protocol interactions and given the current experimental state of tasks in general, it seems worth proposing a somewhat more radical change that reduces the complexity of the overall specification and makes tasks more "native" to MCP at this time. In particular, the choice to allow unsolicited tasks (in addition to immediate results) means promoting tasks to a first-class concept intended for all persistent operations, as opposed to being a parallel and somewhat specialized concept.

This happens to align with the proposed SEP-2322, but the two are not coupled with one another.

Splitting Reads (tasks/get) and Writes (tasks/update)

Earlier drafts of this redesign let tasks/get carry inputResponses so a single round trip would both submit responses and observe the resulting state. That conflation has costs: it makes the read path non-idempotent (a retried tasks/get could re-submit responses), it forces the read path to share the eventual-consistency model of the write, and it complicates intermediaries that want to cache or deduplicate reads. Splitting the methods leaves tasks/get as a pure, idempotent read that any layer can cache or replay safely, and confines write semantics — including their eventual-consistency window — to tasks/update.

tasks/update's ack-only response shape follows from the same separation: there is no read data the server needs to return that the client cannot get from a follow-up tasks/get, and forcing an embedded Task into the response would re-introduce the non-idempotency we are trying to avoid. The cost is one extra round-trip per round of input — paid only when the task actually requires a client request.

Task Creation Consistency

The following new requirement is introduced:

A server MUST NOT return CreateTaskResult until the task is durably created — that is, until a tasks/get for the returned taskId would resolve. In eventually-consistent environments, the server MUST wait for consistency before responding. This requirement eliminates the need for clients to speculatively poll for task creation.

Unlike tasks/update and tasks/cancel, task creation is strongly-consistent. This has to be the case to avoid speculative tasks/get requests from requestors that would otherwise not know if a task has silently been dropped or if it simply has not been created yet. Conversely, eventual consistency in tasks/update and tasks/cancel works because the client behavior is not contingent on the results of those operations (the client can continue to poll either way). While consistent task creation does increase latency costs in distributed systems that did not already behave this way, explicitly introducing this requirement simplifies client implementations and eliminates a source of undefined behavior.

This also aligns with long-running operation APIs in general, which typically require that once an operation is acknowledged, it must be findable via the polling endpoint.

Ack-only Cancellation

In the 2025-11-25 design of tasks, tasks/cancel returned a task describing the task's state immediately after the cancellation attempt. That return shape implies a synchronous read — the server must consult task state to populate it — but cancellation is inherently asynchronous in many applications (a separate worker decides whether and when to honor it), so the returned task object would in many cases simply repeat what the next tasks/get would show. Reducing tasks/cancel to an ack matches the operation's actual semantics: The request is a signal, not a state query. Clients that want to know the post-cancel status do so via tasks/get on the same code path they use for all other state observation.

The eventual-consistency on the ack is the same separation as for tasks/update: The server may record the cancellation request and respond before the worker has actually transitioned the task, without allowing the client to interpret the ack as strongly-consistent.

While tasks/update and tasks/cancel use ack-only response shapes for the reasons above, servers SHOULD still return errors for clearly invalid requests — such as an unknown taskId. The ack-only design is about avoiding synchronous reads of task state in the success path, not about suppressing errors that the server can detect at request time. Returning errors for invalid inputs gives clients a faster signal that something is wrong, rather than forcing them to discover the problem indirectly through subsequent tasks/get polls.

Composition with Multi Round-Trip Requests

The following new requirement is introduced:

Server implementations that use multi round-trip requests in conjunction with task creation (for example, a tool that requires elicitation over InputRequiredResult before creating a task) SHOULD resolve all MRTR exchanges synchronously before responding with a CreateTaskResult.

A tools/call that supports both MRTR (SEP-2322) and this extension may use them sequentially by sending one or more InputRequiredResult exchanges to gather input synchronously, followed by a CreateTaskResult to hand off to asynchronous execution. This composition is a consequence of the resultType discriminator — each response is independently typed and the client switches behavior based on the value it receives, without maintaining any state between the two modes. Prohibiting this would require imposing an artificial constraint with no protocol-level mechanism to enforce it, since the client is unaware that the server will create a task ahead of time.

The two flows maintain separate state despite sharing field names. The MRTR phase ends when the server returns any non-"input_required" resultType, at which point its inputRequests keys are consumed. The task phase begins with CreateTaskResult and maintains its own inputRequests keys independently. Key uniqueness for task inputRequests is scoped to the lifetime of the task and does not extend to keys from the preceding MRTR phase. Clients do not need to deduplicate across the two flows.

Backward Compatibility

The experimental tasks feature in the 2025-11-25 release is not wire-compatible with this extension. Implementations that need to interoperate with both surfaces can shim at the SDK level by implementing the experimental and extension flows in parallel and dispatching on the negotiated protocol version and the client capability the peer declared. The following table summarizes the expected behavior for each permutation:

Protocol Version tasks.* (legacy) io.modelcontextprotocol/tasks
2025-11-25 Legacy experimental tasks per the 2025-11-25 specification. The client opts into task augmentation per request via the task parameter on CallToolRequest; the server uses tasks/result, tasks/get, tasks/cancel, and (where supported) tasks/list per that specification. This extension does not apply. This extension is not defined under the 2025-11-25 protocol version. Servers MUST NOT treat this capability as enabling tasks under that protocol version; requests proceed as if the client had declared no task capability at all.
2026-06-30 The legacy capability is not part of this extension. Servers MUST treat clients declaring only the legacy capability as non-declaring with respect to this extension. Servers that simultaneously support the 2025-11-25 Tasks specification alongside this extension SHOULD continue to permit tasks/get and tasks/cancel requests from such clients to operate on tasks created under that flow. The canonical case. Full task lifecycle as specified in this document, with the following wire-level differences from the 2025-11-25 experimental feature:
  • tasks/result is removed; clients calling it MUST receive -32601 (Method Not Found).
  • The task parameter on CallToolRequest is removed; servers MUST ignore it (treat the field as unknown) rather than using it as an opt-in.
  • The tasks.requests.*, tasks.cancel, and tasks.list capability declarations are not part of this extension. Servers that previously advertised these MUST migrate to declaring io.modelcontextprotocol/tasks, and MUST NOT continue to advertise the legacy capabilities under any protocol version that includes this extension.

A server that returns the standard CallToolResult shape — i.e., never elects to create a task — remains fully spec-compliant under this extension. Clients that have negotiated the extension MUST handle both result shapes for any augmented request.

Security Implications

  • Task ID unguessability. A server MAY use task IDs as bearer tokens for a server's stored state. Servers MUST generate them with sufficient entropy that a third party cannot enumerate or guess them.
  • Auth binding. Servers MUST perform authentication and authorization checks on each task-related request to ensure that the client has permission to access a task.
  • Cross-caller correlation. Because there is no tasks/list, a server cannot inadvertently leak the existence of one caller's tasks to another. This is an improvement over the 2025-11-25 tasks specification, in which a poorly-scoped list could expose unrelated task IDs.
  • Input-request trust model. inputRequests carry elicitation and sampling payloads from the server through the client to the user or model. Hosts MUST apply the same trust model to these payloads as they would to standard elicitation/sampling requests. A task is not a higher-trust channel.

Reference Implementation

Implemented in mcpkit (see usage example).

SEP-2692 stdio Server Process Lifetime

Draft · Informational · Created 2026-05-06

Imported from upstream. Proposed as modelcontextprotocol#2692 by @nickcoai on 2026-05-06, 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, and there is no Security Implications section. 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 documents the intended process lifetime for MCP servers using the stdio transport. For stdio, the server process is expected to live approximately as long as the host application instance that launched it, rather than being scoped to a single task, thread, or conversation. A single stdio server process should therefore be able to serve multiple conversations over its lifetime.

This is a documentation-only clarification. It does not change the wire protocol, schema, or lifecycle messages.

Motivation

The stdio transport documentation states that the client launches the MCP server as a subprocess, but it does not explicitly define the intended lifetime boundary for that subprocess. In practice, this leaves room for implementations to treat a task, thread, or conversation as the natural process boundary.

That interpretation is undesirable:

  • It conflates application-level concepts such as tasks and conversations with the transport-level lifetime of the server process.
  • It can cause unnecessary process churn and repeated initialization work.
  • It can lead server authors to assume that process-local state is discarded between conversations, even though a stdio server may continue serving the host application across many of them.

Clients and server authors need an explicit statement of the intended model so that stdio implementations converge on the same operational assumptions.

Specification

The MCP documentation should clarify the following expectations for the stdio transport:

  1. A stdio MCP server process is expected to have approximately the same lifetime as the host application instance that launched it.
  2. Stdio clients should not use an individual task, thread, or conversation as the default lifetime boundary for the server process.
  3. While a stdio server process remains connected to a host application, it should be expected to handle requests associated with multiple tasks, threads, or conversations.
  4. Stdio server authors should design their servers with the expectation that a single process can serve multiple conversations over its lifetime.
  5. Host applications may still intentionally start, stop, or restart stdio servers for application-level reasons such as configuration changes, explicit server disablement, crash recovery, or application shutdown. This clarification does not require every configured server to remain running forever.

These statements should be added to the stdio transport documentation and reflected in client- and server-facing documentation where stdio lifecycle expectations are described.

Rationale

The host application is the most natural owner of a stdio subprocess: it launches the server, owns the pipes, and decides when the subprocess is no longer needed. Tasks, threads, and conversations are application-level concepts that may come and go while the same host application instance remains active, so they are poor default process lifetime boundaries.

A task-scoped or conversation-scoped process model can be useful as an intentional host implementation choice, but documenting it as the default would make stdio behavior less predictable and would encourage servers to assume a narrower lifetime than the transport actually requires.

This clarification also keeps stdio aligned with the transport documentation's existing subprocess model while leaving host applications free to apply explicit resource management policies when they choose to do so.

Backward Compatibility

This SEP introduces no protocol or wire-format changes. It only clarifies the intended documentation of existing stdio behavior.

Security Implications

This SEP introduces no new security implications.

Reference Implementation

Not applicable. This SEP proposes documentation-only clarification.

SEP-2694 Resumable Task Event Streams

Draft · Extensions Track · Created 2026-05-06

Imported from upstream. Proposed as modelcontextprotocol#2694 by @rynowak on 2026-05-06, 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 extends MCP Tasks with a minimal, resumable event stream for long-running task execution. It defines one request, tasks/stream, that starts or resumes streaming for a task. The server delivers events as notifications/tasks/event notifications on the same connection used by that tasks/stream request. Each task event has a task-scoped sequence number, task ID, event type, and JSON payload.

This proposal builds on existing task work. SEP-1686 defines durable task status and result retrieval, but not an interoperable event timeline for intermediate execution. SEP-2679 proposes a focused and useful push-based partial result notification for incremental content delivery. This SEP addresses a broader task-event use case where clients need to resume from a specific stream position and receive implementation-defined event types beyond partial content. SEP-2669 proposes task interaction methods (tasks/steer, tasks/pause, and tasks/resume), which let clients influence running tasks but do not define a replayable task-observability channel.

Resumable task event streams make task execution observable across reconnects, late joins, agent-to-agent delegation, subagent orchestration, tool traces, artifacts, and rich execution timelines without requiring every server to invent a custom polling or notification protocol.

Two requirements distinguish this proposal from prior partial-result and streaming work:

  1. Resumability: clients can resume after a specific task event sequence number.
  2. Extensibility: servers can emit vendor-prefixed task event types with arbitrary JSON object payloads, including and beyond partial result content.

Motivation

Tasks are intended to support long-running work that may outlive a single request/response cycle. For agentic workloads, the final task result is often not enough. Clients need to observe what happened while the task was running: assistant deltas, intermediate messages, tool calls, tool results, spawned subtasks, artifacts, status transitions, checkpoints, and domain-specific milestones.

Without a standard event stream, implementations choose between incomplete alternatives:

  1. Send only the final tasks/result, losing all intermediate context and real-time UX.
  2. Use tasks/get polling, which exposes coarse status but not a detailed execution timeline.
  3. Emit ad hoc notifications that are fire-and-forget and cannot be replayed after reconnect.
  4. Split observability into custom tools or resources, forcing models and host applications to learn server-specific conventions.

This is particularly limiting for agent-to-agent systems. A parent agent may delegate a task to a specialist agent, disconnect, reconnect later, and need to recover exactly what it missed. A human user may join a running task late and need the retained execution history. A client may pause or steer a task using SEP-2669's interaction methods and still need to observe the resulting status transitions, accepted feedback, and resumed work. A hosted MCP deployment may need to stream execution traces to multiple clients while enforcing explicit retention and replay limits.

Relationship to SEP-2679

SEP-2679 proposes notifications/tasks/partial, with taskId, content, and an incrementing seq. That is a useful additive primitive for incremental output and improves real-time UX for tasks that produce partial content. This SEP is complementary: it uses the same intuition that task execution should be observable while it is running, but applies it to a replayable task event stream.

The main difference is scope. SEP-2679's seq supports ordering, duplicate detection, and gap detection for partial content notifications. This SEP uses the same simple sequence-number model as the resume cursor by letting the client call tasks/stream with after, which gives the server a place to replay retained events or return events_gone.

SEP-2679 also models each partial as ContentBlock[], which is a good fit for incremental content. This SEP uses an event envelope so the same stream can also represent first-class execution events such as tool calls, tool results, subtask lifecycle changes, model usage, artifacts, checkpoints, pause/resume transitions, or vendor-specific timeline entries.

The two proposals could be aligned if SEP-2679's partial-content stream grows to address the requirements this SEP treats as necessary for many task scenarios: extensibility beyond partial content and best-effort reconnection through an explicit resume request. In that case, SEP-2679's focused partial-content payload could become one well-defined event shape carried by a more general task event stream.

This SEP can carry SEP-2679-style partial content as a prefixed, implementation-defined event in a broader task event stream:

{
  "seq": 42,
  "taskId": "task-123",
  "type": "com.example.content.partial",
  "payload": {
    "content": [
      {
        "type": "text",
        "text": "partial text"
      }
    ]
  }
}

Why SEP-2669 is complementary

SEP-2669 adds tasks/steer, tasks/pause, and tasks/resume. Those methods let clients affect task execution, but they do not define how clients observe the detailed consequences of those interactions. A task that is paused, steered, and resumed should be able to expose a replayable event timeline showing the relevant status changes and subsequent work.

This SEP does not change the semantics of SEP-2669. Instead, it provides the observability channel that lets clients reconnect, catch up, and correlate interaction methods with task execution history.

Relationship to prior proposals

Several prior proposals explored adjacent streaming, partial-result, and resumability problems. This SEP keeps the parts that are useful for task event streams while deliberately avoiding broader changes that are not required for this proposal.

Proposal What it proposed Relationship to this SEP
SEP-2679: Task streaming partial results Push notifications/tasks/partial with taskId, ContentBlock[], and seq. This SEP builds on the value of partial content streaming by adding a client-controlled resume cursor and allowing implementations to carry equivalent payloads as prefixed task events.
SEP-1905: Task Result Streaming and Immediate Result Acceptance Adds response modes (immediate, task, streaming), partial-content, seqNr, and resume through tasks/result. This SEP keeps the need for ordered replay, but avoids response-mode negotiation, multiple delivery mechanisms, and changes to tasks/result.
SEP-2509: task output snapshots and issue #2452 Pull-based tasks/read snapshots of in-progress output, especially logs and build/test output. Snapshot reads are useful but solve a different pull use case. This SEP defines push delivery plus replay for event history.
SEP-383 and SEP-776 Partial results via progress notifications or multiple JSON-RPC responses. This SEP avoids overloading progress notifications or JSON-RPC response cardinality. Task events are explicit notifications tied to tasks/stream.
SEP-899 and SEP-925 Generic transport-agnostic resumable streams or requests. This SEP keeps cursor-based resume as a data-layer concern, but scopes it narrowly to task event streams.
SEP-1699, SEP-1858, and SEP-2322 Transport and multi-round-trip request flow changes for Streamable HTTP and server-to-client interactions. This SEP relies on same-connection delivery from a client request, aligning with request-associated message constraints rather than adding unsolicited push.
SEP-2532: Resource Streaming resources/stream for efficient binary resource delivery. Binary resource transfer should remain separate. Task events may reference artifacts or resources instead of embedding large binary payloads.
SEP-2268: Subtasks Nested tasks with parent/child relationships and independent subtask results. Subtasks complement task event streams; subtask lifecycle can be represented as task events without requiring nested tasks for every event.

Specification

This SEP defines two protocol messages:

Message Purpose
tasks/stream Client request that starts or resumes streaming events for a task
notifications/tasks/event Server notification that delivers each task event on that same stream

Clients that call tasks/stream against a receiver that does not support task event streams receive -32601 (Method not found). If Tasks are negotiated through capabilities, implementations MAY advertise this support as an eventStreams sub-capability of Tasks, but the interoperable protocol surface is the tasks/stream method itself.

Task Event

A task event is an immutable entry in a task's event stream.

interface TaskEvent {
  /**
   * Monotonically increasing sequence number for this task's event stream.
   */
  seq: number;

  /**
   * The task this event belongs to.
   */
  taskId: string;

  /**
   * Event type discriminator.
   */
  type: string;

  /**
   * Event payload. The schema is defined by the event type.
   */
  payload: { [key: string]: unknown };

  /**
   * Standard MCP metadata. Task events related to a task MUST include
   * io.modelcontextprotocol/related-task with the same taskId.
   */
  _meta?: { [key: string]: unknown };
}

Sequence numbers are scoped to a single task. The first event in a task stream MUST use seq: 0, and each subsequent event for that task MUST increment by exactly 1. Servers MUST deliver events in sequence order.

Event payloads MUST be JSON objects. This SEP does not standardize event type names or payload schemas. Event type strings that are not prefixed by a vendor- or implementation-controlled namespace are reserved for future MCP standardization and MUST NOT be emitted by implementations unless defined by a future MCP standard. Implementation-defined event types MUST use a vendor-specific prefix, preferably reverse-DNS, for example com.example.review.comment.

tasks/stream

Clients start or resume a task event stream by sending tasks/stream.

Request
interface TaskStreamRequest {
  method: "tasks/stream";
  params: {
    /**
     * The task whose events should be streamed.
     */
    taskId: string;

    /**
     * Exclusive resume cursor. When provided, the server sends retained events
     * with seq greater than this value, then continues with live events.
     *
     * When omitted, the server starts with the earliest retained event for the
     * task, then continues with live events.
     *
     * Must be a non-negative integer.
     */
    after?: integer;
  };
}
Response

The tasks/stream request remains active while the stream is open. The receiver sends task events as notifications/tasks/event notifications on the same connection. When the receiver closes the stream normally, it returns an empty result:

type TaskStreamResult = EmptyResult;

Receivers MAY close a stream before a task reaches a terminal state. Clients MAY call tasks/stream again using the latest fully processed sequence number as after.

A receiver MUST allow at most one active tasks/stream request per task per connection. If a client sends a second tasks/stream for a task that already has an active stream on the same connection, the receiver MUST return -32602 (Invalid params). Clients that want to change the cursor SHOULD cancel the active stream first, then open a new one.

Clients cancel an active stream with the normal MCP cancellation mechanism for the tasks/stream request. Cancellation stops event delivery but does not cancel the underlying task. Clients that want to cancel the task itself use the task cancellation mechanism.

notifications/tasks/event

Receivers deliver task events with notifications/tasks/event. These notifications are the delivery channel for an active tasks/stream request and are sent on the same connection.

{
  "jsonrpc": "2.0",
  "method": "notifications/tasks/event",
  "params": {
    "seq": 42,
    "taskId": "task-123",
    "type": "com.example.content.partial",
    "payload": {
      "content": [
        {
          "type": "text",
          "text": "partial text"
        }
      ]
    },
    "_meta": {
      "io.modelcontextprotocol/related-task": {
        "taskId": "task-123"
      }
    }
  }
}

Receivers MUST include io.modelcontextprotocol/related-task metadata on task event notifications. The metadata task ID MUST match params.taskId.

Task event notifications that are intended to be resumable MUST be sent in response to an active tasks/stream request. Receivers MAY define additional best-effort notifications, but clients MUST NOT rely on unsolicited notifications for replay or resume semantics.

Resume and Replay Semantics

The after parameter MUST be a non-negative integer. Receivers MUST return -32602 (Invalid params) if after is negative, not an integer, or otherwise not a valid sequence number value.

When a client supplies a valid after, the receiver MUST do one of the following:

  1. Deliver retained events with seq greater than the supplied value, then continue with live events. If the receiver has no retained events with seq greater than the supplied value and the task has reached a terminal state, the receiver MUST return TaskStreamResult immediately with no event notifications.
  2. Return an events_gone error if events after the supplied sequence number are no longer retained.

If after is omitted, the receiver MUST start at the earliest event it still retains for the task, then continue with live events. If no events are retained and the task still exists, the receiver starts with future live events. If no events are retained and the task has reached a terminal state, the receiver MUST return TaskStreamResult immediately.

Receivers MUST NOT silently skip retained events after the requested cursor. If the receiver cannot provide a gap-free stream after the cursor, it MUST return events_gone.

Clients SHOULD persist the latest sequence number only after fully processing that event. On reconnect, clients SHOULD pass that sequence number as after. Clients MUST tolerate duplicate delivery after reconnect and SHOULD de-duplicate events by (taskId, seq).

events_gone error

When a cursor is too old to resume, receivers return a JSON-RPC error using the implementation-defined server error range:

{
  "jsonrpc": "2.0",
  "id": 10,
  "error": {
    "code": -32010,
    "message": "Task events are no longer available",
    "data": {
      "reason": "events_gone",
      "taskId": "task-123",
      "earliestSeq": 100
    }
  }
}

The earliestSeq field tells the client the oldest event the receiver still retains. Clients can use this to decide how to recover: call tasks/stream with after set to earliestSeq - 1 to resume from the earliest retained event, or call tasks/stream without after to receive all retained events from the beginning. Clients that require a gap-free history MAY treat the gap as a fatal error and report it to the user instead of resuming.

For Streamable HTTP transports that expose stream-resume semantics through HTTP status codes, an implementation MAY map this condition to HTTP 410 at the transport layer, but the data-layer error reason remains events_gone.

Event Retention

Receivers MAY impose retention limits on task events. Receivers SHOULD retain task events for at least as long as the task remains retrievable, but this SEP does not require indefinite event retention.

Receivers SHOULD document:

Retention property Description
Maximum event age How long events are retained after creation
Maximum events per task How many events may be retained for a task
Terminal retention How long events remain after terminal task state
Payload size limits Maximum payload size per event

Receivers MAY drop old events according to their retention policy. Once a receiver drops an event needed to resume a stream, it MUST return events_gone for clients that resume from before the retained range.

Event Types

This SEP standardizes the event envelope and replay contract. Standard event type names and payload schemas are out of scope for this proposal.

Event type strings that are not prefixed by a vendor- or implementation-controlled namespace are reserved for future MCP standardization. Implementations MUST NOT emit unprefixed event types such as content.partial, task.status, or tool.started unless those names are defined by a future MCP standard.

Implementation-defined event types MUST use a vendor-specific prefix, preferably reverse-DNS, for example com.example.content.partial, com.example.task.status, or com.example.review.comment. The owner of that prefix defines the event payload schema and compatibility rules.

This keeps the base stream interoperable without prematurely standardizing an event vocabulary. The stream envelope, sequence numbers, resume cursor, and retention contract are interoperable across implementations. Event payload semantics are not — two implementations that both support tasks/stream can deliver and replay each other's events, but cannot interpret event payloads without shared knowledge of the vendor-prefixed event types. Future SEPs can define standard unprefixed event types or shared payload schemas once there is enough implementation experience.

Relationship to subscriptions/listen (SEP-2575)

SEP-2575 introduces subscriptions/listen as the general-purpose mechanism for receiving notifications. Clients POST a subscriptions/listen request declaring which notification types they want, and the server delivers matching notifications on that stream. subscriptions/listen is explicitly non-resumable: if the connection drops, missed notifications are lost and the client must re-subscribe from scratch. SEP-2575 states that workloads needing durability or resumability should use the tasks primitive instead.

This SEP positions tasks/stream as the resumable delivery channel that subscriptions/listen deliberately defers to. The two serve different reliability tiers:

  • subscriptions/listen delivers best-effort, fire-and-forget notifications including notifications/tasks/status for lightweight task status changes. No replay, no sequence numbers, no retention.
  • tasks/stream delivers resumable, sequenced task event history via notifications/tasks/event. Clients can reconnect with after and catch up on missed events within the receiver's retention window.

A client that only needs to know whether a task is still running can subscribe to notifications/tasks/status through subscriptions/listen. A client that needs the full execution timeline — tool calls, intermediate results, subagent lifecycle, artifacts — opens a tasks/stream. Both can be active simultaneously for the same task.

This SEP does not modify subscriptions/listen or change how notifications/tasks/status is delivered. The two mechanisms are complementary: subscriptions/listen handles general notification fan-out, while tasks/stream handles task-scoped durable delivery with replay.

Relationship to Task Status Notifications

Existing or proposed task status notifications remain useful for lightweight status updates. Task event streams provide a stronger replay contract. A receiver that emits both notifications/tasks/status and notifications/tasks/event SHOULD ensure status event entries are consistent with the task status visible through tasks/get.

If SEP-2669 is accepted, receivers SHOULD emit task events for externally visible pause and resume transitions when task event streams are enabled. This SEP does not require a specific payload shape for those events.

Relationship to Progress Notifications

Existing notifications/progress remains the right mechanism for best-effort scalar progress on an in-progress MCP request. It is intentionally lightweight: the client supplies a progressToken, the receiver may send progress updates, and each update contains a numeric progress, optional total, and optional message.

Task event streams serve a different purpose. They provide a replayable task history with sequence numbers, retention semantics, and implementation-defined JSON payloads. Progress notifications do not provide a resume cursor, replay contract, retained history, arbitrary payload schemas, or late-join behavior.

This SEP does not change the semantics of notifications/progress. Implementations MAY continue to emit progress notifications for request-local UX, including for a tasks/stream request if a client supplies a progressToken. Such progress notifications MUST NOT be treated as task event records and do not affect task event seq values.

A receiver that emits both progress notifications and task event streams SHOULD keep them consistent, but they need not be one-to-one. For example, progress can report coarse completion percentage while prefixed task events describe the detailed task timeline.

Message Flow

ServerClientServerClientConnection drops after client processed seq 1tasks/stream(taskId)notifications/tasks/event(seq: 0)notifications/tasks/event(seq: 1)tasks/stream(taskId, after: 1)notifications/tasks/event(seq: 2)notifications/tasks/event(seq: 3)TaskStreamResult({})

Rationale

Why client-initiated stream?

Pure unsolicited notifications cannot be resumable because the client has nowhere to provide its desired stream position. A client-initiated request gives the client an explicit cursor field and gives the server a clear point to validate retention, replay retained events, or return events_gone.

Why sequence numbers?

Task event streams use sequence numbers because they are simple, deterministic, and easy for clients to validate. They let clients detect duplicate events, missing events, and out-of-order delivery without understanding any server-specific cursor format.

Clients only need two operations: persist the last processed seq and provide it back as after. Servers can still map sequence numbers to internal log offsets, database IDs, or message broker cursors privately.

Why a generic event envelope?

ContentBlock[] is output-oriented and works well for partial text or content chunks. Task execution timelines also need to represent non-output events. A generic event envelope supports partial content, tool traces, artifacts, status transitions, subagent lifecycle, and implementation-specific events without requiring a protocol schema change for every new event kind.

Task event payloads are intentionally arbitrary JSON objects. Common event types can be standardized later in separate SEPs, but the core stream must allow servers to carry domain-specific data such as build phases, test results, agent planning steps, UI cards, artifact descriptors, model usage, or vendor-specific execution traces without waiting for the base protocol schema to change.

Why not use resources?

Servers can expose task logs or traces as resources, but that is a convention rather than a task protocol. Clients would need to discover which resources are task event streams, how to correlate them with tasks, how to page through them, and how to resume from a position. A task-specific event stream standardizes those semantics directly.

Why not rely on transport-layer resume?

Transport-layer resume can recover bytes or SSE events for a connection, but task event replay is a data-layer concern. The same task event cursor must work across transports and across reconnects where the transport session may be gone. The client wants to resume a task stream after a task event sequence number, not merely after a transport frame.

Lessons incorporated from prior proposals

This SEP intentionally preserves several lessons from earlier work:

  1. Keep final results canonical: as in SEP-2679, streaming events do not replace the final task result.
  2. Make resume explicit: as in the resumable stream/request proposals, the client must have a concrete cursor it can present after reconnect.
  3. Avoid multiple JSON-RPC responses for one request: prior partial-result proposals showed this is tempting but hard to reconcile with standard JSON-RPC clients.
  4. Do not overload progress: progress remains useful for lightweight status, while task events carry replayable task history.
  5. Bound retention: servers need explicit retention limits and a clear events_gone failure mode.
  6. Keep large binary data out of events: resource streaming proposals show that large binary transfer needs its own mechanism; task events should reference artifacts/resources when payloads would be large.
  7. Preserve extensibility: content-specific schemas are valuable for content events, while agent execution timelines also need an extensible envelope for non-content events. This SEP standardizes that envelope while leaving individual event type schemas to implementation-defined prefixes or future SEPs.

Backward Compatibility

This SEP is additive.

  • Existing task clients and servers continue to work unchanged.
  • Servers that do not support tasks/stream return -32601.
  • Clients that do not open task event streams receive no new required messages.
  • Existing tasks/get and task lifecycle semantics are unchanged.
  • SEP-2679-style partial result notifications can coexist with this SEP. Implementations may support both during migration.
  • SEP-2669 task interaction methods are unchanged. This SEP only provides an optional event timeline that can describe their visible effects.

Security Implications

Task event streams can expose detailed execution history, intermediate data, tool arguments, tool results, artifacts, and user feedback. Implementations MUST apply the same authorization checks to tasks/stream that they apply to tasks/get.

Receivers SHOULD bind task event streams to the same session, tenant, and authentication context as the task. A client that cannot access a task MUST NOT be allowed to stream its events.

Receivers SHOULD treat event payloads as untrusted data. Clients MUST NOT execute event payloads as code. Clients that render event payloads SHOULD apply the same sanitization and content-security policy they use for tool results and resources.

Receivers SHOULD enforce limits on:

Limit Reason
Concurrent streams per client Prevent denial of service
Event payload size Prevent memory and bandwidth exhaustion
Event retention duration Limit sensitive data persistence
Events retained per task Bound storage growth
Resume attempts Detect brute-force task or cursor guessing

Receivers SHOULD avoid retaining sensitive event payloads longer than necessary. Clients SHOULD request deletion of tasks and associated event history when retained data is no longer needed.

Open Questions

  1. Should the events_gone error code be standardized across MCP, or is the data.reason field sufficient?
  2. How should multiple clients streaming the same task interact with server retention policies and backpressure?
  3. If SEP-2679 is accepted first, should notifications/tasks/partial remain a separate convenience notification alongside prefixed task events?
  4. This SEP positions tasks/stream as complementary to subscriptions/listen (SEP-2575), serving a different reliability tier. An alternative design would integrate task event streaming into subscriptions/listen itself, for example by adding resume cursor and retention semantics to subscription streams when scoped to a specific task. Should task event streaming remain a separate tasks/stream request, or should it be folded into subscriptions/listen with task-scoped resume extensions?

SEP-2752 HTTP Message Signing for MCP Client Authentication

Draft · Extensions Track · Created 2026-05-19

Imported from upstream. Proposed as modelcontextprotocol#2752 by @njdawn on 2026-05-19, 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 defines an optional, additive client-authentication mechanism for MCP based on RFC 9421 HTTP Message Signatures. It enables MCP servers to authenticate clients with cryptographic proof-of-possession of a private key — without requiring the client to ever transmit a long-lived secret over the wire and without modifying any existing OAuth or MCP-Session-Id flow.

This SEP is filed as an Extensions Track proposal per SEP-2133: it is not a core-protocol change. It is the optional, opt-in extension io.modelcontextprotocol/http-message-signatures (home: the ext-auth repository), negotiated through capabilities.extensions, disabled by default, with no change to the core schema. It is complementary to — not a replacement for — OAuth and DPoP (SEP-1932): DPoP for the OAuth-bound path, this extension for the non-OAuth / API-key / wallet-native case.

The proposal is a refinement of the now-dormant SEP-1415, incorporating the technical feedback from that thread:

  • Signed-component list and signature parameters corrected to comply with RFC 9421 §2 and §3.
  • The alg signature parameter is prohibited; algorithm is derived from key material (per RFC 9421 §3.2 and the security considerations of §7).
  • Algorithm negotiation uses the standard Accept-Signature response field, not a custom JSON-RPC error envelope.
  • Replay protection uses RFC 9421's nonce + created mechanism rather than mandatory server-side signature caching.
  • Key distribution supports both an initialize-bound flow and an init-less flow compatible with SEP-1372 (MCP-Protocol-Version-header bootstrap), via the Signature-Agent header from draft-meunier-http-message-signatures-directory.
  • A signature tag is defined as the stable client identifier so that keys can be rotated without losing identity continuity.

Motivation

Why MCP needs cryptographic client authentication

Today MCP authenticates clients with bearer tokens (OAuth access tokens, API keys, MCP-Session-Id). A bearer is, by definition, whoever holds the string is the client. That model has well-understood structural weaknesses that the rest of the web stack has been migrating away from:

  1. Secrets are continuously re-exposed to the server. Every request hands the full credential back. A compromised, malicious, or merely curious server endpoint sees the material the client uses to authenticate everywhere it presents that token. Cryptographic authentication inverts this: the private key never leaves the client. The server only ever sees signatures over specific requests — values that cannot be replayed or repurposed.

  2. No protection against man-in-the-middle / intermediary trust. Bearer tokens rely entirely on TLS for confidentiality. Anything that legitimately terminates TLS along the path — CDNs, reverse proxies, corporate egress inspection, logging middleware — sees the bearer in plaintext and can act as the client until rotation. A signed request is verifiable end-to-end: an intermediary can read the request but cannot mint a new signed request without the private key. This is the property mTLS provides at the transport layer, achievable above TLS termination, which is where most production deployments actually need it.

  3. No request integrity. Bearer auth says nothing about the body. A signature over (@method, @target-uri, content-digest, mcp-session-id, mcp-protocol-version) binds the credential to this specific request: changing the body, method, URI, session ID, or protocol version invalidates the signature. Tampering by a compromised proxy or malicious middlebox is detectable rather than silent.

  4. No replay protection. A captured bearer can be replayed indefinitely until rotation, against any endpoint, with any payload. Signatures with created + nonce constrain each signed request to a freshness window (default 300s) and a single use, collapsing the leak blast radius from "until rotated" to single-digit minutes.

  5. No verifiable client identity. The bearer model gives the server no cryptographic way to answer "is this the same client instance I talked to before?" The signature tag (this SEP) and keyid (RFC 9421) give a stable, unforgeable client identifier without requiring a heavyweight OAuth dance — enabling per-client rate limits, scoped permissions, audit logs, and anomaly detection.

  6. Principle of least exposure. Current OAuth flows leave MCP clients in possession of access tokens really intended for server↔resource-server communication. Proof-of-possession lets the spec stop handing clients credentials they didn't need in the first place.

The same arguments drove the web's adoption of WebAuthn over passwords, DPoP over bare bearer tokens in OAuth 2.1, and HTTP Message Signatures in payment and federation protocols. MCP is currently the outlier.

Where this matters most: wallet-based and high-stakes authentication

A growing class of MCP servers brokers access to wallets and other high-value credentials on behalf of agents — signing EVM/Solana transactions, executing swaps, retrieving API keys, performing on-chain transfers, writing to production databases. For these servers the gaps above stop being theoretical:

  • A leaked bearer drains a wallet or exfiltrates secrets up to whatever policy limit exists. There is no recovery — the operation is on-chain or the data is out.
  • Without request-body binding, an intercepted approval for swap 100 USDC → ETH is indistinguishable from an attacker minting a fresh transfer everything → attacker against the same token.
  • Per-client policies (code or LLM-based) are predicated on "this is client X" — which today reduces to "whoever holds the bearer."

Proof-of-possession solves all three: the request body is signed, the client is the keyholder, the bearer becomes worthless on its own. As a corollary, the wallet key itself becomes the natural MCP client credential — the agent's authenticator is the same key material it already uses to sign on-chain.

This generalizes to any MCP server fronting non-revocable side effects: payments, on-chain operations, production writes, secret stores. As more financial and agentic infrastructure integrates over MCP (e.g. via Grok and Claude.ai connectors), the spec needs a proof-of-possession story that ships.

Relationship to existing proposals

Proposal Status Mechanism Relationship
SEP-2133 Final Extensions framework This proposal is an Extensions Track extension under 2133 — not a core change
SEP-1415 Dormant RFC 9421 Picks up 1415's mechanism with the technical-feedback fixes, repackaged as an extension
SEP-1932 Open (PR) RFC 9449 (DPoP) Complementary — DPoP for the OAuth-bound path, this for non-OAuth / wallet. No overlap in core
SEP-1036 Final URL elicitation Orthogonal — concerns user data; this concerns client identity
SEP-1372 Open Init-less MCP This extension works in both initialized and init-less modes
ext-auth (OAuth Client Credentials) Final OAuth 2.0 CC Sibling extension in the same repo; this adds PoP for the non-OAuth case

As an opt-in extension this is additive and disabled by default. Servers and clients that do not negotiate it are unaffected.

Specification

1. Conformance language

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, MAY are to be interpreted as described in BCP 14 when, and only when, they appear in all capitals.

2. Profile of RFC 9421

2.1 Signed components

Every signed MCP request MUST sign the following components (RFC 9421 §2):

Component Source Notes
@method Derived HTTP method
@target-uri Derived Full request URI
content-digest HTTP field SHA-256 of body per RFC 9530. Required even for empty bodies (digest of empty input).
mcp-protocol-version HTTP field MUST be included when present (per SEP-1372 init-less mode).
mcp-session-id HTTP field MUST be included for all post-initialize requests. MUST NOT be included on the initialize request itself or on init-less first requests, since no session is bound yet.

Servers MUST reject signatures whose Signature-Input does not cover all applicable components above.

2.2 Signature parameters

The following RFC 9421 signature parameters MUST be present:

  • created: Unix epoch seconds at signature creation time. Servers MUST reject signatures whose created is more than 300 seconds in the past or more than 60 seconds in the future, accounting for clock skew.
  • nonce: A per-request unique value. MUST be at least 96 bits of cryptographic randomness, base64url-encoded.
  • keyid: The JWK Thumbprint per RFC 7638 of the public verification key.
  • tag: The stable client identifier (see §2.5). For first contact, equals keyid; thereafter remains stable across key rotation.

The alg parameter MUST NOT be sent and MUST be ignored if present. The signature algorithm is derived from the public key material (see §2.4), in line with RFC 9421 §7.3.5's guidance against alg-substitution attacks.

2.3 Replay protection

Servers MUST maintain a nonce cache scoped to (tag, time-window) and MUST reject any request whose (tag, nonce) tuple has been seen within 2 × max_clock_skew + freshness_window (default: 660 seconds).

Servers SHOULD NOT cache full signature values; the nonce-based check is sufficient and is constant-size per client.

2.4 Key material

Public keys are conveyed as JWKs per RFC 7517. Implementations MUST support at least the following curve/algorithm pairing:

  • Ed25519 (kty: "OKP", crv: "Ed25519") — RECOMMENDED default. Algorithm: ed25519 per RFC 9421 §3.3.6.

Implementations MAY additionally support:

  • ECDSA P-256 (kty: "EC", crv: "P-256") with ecdsa-p256-sha256.
  • ECDSA P-384 (kty: "EC", crv: "P-384") with ecdsa-p384-sha384.

The signature algorithm is determined by the JWK kty + crv combination. Servers MUST NOT accept a signature whose computed algorithm does not match the algorithm registered for the presented keyid.

The keyid value MUST equal the JWK Thumbprint (RFC 7638) of the presented JWK; servers MUST reject any mismatch.

2.5 Stable client identity via tag

The tag parameter (RFC 9421 §2.3) carries a stable client identifier. It is opaque to the protocol and MUST be:

  • Generated by the client on first contact (one-time, persistent per logical client instance).
  • Cryptographically bound to the initial key by being signed in the first request that introduces the key.
  • Stable across key rotations (a rotation request signs a new key under the old key + same tag; see §3.4).

This separates identity (the tag) from proof material (the key), letting servers retain audit trails, rate-limit buckets, and per-client policies through key changes.

2.6 Key distribution
Identity model & extensibility

This SEP is opinionated on the wire profile (§2.1–2.5: covered components, the alg prohibition, replay, keyid == JWK Thumbprint, the stable tag) and pluggable on identity and key discovery. How a request is signed and verified is fixed; where the server learns the verifying key — and any richer identity attached to it — is an extensible set of key-resolution methods over that same signing base.

Conforming servers MUST implement the Signature-Agent baseline (§2.6.1) and MAY accept additional registered methods, advertising the set they accept via the capabilities block (§3.1). The design intent is a decentralized default — no central identity authority required — with centrally-managed identity available opt-in for deployments that want it:

Method Identity model Status
Signature-Agent (Web Bot Auth HMS Directory) Decentralized; key self-published MUST (baseline)
cnf in clientInfo Session-bound, decentralized MAY
WIMSE Workload-Identity-Token Centrally-issued workload identity MAY (registered)
AAuth assertion OAuth-extension identity MAY (registered)

Identity beyond "the keyholder" — multi-hop / call-chain attribution to defeat the confused-deputy problem — is out of scope (§"What this SEP does not address"); the stable tag and the signed content-digest are the hooks a PIC / receipt-chain layer (or WIMSE) binds to per actor.

Per draft-meunier-http-message-signatures-directory, clients MAY include a Signature-Agent header on any signed request:

Signature-Agent: "data:application/http-message-signatures-directory+json;base64,<base64url(JWKS)>"

For desktop / local clients that cannot host a JWKS URL, the data: form is REQUIRED. For hosted clients, the URL form (Signature-Agent: "https://client.example/.well-known/http-message-signatures-directory") is permitted.

This is the only key-distribution method that works for init-less first contact (SEP-1372).

2.6.2 cnf in the extension settings (initialize-bound mode)

Clients that perform initialize MAY additionally bind a key by carrying a cnf (confirmation) member in this extension's settings object under capabilities.extensions (§3.1) — not in core clientInfo, so no core schema is touched:

{
  "method": "initialize",
  "params": {
    "capabilities": {
      "extensions": {
        "io.modelcontextprotocol/http-message-signatures": {
          "cnf": {
            "jwk": {
              "kty": "OKP",
              "crv": "Ed25519",
              "x": "3TQjAzEPW-sE81J4eWUuI2ZQCRJKELwdco_cJa7e9pM"
            }
          }
        }
      }
    },
    "clientInfo": { "name": "example-client", "version": "1.0.0" }
  }
}

The initialize request itself MUST be signed with the corresponding private key. The server binds (mcp-session-id, jwk-thumbprint, tag) for the lifetime of the session.

2.6.3 Registered optional methods (WIMSE, AAuth)

Deployments that need centrally-managed or federated identity MAY carry it in the key-resolution slot without changing the signing base:

  • WIMSE Workload-Identity-Token (draft-ietf-wimse-http-signature) — the token carries the verification key material and binds it to a workload identity. Suited to enterprise / multi-workload deployments with an identity authority.
  • AAuth assertion (draft-hardt-aauth-protocol) — for deployments already extending OAuth 2.

A server is not required to implement these; it advertises which it accepts via §3.1. In all cases the signing profile (§2.1–2.5) is unchanged — only keyid resolution differs.

2.7 Optional canonical (JCS) content digest

The baseline content-digest (§2.1) is the RFC 9530 digest over the exact request octets. An intermediary that re-serializes the JSON body (key reorder, whitespace, unicode normalization) invalidates that digest even when the payload is semantically unchanged — relevant for receipt / evidence-relay topologies (e.g. draft-hopley-x402-payment-evidence-frame).

For those topologies, servers and clients MAY negotiate an additional covered component carrying a digest computed over the JCS-canonicalized (RFC 8785) payload, advertised + selected via the capabilities block (§3.1) so both sides agree before relying on it. The byte-exact content-digest remains the MUST baseline; the JCS variant is purely additive, for deployments whose path includes re-serializing relays. JCS is not mandated by default — it imports canonicalization edge cases (number/exponent form, unicode) that the common single-hop MCP topology does not need.

3. Server behavior

3.1 Capability advertisement

This extension is negotiated through the capabilities.extensions map defined by SEP-2133 — not through a new core ServerCapabilities field. The server advertises the extension identifier with a settings object in the initialize response; the client does the same in the request:

{
  "capabilities": {
    "extensions": {
      "io.modelcontextprotocol/http-message-signatures": {
        "algorithms": ["ed25519"],
        "requiresSignature": false,
        "keyResolution": ["signature-agent", "cnf"],
        "contentDigest": ["rfc9530"]
      }
    }
  }
}

Settings object (an empty {} is permitted and means "supported, with defaults"):

  • algorithms — accepted algorithms in server preference order ("ed25519" baseline; "ecdsa-p256-sha256" / "ecdsa-p384-sha384" optional).
  • requiresSignature — when true, the server rejects unsigned requests; defaults to false.
  • keyResolution — accepted key-resolution methods (§2.6); "signature-agent" is the baseline, "cnf" / "wimse" / "aauth" optional.
  • contentDigest — accepted content-digest profiles (§2.7); defaults to ["rfc9530"].

Per SEP-2133 the extension is disabled by default and requires explicit opt-in. A server that sets requiresSignature: true MUST reject unsigned requests with HTTP 401 Unauthorized and an Accept-Signature header (§3.3); otherwise it MUST gracefully fall back to core bearer/OAuth behavior for clients that do not negotiate the extension.

3.2 Verification flow

On receiving a signed request, the server MUST:

  1. Parse Signature-Input and Signature headers (RFC 9421 §4.2).
  2. Resolve the public key for the keyid:
    • First, consult the session-bound key (if a session exists).
    • Otherwise, parse Signature-Agent.
    • Otherwise, reject with 401.
  3. Verify the keyid equals the JWK Thumbprint of the resolved key.
  4. Verify created is within [now - 300s, now + 60s].
  5. Verify (tag, nonce) has not been seen.
  6. Verify all required components from §2.1 are present in Signature-Input.
  7. Verify the signature cryptographically using the algorithm derived from the key (§2.4).
  8. Record (tag, nonce) in the replay cache before responding.

Failures in steps 1–7 MUST produce HTTP 401 Unauthorized with Accept-Signature (§3.3) and, optionally, a JSON-RPC error body describing which check failed.

3.3 Algorithm and component negotiation via Accept-Signature

When rejecting a signed request for protocol-level reasons (missing component, unsupported algorithm, unsigned-but-required), the server MUST include an Accept-Signature header (RFC 9421 §5.1) describing what it expects:

HTTP/1.1 401 Unauthorized
Accept-Signature: sig1=("@method" "@target-uri" "content-digest" "mcp-protocol-version" "mcp-session-id");created;nonce;tag="mcp-server.example"

This replaces the custom JSON-RPC error envelope proposed in SEP-1415, keeping signature-layer negotiation at the signature layer.

When multiple algorithms are acceptable, the server MUST advertise them via the capabilities mechanism (§3.1) and MAY repeat the Accept-Signature header per algorithm.

3.4 Key rotation

A client rotates its key by sending a signatures/rotateKey request signed with both the old and new keys (two Signature entries: sig-old and sig-new):

{
  "method": "signatures/rotateKey",
  "params": {
    "newKey": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
  }
}

The server MUST verify both signatures, that the tag matches, and that the new keyid equals the new JWK's thumbprint. On success the server replaces the bound key and SHOULD retain audit history.

4. Client behavior

4.1 Signer abstraction

This SEP does not constrain where signing happens. RFC 9421 only requires that the request carries a valid Signature under the public key the server has bound for the client. Conforming client implementations SHOULD treat signing as an abstract operation that can be satisfied by any of the following, interchangeably:

  • In-process key material held in a non-extractable form (e.g. WebCrypto SubtleCrypto with extractable: false).
  • OS-managed key store (macOS Keychain, Windows DPAPI / CNG, Linux Secret Service / kernel keyring).
  • An out-of-process local signer following an ssh-agent-style pattern — the agent process delegates signing to a separate process over a Unix socket or named pipe, and never sees the private key itself. Reference instantiations include ssh-agent, gpg-agent, hardware-backed wallet daemons, and projects like authsome.
  • Hardware tokens (YubiKey, Ledger, TPM, Secure Enclave).
  • A remote KMS (AWS KMS, GCP KMS, HashiCorp Vault Transit) reached over an authenticated channel.

The out-of-process variants are explicitly recommended for deployments where the surrounding agent process executes untrusted LLM-generated code, since the model cannot exfiltrate key material it never held. PoP at the protocol layer does not by itself defend against an attacker who can coerce the agent process to print env vars or shell history; pairing PoP with an out-of-process signer does.

4.2 Requirements

Clients SHOULD:

  • Generate one keypair per (MCP server origin, logical client identity).
  • Persist private key material in one of the storage classes above; never in plaintext config.
  • Generate a fresh nonce per request from a CSPRNG.
  • Implement the Accept-Signature negotiation loop (single retry on 401 if the server's requirements were unmet).

Clients MUST NOT:

  • Reuse nonce values.
  • Send the alg signature parameter.
  • Send the private key over the wire under any circumstances.

5. Core schema impact: none

As an Extensions Track extension (SEP-2133) this proposal makes no change to the core MCP schema. All negotiation rides the existing capabilities.extensions map (§3.1); the key material (cnf), algorithm set, key-resolution methods, and content-digest profile all live inside this extension's settings object — not in core ClientCapabilities, ServerCapabilities, or Implementation.

The only method the extension introduces is signatures/rotateKey (§3.4). It is scoped to this extension and MUST be routed only when the extension has been negotiated; implementations SHOULD treat it as namespaced to the extension identifier to avoid collisions.

6. Example signed request

POST /mcp HTTP/1.1
Host: server.example.com
Content-Type: application/json
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
MCP-Protocol-Version: 2026-03-26
MCP-Session-Id: sess_abc123
Signature-Agent: "data:application/http-message-signatures-directory+json;base64,eyJrZXlzIjpbey4uLn1dfQ=="
Signature-Input: sig1=("@method" "@target-uri" "content-digest" "mcp-protocol-version" "mcp-session-id");created=1747641600;nonce="V8wA3pX2qF7nL1cR";keyid="7CECy79zsOm80SK3EgvHmsLM39PfUmbfSTxqOLhfgsM";tag="agent-instance-9f3e"
Signature: sig1=:LAH8BjcfcOcLojiuOBFWBo0hWDdMOGvAdEBM0h/MIRoNfj6Ae5OOKlEOUcKTOvXsyHkC5bV9D6m9QQDnR3QxDw==:

{"jsonrpc":"2.0","id":"req_42","method":"tools/call","params":{"name":"wallet_sign_swap","arguments":{}}}

Note that alg is absent (derived from the Ed25519 JWK), created and nonce are parameters (not components), and tag provides the stable client identity.

Rationale

Why RFC 9421 and not DPoP (SEP-1932)

DPoP (RFC 9449) and HTTP Message Signatures (RFC 9421) solve overlapping but distinct problems:

Concern DPoP RFC 9421 (this SEP)
Bind credential to a specific request Yes Yes
Requires OAuth flow Yes (sits inside one) No
Works for non-OAuth MCP servers (API key, anonymous) No Yes
Signs full request including body Optional, often not Yes (content-digest required)
Industry adoption beyond OAuth Low Higher (federation, payments, Web Bot Auth)
Layered above TLS termination Yes Yes

These should ship together: DPoP for the OAuth-bound case (where SEP-1932 already specifies it), RFC 9421 for the broader case where MCP servers want PoP without dragging in an OAuth dependency. Most wallet/payment use cases fall in the latter bucket.

Why fix SEP-1415 rather than replace it

The original 1415 proposal is fundamentally correct in mechanism — RFC 9421 is the right tool — but the discussion thread surfaced a series of small but blocking RFC-compliance bugs (incorrect signed-component lists, alg parameter usage, custom error envelopes, missing init-less story). This SEP keeps the mechanism and fixes each one explicitly, with citations to the underlying RFCs and the original thread comments.

Alternative: signed JSON-RPC envelopes

Considered and rejected. Signing the JSON-RPC payload alone misses HTTP-layer integrity (headers, URI, method), is incompatible with existing HTTP signing tooling and proxies, and forces every transport (stdio, WebSocket, Streamable HTTP) to re-invent a parallel scheme. RFC 9421 is HTTP-specific by design; stdio-only deployments are out of scope for this SEP and can be addressed in a follow-up if needed.

Alternative: mTLS

Considered and rejected as primary mechanism. mTLS is incompatible with the vast majority of MCP deployments that terminate TLS at a CDN or load balancer, and is impractical for local desktop clients. mTLS remains a valid additional layer where the deployment supports it.

Backward compatibility

Fully additive:

  • Servers that do not implement this SEP ignore unknown headers (Signature, Signature-Input, Signature-Agent, Content-Digest) and continue to function on existing bearer/OAuth auth.
  • Clients that do not implement this SEP send unsigned requests; servers that do not set requiresSignature: true accept both.
  • The cnf field on clientInfo is optional; servers that don't understand it ignore it per the general capabilities-negotiation rules.
  • No existing message format changes.

Security considerations

Threat coverage

Threat Without this SEP With this SEP
Bearer-token theft Full client compromise Bearer alone insufficient; signature still required
Request replay Full replay surface Bounded by 300s + nonce cache
Request tampering by proxy Undetectable Detected (content-digest covered)
Session-ID hijack Full takeover Hijacker lacks private key
Server-side credential exfiltration Token usable anywhere Only past signatures observable; key never exposed
TLS-terminating intermediary Sees bearer Sees signed requests it cannot forge

New attack surface

  • Compromised client key store. Same residual risk as any PoP scheme; mitigated by OS keychain / HSM storage and key rotation (§3.4).
  • Nonce cache exhaustion / DoS. Bounded by per-tag rate limiting and the small per-entry cost (a single (tag, nonce) tuple per accepted request, evicted after the freshness window).
  • alg substitution. Explicitly prevented by §2.2's prohibition of the alg parameter and algorithm derivation from key material.
  • keyid confusion. Prevented by §2.4 requiring keyid == JWK Thumbprint and a single canonical thumbprint computation.

What this SEP does not address

  • Authorization. Message signing proves the request came from the keyholder; it says nothing about what that keyholder is allowed to do. Authorization is the job of OAuth scopes, ext-auth, or per-tool policy.
  • User identity. A signed request proves a specific client instance sent the request. Tying that to a human user remains the job of OAuth / OIDC.
  • Server-side authentication of the server to the client. TLS / OOB pinning continues to handle this. A future SEP could profile RFC 9421 signed responses if there is demand.

Reference implementation

A reference implementation is planned in two parts:

  1. Server-side (Rust): an axum::middleware layer wrapping the rmcp Streamable-HTTP transport, verifying RFC 9421 signatures per §2 and emitting Accept-Signature per §3.3.
  2. Client-side (TypeScript): a fetch wrapper for the official @modelcontextprotocol/sdk that signs outbound requests using @waqas-shoukat/http-message-signatures or equivalent, with Ed25519 keys held in the browser's SubtleCrypto non-extractable form or a desktop OS keychain.

Both implementations will be linked from this SEP once Draft → In-Review transitions and a sponsor is assigned.

Open questions

  1. Should signatures/rotateKey be its own RPC method, or should rotation reuse initialize with cnf?
  2. Should the spec mandate Ed25519, or only RECOMMEND it (current text)?
  3. Should the freshness window (300s) be server-configurable and advertised in capabilities, or fixed by the spec?
  4. Is a future signed-response counterpart in scope for a separate SEP, or should we leave that to mTLS / TLS pinning indefinitely?

Extension governance (SEP-2133)

Field Value
Extension identifier io.modelcontextprotocol/http-message-signatures
Home repository modelcontextprotocol/ext-auth (on acceptance)
Incubation experimental-ext-http-message-signatures (proposed), associated with the MCP Authorization Working Group
Working Group MCP Authorization
License Apache-2.0
Stability Experimental during incubation; opt-in, disabled by default

Per SEP-2133 this is an Extensions Track proposal: reviewed by the Core Maintainers for inclusion as an official extension, and thereafter evolving independently under the ext-auth maintainers and the Authorization WG. It is OAuth-agnostic and complementary to DPoP (SEP-1932), sitting beside OAuth Client Credentials in ext-auth. A reference implementation in an official SDK is provided (see Reference implementation), satisfying the Extensions Track prerequisite. Breaking changes will use a new identifier (e.g. io.modelcontextprotocol/http-message-signatures-v2); non-breaking changes are negotiated through the settings object (§3.1).

References

SEP-2792 Per-Request User Language Preference

Draft · Standards Track · Created 2026-05-26

Imported from upstream. Proposed as modelcontextprotocol#2792 by @SamMorrowDrums on 2026-05-26, 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 defines a transport-neutral way for an MCP client to propagate a user's natural-language preference on each request and for a server to report the resulting language of its response. The canonical fields are io.modelcontextprotocol/acceptLanguage in request _meta and io.modelcontextprotocol/contentLanguage in response _meta.

The fields reuse HTTP semantics rather than defining MCP-specific language syntax: acceptLanguage uses the Accept-Language field-value grammar and meaning, including weighted language ranges and the wildcard; contentLanguage uses Content-Language syntax and meaning; matching follows RFC 4647. Streamable HTTP mirrors the canonical _meta values into the standard HTTP fields and uses Vary: Accept-Language for language-dependent cache variants.

This is language-preference propagation, not a general locale system or translation framework. It does not communicate timezone, currency, calendar, measurement, formatting, or other non-language preferences. The preference is request-scoped and does not create session state.

Motivation

MCP exposes user-facing text before a model or tool is chosen. Discovery responses contain display titles, elicitation requests contain UI messages and form labels, and progress notifications can contain user-visible status text. MCP currently has no common way to carry the language preference already known to an operating system, desktop application, terminal, or HTTP user agent.

Relying on a model is not sufficient. A model is not guaranteed to know the user's operating-system or application preference, and putting that preference into chat context is indirect and unreliable. Tool arguments are also too late for discovery and would require every tool and method to define duplicate semantics.

MCP is intended to work globally and across transports. HTTP already defines a widely implemented language-preference signal, but stdio and other transports need equivalent semantics. Defining the signal in core _meta and mirroring it to HTTP preserves transport parity without inventing syntax, matching rules, or translation infrastructure.

Why this is core protocol behavior

This proposal is optional to use, but its meaning belongs in the core specification rather than an extension:

  • Discovery and list methods need the preference before any model or tool selection.
  • Stateless MCP requires the current preference to travel with every request, not be established as connection or session state.
  • The signal propagates existing user context that MCP currently drops; it is not a new domain feature.
  • Existing core fields need one consistent interpretation. Defining competing extension semantics for the same title, elicitation, and progress fields would fragment client and server behavior.
  • HTTP clients can already carry this signal, while transport-neutral core semantics give stdio and future transports parity.

No capability negotiation is required. A client can send the preference to an older server, which ignores the unknown _meta key, and a server can ignore a preference it does not support.

Specification

Scope

This SEP standardizes only propagation, selection, and reporting of a user's natural-language preference. Language tags can contain script or region subtags, but those subtags do not imply any non-language locale behavior.

This SEP does not define translation catalogs, message formatting, machine translation, or preferences for timezone, currency, calendars, collation, number or date formats, units of measurement, location, or other locale concerns.

_meta fields

Field Direction Type Meaning
io.modelcontextprotocol/acceptLanguage Request string The user's language preference, with the syntax and semantics of HTTP Accept-Language.
io.modelcontextprotocol/contentLanguage Response string The language or languages used for the intended audience, with the semantics of Content-Language.

_meta is the canonical carrier on every transport.

Request behavior

A client that knows an applicable user language preference SHOULD include io.modelcontextprotocol/acceptLanguage in params._meta on every request. The value MUST conform to the HTTP Accept-Language field-value grammar in RFC 9110 §12.5.4. This includes weighted RFC 4647 language ranges and the wildcard, for example:

fr-CA,fr;q=0.9,en;q=0.5

The preference is request-scoped:

  • A client MAY change or omit it on any request.
  • A server MUST NOT bind it to a connection or logical session.
  • A server MUST NOT reuse a preference from an earlier request when processing a later request that omits or changes the field.
  • Messages produced while processing one request, such as an input-required elicitation or a progress notification, use that request's selected language.

A server MAY ignore the preference.

A server that participates MUST use matching behavior compatible with HTTP language negotiation and RFC 4647, including weights and wildcard semantics. The server can use lookup or filtering as appropriate to its representation. It MUST NOT define a different interpretation of the field value.

If no supported language matches, the server MUST use its server-defined default and MUST NOT return an error solely because the preference was unmatched.

Response behavior

If a server honors acceptLanguage, including by falling back to its default, it MUST report the actual language used in io.modelcontextprotocol/contentLanguage. The value MUST follow Content-Language syntax and meaning in RFC 9110 §8.5: one or more BCP 47 language tags describing the natural language of the intended audience. It does not need to enumerate languages that appear only in quotations, names, or embedded source material.

The field is carried:

  • in result._meta for a successful or input-required response;
  • in error.data._meta for an error response whose user-facing text is localized; and
  • in params._meta for a localized notification associated with the request.

If error.data carries contentLanguage, it MUST be an object. The machine-readable error.code remains unchanged.

A server that ignores acceptLanguage MAY omit contentLanguage. Omission does not identify any language.

Translation boundary

Language selection does not imply that every natural-language string should be translated. When a server honors the preference and has a supported selected language, it SHOULD translate these display-oriented fields:

  • BaseMetadata.title on Implementation, Tool, Resource, ResourceTemplate, Prompt, PromptArgument, and PromptReference;
  • ToolAnnotations.title;
  • ElicitRequestFormParams.message and ElicitRequestURLParams.message;
  • property-level title and description values in ElicitRequestFormParams.requestedSchema (property keys remain stable); and
  • ProgressNotificationParams.message.

A server MAY translate Error.message when it is intended for user display. Error.code remains stable.

A server MAY translate other natural-language, model-facing, or returned content, but this is not expected in ordinary circumstances. Translation can change meaning and model behavior, including tool selection and argument generation. This category includes general description fields such as Tool.description, Resource.description, ResourceTemplate.description, Prompt.description, PromptArgument.description, and Implementation.description, as well as tool, prompt, and resource content. Servers that translate such content are responsible for preserving its domain-specific semantics.

A server MUST NOT translate a machine-interpreted identifier or token. This categorical rule includes:

  • MCP method names;
  • every BaseMetadata.name and every params.name, including tool, prompt, and resource names;
  • URIs and URI templates;
  • MIME types;
  • JSON and JSON Schema property keys;
  • enum and const values;
  • type, result, role, and other discriminators;
  • capability and extension identifiers;
  • protocol versions;
  • error codes;
  • _meta keys; and
  • opaque IDs, cursors, state values, and tokens.

Explicit language selection takes precedence

acceptLanguage is fallback user context, not a replacement for method-specific or domain-specific content selection. If a method or tool has an explicit language argument, that argument SHOULD take precedence over the negotiated preference for the content it controls.

For example, a tool that retrieves an official legal document can expose a language argument because the chosen edition is part of the operation's meaning. The preference still localizes discovery before the tool is selected and MAY provide a fallback when the explicit argument is absent.

Streamable HTTP binding

Streamable HTTP mirrors the canonical _meta values into standard HTTP fields:

HTTP field Canonical MCP field
Accept-Language params._meta["io.modelcontextprotocol/acceptLanguage"]
Content-Language response _meta["io.modelcontextprotocol/contentLanguage"]

A Streamable HTTP client that sends acceptLanguage MUST send an Accept-Language field whose value agrees exactly with it.

Exact field value agreement

Language matching is semantic: RFC 4647 over the parsed value, with case-insensitive tags, weights, and the wildcard as RFC 9110 §12.5.4 defines them. Mirror agreement is a different question and is decided by exact equality of the two exposed field values. It is not semantic Accept-Language or Content-Language equivalence, and it is not equality of the bytes on the wire, which differ between HTTP versions.

A receiver determines agreement as follows:

  1. Take the decoded _meta string from the parsed JSON body. The implementation MUST NOT trim, case-fold, reorder, or otherwise normalize it.
  2. Take the HTTP field value after normal field parsing. Optional whitespace surrounding a field line is not part of the field value (RFC 9110 §5.5). If the HTTP API exposes repeated field lines separately, join every value in received order with a comma and a space (", "), per the consistency recommendation in RFC 9110 §5.3; if it exposes the complete combined field value, use that directly.
  3. Compare the two strings character for character. Implementations MUST NOT parse, normalize, or re-serialize either value beyond the join in step 2. Both grammars are ASCII, so this is equivalent to comparing their ASCII encodings octet for octet.

Implementations MUST use an HTTP API that exposes either every field line value or the complete combined field value. An accessor that returns only the first value of a repeated field is not sufficient, because it can hide a conflicting field line.

Senders MUST emit exactly one field line for each mirrored language field, so that no legal recombination of repeated field lines can change the value a receiver computes.

Internal whitespace, tag casing, range order, and q-value spelling are therefore significant, even where HTTP treats the values as equivalent: en-US and en-us, en;q=0.9 and en;q=0.900, and en-US,en and en-US, en are each pairs that do not agree.

Presence and mismatch

The payload/header agreement rule from SEP-2243 and the current Streamable HTTP server validation applies when the header is present. The server MAY tolerate a missing or stripped Accept-Language header and use the canonical _meta value. If both are present and do not agree, the server MUST reject the request with HTTP 400 Bad Request and HeaderMismatch (-32020). A bare header without the canonical _meta field does not create an MCP preference.

Agreement is evaluated before grammar validation, so the outcome never depends on whether either value happens to be well formed.

For a JSON response carrying contentLanguage, including an error response carrying it in error.data._meta, the server MUST mirror it into Content-Language. If a client receives both values and they do not agree, the response is malformed.

An SSE response header describes the stream as a whole and is fixed before individual messages are produced. A server MAY omit Content-Language on text/event-stream, unless one value accurately describes every applicable message on that stream. If it does send one, every contentLanguage carried by a message on that stream MUST agree with it. Each localized response or notification still carries its own canonical _meta value.

Non-HTTP transports use only _meta; all request scope, matching, fallback, reporting, and translation rules remain the same.

Caching

Language-dependent responses remain cacheable. Existing ttlMs and cacheScope freshness and sharing semantics are unchanged.

Any MCP or client cache that reuses a response MUST include the request's acceptLanguage value in its cache key when the representation varies by language. A cached response for one preference MUST NOT be served for a different preference. This applies even when both preferences happen to select the same language.

An HTTP response that varies by language MUST include Vary: Accept-Language. If a shared cache cannot observe the language signal, for example because an intermediary stripped the header while preserving _meta, the response MUST either be private or use an equivalent language-aware key visible to that cache. cacheScope: "public" permits sharing across authorization contexts; it does not permit sharing across language variants without the required language key.

Examples

Localized discovery with stable identifiers

The client sends its preference before any model or tool argument is involved:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/acceptLanguage": "fr-CA,fr;q=0.9,en;q=0.5"
    }
  }
}

The server localizes the display title while preserving the machine name:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "tools": [
      {
        "name": "weather_current",
        "title": "Météo actuelle",
        "inputSchema": {
          "type": "object"
        }
      }
    ],
    "ttlMs": 300000,
    "cacheScope": "public",
    "_meta": {
      "io.modelcontextprotocol/contentLanguage": "fr-CA"
    }
  }
}

The cache entry is keyed by the request's acceptLanguage value in addition to the existing request and authorization dimensions.

Localized elicitation UI with a stable field key

While processing a request with acceptLanguage: "de", a server can return an input-required result containing localized elicitation text:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "contact": {
        "method": "elicitation/create",
        "params": {
          "message": "Bitte geben Sie Ihre E-Mail-Adresse ein.",
          "requestedSchema": {
            "type": "object",
            "properties": {
              "email": {
                "type": "string",
                "format": "email",
                "title": "E-Mail-Adresse",
                "description": "Adresse für die Bestätigung"
              }
            },
            "required": ["email"]
          }
        }
      }
    },
    "_meta": {
      "io.modelcontextprotocol/contentLanguage": "de"
    }
  }
}

The property key email, schema type, and format remain unchanged.

Explicit content language override

Suppose the same user preference is "en", but a get_official_document tool call includes "language": "ga". The explicit argument selects the Irish edition of the returned resource content. The preference can still localize discovery and other display fields, but it does not override the operation's explicit content selection.

Rationale and Alternatives

Why _meta

Language preference is orthogonal to any individual method and applies before method-specific behavior. _meta carries it without changing every request shape and works on HTTP, stdio, and future transports.

Why reuse HTTP semantics

A single language tag would lose preference ordering, weighted fallback, and wildcard behavior. Reusing Accept-Language, Content-Language, RFC 4647, Vary, and HTTP privacy guidance gives implementations established parsers, matchers, and cache behavior without an MCP-specific grammar.

Why not an extension

An extension would make core discovery and UI fields depend on an optional, separately interpreted contract. Competing extensions could assign different language semantics to the same fields, and clients would need the preference before discovering which extension or tool applies. Core _meta defines one transport-neutral meaning while remaining backward compatible and optional to honor.

Why not tool arguments or model context

Tool arguments cannot localize tools/list, prompt and resource discovery, elicitation UI on methods with no arguments (such as resources/read), or progress messages before a tool call. Adding the preference to every tool would duplicate syntax and matching rules. Model context is also not a reliable source for a host-level user preference and unnecessarily consumes model-visible context. Explicit domain language arguments remain appropriate when language changes the operation itself and take precedence as specified above.

Why per request

Users can change preferences at any time, and one client or server connection can serve multiple users or contexts. Per-request propagation follows stateless MCP and prevents stale connection state from selecting the wrong language.

Rejected alternatives

  • HTTP headers only: excludes stdio and makes semantics transport-specific.
  • A bespoke locale field: conflates language with unrelated locale concerns and discards standard weighted fallback semantics.
  • Handshake or capability state: conflicts with stateless operation and cannot represent a changed preference on the next request.
  • Automatic translation of all natural-language fields: changes model-facing semantics and exceeds the display-oriented need this SEP addresses.

Backward Compatibility

The new _meta keys are optional and unknown _meta keys are ignored. Existing clients and servers continue to use their current language behavior. Servers can adopt matching, reporting, translation, and HTTP mirroring incrementally, provided that a server claiming to honor the preference follows all requirements in this SEP.

Existing HTTP deployments that rewrite Accept-Language while leaving the JSON body unchanged will cause HeaderMismatch. Normalizing whitespace, case-folding tags, reordering ranges, canonicalizing q values, and splitting or recombining field lines each change the value a receiver computes, even though HTTP permits them. An intermediary on an MCP path must preserve each mirrored language field exactly or remove it; removal is always safe, because a missing Accept-Language is tolerated. This is similar to the routing-integrity requirement already used by Streamable HTTP mirroring.

Security and Privacy

Accept-Language can increase fingerprinting entropy. Clients SHOULD follow the privacy guidance in RFC 9110 §12.5.4, including sending only the detail needed for the interaction or omitting the preference when appropriate.

Receivers MUST validate the field-value grammar before using a parser or matcher. A malformed canonical value is ignored as an absent preference and MUST NOT on its own produce an error. Because agreement is checked first and compares the values as strings, a present HTTP mirror still has to agree exactly, whether or not either value is well formed.

Incorrect cache variation can disclose one user's localized representation to another user. Implementations MUST follow the cache-key, cacheScope, and Vary requirements above.

Translations of model-facing text can alter model behavior. Servers that use the MAY category should consider the implications of this. Machine-interpreted identifiers remain stable to prevent routing, validation, authorization and parsing failures.

Reference Implementation

modelcontextprotocol/typescript-sdk#2158 is a reference implementation covering Streamable HTTP and stdio. github-mcp-server PR #25 is prior art for server-side translation catalogs that can consume the request-scoped preference.

Conformance

Per SEP-2484, a conformance scenario is required before this SEP can reach Final. Tests should cover at least:

  1. Weighted ranges, wildcard matching, supported selection, and default fallback without an unmatched-preference error.
  2. Preference changes and omission on consecutive requests over one connection, proving no earlier value is reused.
  3. Localized tools/list titles with stable names and a reported contentLanguage.
  4. Localized elicitation and progress display text with stable property keys and tokens.
  5. Preservation of all machine-interpreted identifiers and explicit language arguments taking precedence.
  6. HTTP request and JSON response mirroring, tolerated missing Accept-Language, HeaderMismatch (-32020) on a present mismatch, and a malformed response on conflicting duplicated values.
  7. Exact agreement edge cases, each of which MUST NOT agree: en-US and en-us; en-US,en;q=0.9 and en-US, en;q=0.9; en;q=0.9 and en;q=0.900. A field line Accept-Language: en and a _meta value of en MUST agree, because the surrounding whitespace is not part of the field value. Two field lines valued en-US and en;q=0.9 combine to en-US, en;q=0.9 and MUST agree with exactly that _meta value.
  8. SSE and notification reporting through per-message _meta.
  9. MCP and HTTP cache separation across language preferences, including ttlMs, cacheScope, Vary: Accept-Language, and the stripped-header shared-cache case.
  10. HTTP language-preference privacy guidance.

Acknowledgments

Thanks to @pja-ant and @kurtisvg for the transport-parity and scope feedback on PR #2355, to the Transport Working Group for review, and to the authors of SEP-2243, SEP-2575, and SEP-2484.

SEP-2793 Tool Risk Metadata

Draft · Standards Track · Created 2026-05-27

  • Status: Draft
  • Type: Standards Track
  • Created: 2026-05-27
  • Author(s): walbis (@walbis)
  • Issue: #2793

Imported from upstream. Proposed as modelcontextprotocol#2793 by @walbis on 2026-05-26, 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 the cited prototype repository returns 404, so nothing can be run or verified. 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.

SEP Number #2793
Title Tool Risk Metadata
Author walbis
Sponsor seeking
Status Draft
Created 2026-05-27
Specification MCP 2025-11-25 (draft)
Prototype https://github.com/walbis/karai (config/tool_policies.yaml — manual catalogue, ~30 tools) + https://github.com/walbis/mcp-risk-inferrer (heuristic inferrer, v0.1)
PR #2793 (https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2793)
SDKs TBD

Abstract

Extend ToolAnnotations with structured, machine-readable risk metadata — graded riskLevel, action category, blastRadius, reversibility, sideEffects, approvalRecommendation, and minTrustLevel — so MCP clients can make consistent allowlist and approval decisions across tools without each client rebuilding the same bespoke per-tool catalogue.

All fields are optional and purely additive; clients that don't understand them are unaffected, and servers that don't declare them work unchanged.

Motivation

ToolAnnotations already carries boolean hints (readOnlyHint, destructiveHint, idempotentHint, openWorldHint). Those answer yes/no questions; they don't answer how risky / what scope / how reversible. In practice every MCP-consuming agent platform reinvents the same graded vocabulary:

  • KARAI's config/tool_policies.yaml (~30 tools) carries min_level, risk_level (low|medium|high|critical), category (read|observe|mutate|delete|destroy|utility), destructive, and approval_escalation.
  • Other consumers (Claude Desktop, Cursor, Cline, Continue, OpenDevin, …) ship analogous allowlists in YAML/JSON config or hard-coded heuristics.
  • Tool authors have no standard place to declare intended risk — the description field is free-form, can't be parsed, and isn't trusted.

This causes:

  1. Approval fatigue / silent escalation. When consumers default too permissive, mutating tools are auto-approved; when too strict, read-only tools spam the operator. Without a graded scale this can't be tuned per category.
  2. N×M reinvention. Every (consumer × MCP server) pair re-classifies the same tools. A community catalogue (CycloneDX-style for tools) only works if there's a standard shape to fill in first.
  3. Compliance gap. Enterprise audit / BDDK / SOC2 reviewers ask "which tools require approval, why?" — there's no standard answer.

destructiveHint almost fills this gap but is binary: a tool that deletes one row and a tool that drops a whole namespace get the same flag. This proposal grades the existing direction rather than replacing it.

Specification

Extend ToolAnnotations in schema/draft/schema.ts:

export interface ToolAnnotations {
  // ── existing fields (unchanged) ─────────────────────────────────
  title?: string;
  readOnlyHint?: boolean;
  destructiveHint?: boolean;
  idempotentHint?: boolean;
  openWorldHint?: boolean;

  // ── new: graded risk metadata (all optional) ────────────────────

  /**
   * Graded risk classification, complementing the binary
   * `destructiveHint`. Clients use this to decide approval policy.
   */
  riskLevel?: "low" | "medium" | "high" | "critical";

  /**
   * Action category. Clients use this for default policy and UI.
   * `read` and `observe` are non-mutating; `mutate` changes state;
   * `delete` removes specific items; `destroy` removes broad scopes.
   */
  category?: "read" | "observe" | "mutate" | "delete" | "destroy" | "utility";

  /**
   * Scope of an undesired outcome:
   * `item` — single resource;
   * `namespace` — a collection / project;
   * `cluster` — whole cluster or account;
   * `organization` — multi-tenant impact;
   * `global` — service-wide impact.
   */
  blastRadius?: "item" | "namespace" | "cluster" | "organization" | "global";

  /**
   * Whether the effect can be undone:
   * `auto` — tool is idempotent or has built-in undo;
   * `manual` — operator can roll back via a separate action;
   * `none` — irreversible (state loss, sent message, charged card).
   */
  reversibility?: "auto" | "manual" | "none";

  /**
   * Side effects the tool may produce beyond its primary action,
   * for compliance/audit reporting. Open vocabulary; common values:
   * `"state_loss"`, `"downtime"`, `"cost"`, `"external_notification"`,
   * `"data_exfiltration"`, `"privilege_change"`.
   */
  sideEffects?: string[];

  /**
   * Client policy hint:
   * `none` — auto-approve;
   * `single` — one approver;
   * `multi` — multiple approvers required.
   * Advisory; the client's own policy engine ultimately decides.
   */
  approvalRecommendation?: "none" | "single" | "multi";

  /**
   * Advisory minimum trust level on a 1–5 scale
   * (1 = observer / read-only operator, 5 = autonomous engineer).
   * Mirrors common agent-platform trust ladders; clients map to their
   * own scale.
   */
  minTrustLevel?: number; // 1..5
}

Default behaviour

A client that doesn't recognise these fields ignores them — purely additive. A server that doesn't declare them works exactly as today.

When a client implements risk-aware policy and a tool is missing risk metadata, the client SHOULD default to its most-restrictive policy bucket (e.g. riskLevel="critical", approvalRecommendation="multi") until an operator explicitly classifies that tool. This matches the "fail-closed" principle familiar to security-conscious deployments.

Inference

Servers can declare these fields; consumers without server-declared metadata can infer them from the tool's name, description, and input schema. A reference inferrer (https://github.com/walbis/mcp-risk-inferrer) ships a verb-based heuristic — get|list|describe → read/low; create|apply|update → mutate/medium; delete|destroy|drop → delete/high; risky flags (force, recursive, cascade, all_namespaces) bump risk one grade; scope-hint params (namespace, cluster, org) infer blastRadius — plus a planned optional LLM augmenter for nuance. The inferrer emits the same ToolAnnotations shape, so server-declared and inferred manifests are interchangeable.

Rationale

Why extend ToolAnnotations instead of adding a parallel risk block? Existing hints are already advisory metadata about the tool's runtime character. Risk is the same kind of advisory metadata at a finer grain; parallel structures would force clients to consult two places.

Why closed enums for most fields but open strings for sideEffects? Closed enums let clients build deterministic UI and policy logic; open strings let new side-effect categories emerge in the wild without spec revision (consumers ignore unknown values).

Why minTrustLevel as a number 1–5 instead of a string? Numeric ordering supports < / ≥ comparisons natively in policy engines. The 1–5 range mirrors the most common agent-trust ladder (observer → assistant → operator → engineer → autonomous); consumers using different scales map at the boundary.

Why isn't approval enforcement specified? Approval is a client concern (UI, identity, audit). The spec offers a recommendation the tool author surfaces; what the client does with it (auto-approve in dev, require dual approval in prod) stays a client policy decision.

Should destructiveHint be deprecated? No — it's the binary view many consumers already wire to. riskLevel is additive, not a replacement.

Backward compatibility

Pure addition to an optional object. Older clients ignore the new fields; older servers don't emit them. No breaking change.

Reference implementations

  • KARAI (https://github.com/walbis/karai) — already maintains a manual classification at config/tool_policies.yaml for 30+ K8s tools, using a vocabulary almost identical to this proposal. That catalogue is the proof of need: every consumer ends up writing one.
  • mcp-risk-inferrer (https://github.com/walbis/mcp-risk-inferrer) — reference inferrer that derives this metadata from existing tools that haven't declared it, so the ecosystem can bootstrap without waiting for every server to update. v0.1 ships a heuristic classifier (Python, MIT) and a CLI; v0.2 plans an optional LLM augmenter, v0.3 a live MCP connector.

Open questions

  1. Resource / Prompt analogues. Resources (data exfiltration risk) and Prompts (injection vector risk) have parallel concerns. This SEP scopes to Tools; a follow-up SEP could extend the same vocabulary if the community thinks it's useful.
  2. Versioned manifests. Should ToolAnnotations carry a schema version so consumers know which vocabulary set applies? Deferred — the protocol version already covers this implicitly.
  3. Trust scale normalisation. minTrustLevel: 1..5 is opinionated. If consumers diverge significantly, a free-form string with an accompanying convention doc might serve better. Soliciting feedback.

Security considerations

  • A malicious server can declare low risk for a high-risk tool. Clients SHOULD NOT trust server-declared metadata blindly when the server isn't authenticated/curated. The reference inferrer + community-curated catalogues (separate work) provide the verification layer.
  • The most-restrictive default for missing metadata is the safe baseline; this proposal doesn't change that.

SEP-2809 Attested Tool-Server Admission (ATSA)

Final · Standards Track · Created 2026-05-22

Imported from upstream. Proposed as modelcontextprotocol#2809 by @metereconsulting on 2026-05-28, 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 — accepted. The document carries every required section, and a prototype a reviewer can run is cited at https://github.com/enclawed/enclawed-oss. Conformance tests and the reference documentation chapter are still required before Final.

The key words MUST, MUST NOT, REQUIRED, SHALL, SHOULD, SHOULD NOT, MAY, and OPTIONAL are to be interpreted as in RFC 2119 and RFC 8174.

Abstract

MCP standardizes how a host and a tool server exchange messages, but not trust: a host reads a server's self-declared tool list and dispatches calls with no notion of which servers it may use, at what sensitivity, or which of a server's tools are in bounds. This SEP adds an optional, purely additive admission layer: a server publishes a small, offline-signed clearance assertion at a well-known URI; a host verifies it against a locally pinned trust root before any tool dispatch; admitting a server is kept distinct from authorizing its tools via a closed per-server tool allow-list; and every admission decision is auditable. No existing MCP message changes shape, and an unextended host ignores the mechanism and behaves exactly as today.

Motivation

A host today takes a server's identity (beyond its TLS certificate) and its advertised tool list on faith. A prompt-injected model can therefore drive a destructive tool on any server it connects to. This is exploitable in every deployment; for a single operator it is a risk they absorb, but for a regulated operator it is disqualifying — and, lacking any admission record, unauditable. NIST SP 800-53 expects least-privilege access enforcement (AC-3/AC-6), information-flow enforcement between sensitivity levels (AC-4), and an audit record of access decisions (AU-2/AU-9); none can be satisfied if the host has no notion of which server is which, at what level, or which tools are in bounds.

The existing specification provides no place to express any of this. TLS proves the host reached the named endpoint; MCP's authorization profile proves the user may use the server; neither answers the missing third question — is this server one the host is authorized to use as a tool provider, and at what sensitivity?

The critical failure modes of this trust gap are empirically demonstrable. Adversarial testing against unprotected production multi-server MCP deployments reveals three exploitable runtime behaviors that bypass standard post-invocation defenses:

  1. Tool-Name Shadowing (/tool-name-shadow): An untrusted or injected server registers a tool name that collides with a co-loaded, legitimate tool surface, causing the host to route execution calls silently to the malicious server.
  2. Dynamic Schema Drift (/tool-schema-drift): A malicious server advertises an entirely benign schema during the initial tools/list handshake to pass static filters, but mutates parameter constraints or semantic requirements before the subsequent tools/call invocation.
  3. Registry Poisoning (/tool-registry-poison): Direct manipulation of the host's tool cache via downstream prompt injection or unvetted secondary discovery layers.

Baseline evaluations show that relying on the model layer to detect these exploits creates a brittle, non-deterministic boundary. Under standard model profiles, permissive personas completely fail to identify active schema drift, while catching tool-name shadowing imposes a severe reasoning load that degrades rapidly under context-window exhaustion. ATSA moves the security boundary from probabilistic, post-invocation reasoning to deterministic, cryptographic admission-time gating—preventing the prompt-injected model from ever accessing or driving an unvetted tool surface.

Specification

Discovery

A server MAY advertise an attestation two interoperable ways:

  1. Well-known resource. The server SHOULD serve a Server Attestation Document (SAD) at /.well-known/mcp-attestation on the same origin as its MCP endpoint, over TLS.
  2. In-band. A server MAY return the SAD in the initialize result under an experimental capability key.

A conforming host MUST attempt (1), MAY attempt (2), and MUST treat the absence of both as unattested, applying its configured posture (see Backward Compatibility).

Server Attestation Document (SAD)

A JSON object. REQUIRED fields: v (integer 1); id (stable server identity); publisher; version; clearance (a canonical name or alias of a level in a named, totally ordered classification scheme); capabilities (string array that MUST contain "mcp-server"); signerKeyId (identifies a key in the host trust root); signature (base64 detached signature over the canonical body). OPTIONAL, and part of the signed body when present: netAllowedHosts (string array — when non-empty, binds the SAD to those origins) and verification (string, e.g. "tested"). Verifiers MUST ignore unknown fields and MUST NOT include them in the canonical body until a future v registers them. (Formal JSON Schema: see the reference implementation.)

Canonicalization and signature

The canonical body is the deterministic JSON serialization of every registered field except signature, with object keys in sorted order, array members sorted, and an absent signerKeyId serialized as null. Signer and verifier MUST compute the signature over exactly these bytes. The reference profile uses Ed25519; a profile MAY substitute another suite if signer and verifier agree.

Host verification rules

Before treating a connection as admitted, a host MUST evaluate the following in order and MUST deny on the first failure:

  1. the SAD parses and its capabilities contain "mcp-server";
  2. signerKeyId and signature are present;
  3. signerKeyId resolves to a key in the trust root;
  4. that key's validity window (notAfter, if set) includes now;
  5. the key is approved for the asserted clearance, compared against the canonical level name so that an alias cannot widen a key's scope;
  6. the signature verifies over the canonical body;
  7. clearance dominates the host's required level (numeric rank comparison);
  8. if netAllowedHosts is non-empty, the connected origin is a member.

A host in a deny-by-default (high-assurance) posture MUST reject an unattested or failing server; a permissive host SHOULD surface the failure and MUST NOT silently treat it as success.

Tool authorization

Admitting a server MUST NOT be read as authorizing all its tools. A host SHOULD keep a per-server allow-list and MUST deny a tools/call for any tool absent from it before any network dispatch, regardless of what tools/list advertises. Per-tool clearance MAY be added in a future v.

Audit

A conforming host MUST append a tamper-evident record for every admission decision (allow / deny / warn) and every tool-authorization denial, carrying the resolved signerKeyId/clearance or the denial reason. Tamper-evident means the record is committed to an append-only construction in which each record is cryptographically chained to its predecessor, such that removal, reordering, or mutation of any record is detectable by chain verification. An unrecorded admission decision is unauditable and therefore non-conforming.

Such records SHOULD conform to the admission-control extension profile of the Tamper-Evident Audit Record Contract (SEP-3004), whose canonical form, shared integrity chain, and C-REC-1…C-REC-7 conformance vectors give the tamper-evidence an interoperable, independently verifiable shape across implementations. SEP-3004 §2.2 names this extension and defers its registration to this SEP; the profile is defined immediately below. Conformance to SEP-3004 is a SHOULD, not a MUST: a host MAY satisfy the requirement above with any tamper-evident construction (for example, its own hash-chained audit log) and adopt the SEP-3004 profile to gain cross-implementation audit interoperability.

Audit record profile: admission-control

This defines the admission-control extension named by SEP-3004 §2.2, which cross-references this SEP for the definition. A host that conforms its admission records to SEP-3004 (the SHOULD above) carries the admission decision under this type id. Every value defined here is a JSON string, keeping the extension body within SEP-3004's string-only canonical form (§2.3); enumerated fields are closed string vocabularies. Keys are serialized in the every-level lexicographic sort SEP-3004 §2.3 mandates.

Type id: admission-control

Required fields

Field Type Description
server_id string Stable identifier of the tool server whose admission was evaluated (its discovery identity, §Discovery).
clearance_decision string (enum) The gate verdict: admitted (the clearance assertion verified and all verification rules passed), denied (a verification rule failed, the server is outside the trust root, or the requested tool is outside the allow-list), or deferred (admission requires an out-of-band decision, e.g. a first-use prompt).

Optional fields

Field Type Description
clearance_level string The resolved clearance sensitivity classification from the Server Attestation Document.
trust_root_id string Identifier of the trust root against which the assertion was verified (§Trust establishment).
assertion_uri string The well-known URI (RFC 8615) the clearance assertion was retrieved from (§Discovery).
assertion_serial string Version/serial of the clearance assertion, for correlating admission across rotations.
requested_tool string The specific tool the recorded event attempted to invoke.
tool_allowlist_scope string The tools admitted for this server, encoded as a single canonical string: the allow-listed tool names normalized and lexicographically sorted (per SEP-3004 §2.3 ordering), joined by a single U+0020 space. The empty string denotes deny-by-default. Tool identifiers contain no whitespace.

event_type vocabulary (§2.9)

event_type Emitted when
tool_call A tool invocation is evaluated against the admission gate.
server_discovery A server's clearance assertion is evaluated for the first time.
admission_recheck An already-admitted server is re-verified (assertion rotation or trust-root update).

Outcome binding (SEP-3004 §2.1.1)

The clearance_decision is decision evidence that informs the record's base outcome; the base outcome states the enforcement result. The binding is:

  • clearance_decision: admitted → outcome: allowed
  • clearance_decision: denied → outcome: denied
  • clearance_decision: deferred → outcome: deferred

A record whose base outcome is allowed MUST NOT carry clearance_decision: denied, and vice versa.

Trust establishment

A trust root binds each key to the set of clearance levels it may sign for. That set is an explicit list of canonical level names, not a ceiling: a ceiling and a list differ for non-contiguous approvals, and the explicit list is the unambiguous reading of rule 5.

How the trust root is provisioned, pinned, and sealed is deployment policy and out of scope of this wire format. Profiles MAY anchor it in a pinned key set, X.509 chains, SPIFFE SVIDs, or a sigstore-style transparency log; the verification rules are independent of the choice.

Error signalling

When a host rejects a server for an attestation reason and surfaces it in-band, it SHOULD carry a machine-readable data.reason, one per verification rule: unsupported_version (0), not_mcp_server (1), unsigned (2), signer_not_trusted (3), signer_expired (4), signer_not_approved (5), bad_signature (6), below_required (7), host_not_bound (8), and tool_not_admitted (9, tool authorization).

unsupported_version (0) reports the rejection required by Compatibility, versioning, negotiation: it precedes the numbered rules, because a document whose version a verifier does not understand cannot be evaluated against them.

Compatibility, versioning, negotiation

v versions the document as "MAJOR.MINOR". A verifier MUST reject a document whose MAJOR it does not understand, and MUST accept a higher MINOR within a MAJOR it understands: minor revisions are additive only, so fields introduced by one are ignorable by an older verifier under the same rule that governs unknown fields. A document is expressed as a string because JSON does not distinguish 1.0 from 1. The extension is purely additive: an unextended host never fetches the SAD, an unextended server never sees the host-side allow-list, and a host MUST interoperate with unattested servers under its posture — enabling incremental rollout.

Rationale

Why a spec extension rather than a per-vendor layer

The mechanism can be implemented above MCP today — but additive is not interoperable. Bolted on by one vendor, attestation secures only that vendor's island; a server's signed clearance document is meaningful only to hosts that already agreed, out of band, on its shape and verification order. The payoff ("this server is attested" as a claim any host can check; servers publishing one document instead of N vendor dialects; SDKs shipping the gate on by default) only materializes once a single format is agreed, and only the spec owner can mint that Schelling point. That the mechanism rides entirely above MCP is what makes adoption cheap and low-risk, not a reason to defer it.

Design decisions

  • Offline, detached signature. Admission requires no online call to a third party; the host verifies against a locally pinned trust root. This keeps admission available and fast and avoids a runtime dependency on the signer's infrastructure.
  • Well-known URI for discovery. A host can evaluate admission before committing to a session, and the mechanism rides the established RFC 8615 pattern instead of inventing a new one.
  • Admission separated from tool authorization. Admitting a server says nothing about which of its tools are in bounds. The closed per-server allow-list is enforced before any network dispatch, defeating a prompt-injected model that requests a tool the server advertises but the host never approved.
  • Numeric clearance ranks in a named, totally ordered scheme. Dominance is a single comparison; naming the scheme lets different sensitivity vocabularies (government, healthcare, finance) coexist and interoperate.
  • Canonicalization fixes the signed bytes. Sorted keys, sorted arrays, absent signerKeyId as null, signature excluded — so signer and verifier agree on exactly what was signed regardless of JSON serializer.
  • Admission separated from tool authorization. Admitting a server says nothing about which of its tools are in bounds. The closed per-server allow-list is enforced before any network dispatch, defeating a prompt-injected model that requests a tool the server advertises but the host never approved. Empirical validation: Independent defender-side benchmarks confirm that runtime reasoning fails to contain dynamic exploits like schema drift under non-stringent model personas. Enforcing a strict host-side allow-list at the admission layer isolates the model from unvetted execution vectors entirely.

Alternatives considered

  • Leave it to each vendor / keep it an extension only. Rejected: additive ≠ interoperable (above). Extensions are the right place to experiment; this pattern has finished experimenting (production + evaluation), and its remaining value is gated on a single agreed format that only the spec can supply.
  • In-band capability only, no well-known document. Rejected: discovery must be possible before a connection is trusted, but initialize already implies a session. The well-known fetch lets a host decide admission before it commits.
  • mTLS / client certificates. Rejected: TLS answers "did I reach the named endpoint," not "is this server cleared to operate at this sensitivity, and which of its tools are in bounds." Orthogonal; ATSA composes with it.
  • OAuth-style bearer token. Rejected: that is user-authorization ("may this user use this server"), a different question from host admission ("may this host use this server as a tool provider, at what level"). ATSA composes with MCP's authorization profile.
  • Trusting clearance embedded in tools/list. Rejected: the tool list is unsigned and self-declared; trust cannot rest on it. The signed SAD is the anchor; the allow-list is host-side state.
  • Online (OCSP-style) revocation as a v1 requirement. Deferred: it adds an availability dependency at admission time. v1 enforces per-signer notAfter; online revocation is future work (see Open questions).

Composition with Runtime Drift Monitoring

The Admission/Runtime Boundary

ATSA establishes trust strictly at the point of admission: it verifies an unforgeable server identity and evaluates a pinned signer key before any network dispatch. This is a point-in-time guarantee that structurally answers whether a host is authorized to communicate with a specific server at a designated sensitivity level at the moment of connection.

By design, the wire format does not address whether a tool's internal behavior or exposed schema schema-drift occurs over the lifetime of an active session. An admitted server may pass verification successfully but subsequently mutate its tool surface: a tool declared as read-only during an initial handshake could later advertise mutating side-effects, inject new sensitive data parameters (e.g., PII), escalate network externalities, or trigger dynamic schema modifications between polling intervals. Because the underlying server identity remains unchanged, the cryptographic admission guarantee holds, but the capability surface has drifted.

This operational boundary is where a downstream runtime drift-monitoring layer composes with ATSA.

Expectations of the Downstream Drift Layer

A downstream continuous-monitoring framework requires a stable, cryptographically verified anchor to bind its behavioral baselines to. ATSA provides this structural dependency via two primitives:

  1. Verified Server Identity (id): The unforgeable identity string that the runtime monitor utilizes as the primary indexing key for its tool schema baseline. Without this cryptographic constraint, a drift monitor can be spoofed into baselining an adversarial substitute server.
  2. Pinned Signer Key (signerKeyId): The cryptographic guarantee that the entity producing tool definitions at execution time matches the entity that was admitted. This ensures an attacker cannot bypass drift telemetry by impersonating an admitted origin.

The downstream monitor anchors each tool baseline to the unique tuple (id, toolName) at the first observation post-admission, checking subsequent tools/list or tools/call envelopes against that baseline.

Explicit Non-Requirements of the Wire Format

To prevent protocol scope creep and maintain structural minimalism, the following boundaries are enforced:

  • State Isolation: The wire format MUST NOT carry drift state, baseline histories, or schema versioning metadata. Baseline persistence and delta-evaluations are strictly implementationconcerns of the runtime monitor.
  • Telemetry Isolation: The wire format is not responsible for detecting or signaling structural mutations. It provides the authenticated identity context; detecting change is the monitor's responsibility.
  • Frequency Decoupling: ATSA is NOT REQUIRED to execute full cryptographic re-attestation on every individual message frame. Admission remains an edge gate; continuous runtime verification lives in the downstream monitoring layer.

Systemic Synergy

Admission control without drift monitoring risks trusting a modified capability surface indefinitely. Conversely, drift monitoring without a verified admission anchor operates in the dark—its baselines remain completely vulnerable to origin spoofing. Composed, they provide a complete security posture: ATSA guarantees who, while the drift layer guarantees that the who hasn't silently changed what.

  • Artifact attestation. sigstore, The Update Framework (TUF), and in-toto apply "verify a signed claim against a trusted root before use" to software supply chains. ATSA applies the same discipline to MCP server admission.
  • Well-known discovery precedent. OAuth 2.0 Authorization Server Metadata (RFC 8414), security.txt (RFC 9116), and OpenID Connect Discovery all ride RFC 8615 without forking the base protocols they extend.

Design-principles alignment

This proposal is written against the MCP design principles:

  • Demonstration over deliberation. The mechanism is in production in the open enclawed-oss distribution, with a JSON Schema, conformance vectors, 48 hermetic tests, an LLM-driven adversarial campaign (27,025 tool-name evasions + 14,378 forged assertions, all denied), and a live Google Workspace end-to-end run. The SEP is written from what the prototype taught, not from theory.
  • Standardization over innovation. ATSA codifies a pattern already proven in a shipping host/server pair; it invents no new primitive. The signed-document-at-a-well-known-URI shape is the one OAuth metadata, security.txt, and OpenID discovery already use.
  • Convergence over choice. One signed-attestation format, one verification ordering — so "this server is attested" becomes a claim any host can check and a server publishes once, instead of N mutually unintelligible vendor dialects. Only the spec owner can mint that single path.
  • Interoperability over optimization. It degrades gracefully: capability negotiation gates in-band discovery, an unextended host never fetches the SAD, an unextended server never sees the allow-list, and a conforming host MUST still interoperate with unattested servers under its configured posture. Nothing requires every participant to be equally capable.
  • Composability over specificity (objection addressed). A reviewer may ask whether this can be built from existing primitives. It can — and is, today, above MCP — which is exactly why adoption is cheap. But composability gives each vendor an island, not interoperability: a server cannot publish one document every host understands until the shape and verification order are agreed in one place. The SEP adds exactly one document and one ordering — the minimum that turns a per-vendor capability into an ecosystem one.
  • Stability over velocity (objection addressed). Because the addition is permanent, it is deliberately small: one well-known document, one optional initialize capability, zero changes to any existing message. An unextended implementation is byte-for-byte unaffected, so the permanent cost to client implementers is bounded to those who opt in.
  • Capability over compensation. ATSA does not compensate for a temporary model weakness. A more capable model that is prompt-injected is more dangerous, not less; the trust and authorization gap it closes is independent of model quality and does not fade as models improve.
  • Pragmatism over purity. Trust-root provisioning is left to deployment policy rather than mandated, because operators' PKI realities differ. The wire format is fixed; the anchoring is not.

Backward Compatibility

None broken. The SAD lives at a well-known URI (RFC 8615); the tool allow-list is host-side state; no existing MCP message changes shape. This is the additive discipline that let OAuth, security.txt, and OpenID discovery ride the well-known mechanism without forking the protocols they extended. A host with no required level and an empty allow-list policy behaves exactly as a host does today.

Security Implications

The design resists confused-deputy tool invocation (tool authorization), server impersonation and cross-origin replay (rules 3/6/8), level escalation (the level is inside the signed body), and signer-key aging (rule 4). Out of scope and delegated to other layers: content inspection of admitted connections, and the behavior of a correctly-admitted, correctly-cleared server within its allowed tools. The trust root is the root of all guarantees; its provisioning and sealing are deployment policy (see Trust establishment), and a compromised signer key is bounded by notAfter until online revocation (future work) lands.

Reference Implementation

A production reference implementation ships in the open enclawed-oss distribution — github.com/enclawed/enclawed-oss — at extensions/mcp-attested (with a first-party Google Workspace bridge at extensions/mcp-google-workspace). It includes a JSON Schema, an error registry, and machine-checkable conformance vectors. This SEP was reverse-derived from that implementation; the mechanism existed and ran in production before the wire format was written down. Every admission decision (allow / deny / warn) and every tool-authorization denial is appended to a hash-chained, append-only audit log with chain verification, satisfying the §Audit MUST with an ATSA-native record shape (SEP-3004 profile conformance is the remaining SHOULD).

Prototype and evaluation

Per the SEP process (explore → prototype → write the SEP from what the prototype taught): the mechanism is implemented and in production, and is evaluated in the companion preprint. Every stated guarantee is backed by an executable test driven by an LLM-generated adversarial corpus:

  • 48 hermetic tests (44 under vitest, 4 under node:test) exercise every verification rule and the tool-authorization rule.
  • A local-LLM (Ollama) coverage campaign generated 27,025 unique tool-name evasions (case/Unicode/whitespace/separator/path/near-miss tricks) — all denied, zero leaked network writes — and 14,378 unique forged clearance assertions — all rejected. This attacker-side validation is complemented by independent defender-side baselines (e.g., the AlgoVoi Agent-Trust-Bench differential profiles across 29-tool surfaces), confirming that unprotected deployments remain uniformly vulnerable to runtime routing and session exploits.
  • A live end-to-end run drove a real Google Workspace MCP endpoint through the gate: the allow-listed tool was admitted and dispatched; out-of-allow-list tools were denied before any network call.

Conformance

A reference implementation, a JSON Schema for the document, and conformance vectors ship in this repository at extensions/atsa.

Seventeen vectors cover every host verification rule and every reason in the error registry, committed as data together with the trust root they verify against, so an implementation in any language can be checked against the same cases. The signing keys derive from fixed seeds, so the vectors are byte-for-byte reproducible. They run with npm test, without network access, a browser, or any key material supplied by the reader.

The reference chapter is published at Attested Tool-Server Admission.

Implementing the specification surfaced two under-specified points, both now resolved in the text above: rejecting an unsupported document version had no registered reason code, and the expression of key approval was unstated.

Open questions / future work

  • Per-tool (vs. per-server) clearance.
  • A short-lived notBefore/expiry on the assertion itself and an online (OCSP-style) revocation channel for compromised signer keys, beyond the per-signer notAfter already enforced.
  • Reconciling the clearance field with MCP's evolving authorization profile and capability-negotiation handshake.

Acknowledgements

Special thanks to:

  • Maaz (@maaz-interlock) from Interlock for his critical contributions to the architectural framing of the runtime boundary and for drafting the normative composition language for downstream continuous drift-monitoring frameworks.
  • Christopher Hopley and the AlgoVoi team for deploying their production adversarial testing suites (agent-trust-bench.algovoi.co.uk) to empirically validate the threat taxonomy closed by ATSA, providing critical defender-side baseline data for the protocol's motivation section.

References

  • Companion preprint: Attested Tool-Server Admission: A Security Extension to the Model Context Protocol — arXiv:2605.24248; archived record: doi:10.5281/zenodo.20349263.
  • RFC 2119 / RFC 8174 (requirement keywords), RFC 8414 (OAuth AS Metadata), RFC 8615 (Well-Known URIs), RFC 8032 (Ed25519), RFC 9116 (security.txt).
  • Tamper-Evident Audit Record Contract: SEP-3004 (in progress; PR #3004) — the record contract the admission-control profile above conforms to.
  • Reference implementation: github.com/enclawed/enclawed-oss — extensions/mcp-attested.

SEP-2817 AI Invocation Audit Context in Request `_meta`

Draft · Standards Track · Created 2026-05-30

Imported from upstream. Proposed as modelcontextprotocol#2817 by @hangum on 2026-05-29, 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 optional convention for carrying AI-generated invocation audit context in MCP request _meta.

When an AI host or client initiates an MCP request, the server often needs operational audit context that is not part of the tool-specific arguments:

  • why the AI/client requested this operation,
  • which model produced the invocation,
  • what user intent caused the work, when safe to provide,
  • and which requests belong to the same user turn.

Today, implementations that need this context must invent server-specific arguments such as user_query, invocation_reason, or transport-specific model metadata. That pollutes tool schemas, does not generalize across transports, and fragments audit logs across MCP servers.

This SEP defines one optional request _meta entry:

  • _meta["io.modelcontextprotocol/aiInvocation"]

The value is an object that may contain invocationReason, model, userIntent, and turnId.

All four fields are optional and client-asserted. Servers may ignore, redact, transform, or store them according to local policy, and MAY echo only turnId back in response _meta for correlation. These fields are not authorization evidence.

Motivation

AI-initiated MCP requests are different from ordinary application requests because the immediate caller may be an AI host acting on behalf of a user. For audit, incident review, and security monitoring, operators often need to answer:

  • What user request caused this MCP request?
  • Why did the AI choose this operation?
  • Which model produced the invocation?
  • Which other MCP requests were part of the same user turn?

In practice, server implementations have already started solving this locally. TadpoleDBHub, for example, records a server-specific audit context using custom request arguments and transport metadata:

  • user_query: original user request, shared across a user turn
  • invocation_reason: per-request rationale generated by the AI/client
  • model attribution from client/transport metadata
  • request/session correlation IDs to join AI audit, MCP service audit, SQL execution audit, and events

This works as an implementation-specific convention, but it has protocol-level drawbacks:

  • It adds audit-only fields to tool-specific arguments.
  • Each server invents different names and semantics.
  • Transport-specific metadata does not generalize cleanly to all transports.
  • Cross-server audit tooling cannot rely on a common location.

A standard _meta convention would let clients provide this audit context consistently without changing each tool schema.

Specification

Clients MAY include the following optional object in request params._meta for AI-initiated MCP requests:

_meta["io.modelcontextprotocol/aiInvocation"]?: {
  invocationReason?: {
    text: string;
  };
  model?: {
    name: string;
  };
  userIntent?: {
    text?: string;
    redacted?: boolean;
  };
  turnId?: string;
}

Example:

{
  "method": "tools/call",
  "params": {
    "name": "execute_query",
    "arguments": {
      "database_id": "123",
      "query": "SELECT * FROM employees LIMIT 10"
    },
    "_meta": {
      "io.modelcontextprotocol/aiInvocation": {
        "invocationReason": {
          "text": "The user asked to view employee rows, and the employees table was identified as the target table."
        },
        "model": {
          "name": "example-model"
        },
        "userIntent": {
          "text": "Show me 10 employees",
          "redacted": false
        },
        "turnId": "opaque-client-generated-id"
      }
    }
  }
}

Intermediaries that forward MCP requests SHOULD preserve _meta["io.modelcontextprotocol/aiInvocation"] unmodified unless they have a specific policy reason to remove or redact it.

Key naming and namespace

This SEP places all fields under a single _meta key in the MCP-reserved namespace:

io.modelcontextprotocol/aiInvocation

Under the current _meta key-name rules, any prefix whose second label is modelcontextprotocol or mcp is reserved for MCP use (for example io.modelcontextprotocol/, dev.mcp/); see PR #2758, which aligned the reserved-prefix rule in the schema with the specification docs. First-party MCP keys already use this form (e.g. io.modelcontextprotocol/clientInfo, io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/tasks).

Because this prefix is reserved for MCP, the key proposed here is not something a third party may self-assign. This SEP requests that, if accepted, MCP define io.modelcontextprotocol/aiInvocation as a protocol-level key. Until this SEP is accepted, conformant implementations that want to experiment SHOULD use a vendor-owned reverse-DNS prefix (for example com.example/aiInvocation).

A single namespaced envelope is used deliberately instead of bare keys such as _meta.invocationReason or _meta.turnId. Bare keys are curated by MCP and risk future collision; grouping the fields under one reserved key keeps them together and provides a single, clear extension point.

Historical context: PR #775 expanded _meta name reservation and the issue SEP-1788 ("Clarify _meta Property Reserved Keys") tracks reserved-key behavior. Note that the earlier reservation grammar (pre-#2758) would not have reserved io.modelcontextprotocol/; implementers should follow the current second-label rule.

invocationReason

invocationReason describes why the AI/client requested this specific MCP operation.

Semantics:

  • invocationReason as a whole is optional; if present, text is REQUIRED — an empty invocationReason object carries no information.
  • text is a concise human-readable explanation, ideally one sentence.
  • invocationReason is scoped to the emitted MCP request. If a client retries a logical operation, each emitted retry request SHOULD carry its own request-level invocationReason.
  • invocationReason is client/AI-asserted and may be inaccurate, incomplete, or hallucinated.
  • invocationReason MUST NOT be used as authorization evidence.

This SEP intentionally does not standardize invocation reason categories, retry attempt ordering, policy outcomes, approval lifecycle states, or server-side decision records.

model

model identifies the model that produced the invocation, when known.

Semantics:

  • The model object as a whole is optional; omit it entirely when the invoking model is unknown. If model is present, name is REQUIRED — an empty model object carries no information.
  • name is the model name as known by the client or host.
  • When available, name SHOULD use the provider's canonical model identifier rather than a display name.
  • In multi-model systems, model identifies the model that produced this MCP request, not necessarily the orchestrator, router, or gateway.
  • This field is client-asserted and may be incomplete or spoofed.
  • It should be used for audit attribution, debugging, and operations, not as a sole security control.

This SEP intentionally standardizes only model.name. Provider, gateway, proxy, or version attribution may be added by future SEPs or implementation-specific metadata if interoperability demand is demonstrated.

userIntent

userIntent describes the user-level intent that caused the work, when safe and appropriate to provide.

Semantics:

  • text MAY contain the user request or a short client-generated intent label.
  • Unlike invocationReason.text and model.name, userIntent.text is intentionally optional: a client MAY signal that user intent existed but was withheld for privacy by sending redacted: true without text.
  • redacted indicates that text was modified, shortened, or sanitized.
  • For privacy-sensitive deployments, clients MAY send a shortened or sanitized text with redacted: true, or omit userIntent entirely. Carrying a hash or reference to a private transcript instead of raw text is left to deployment policy or a future SEP (see Open Questions); this SEP does not define a hash/reference field or format.
  • userIntent is optional and may be omitted for privacy-sensitive deployments.
  • Empty or absent userIntent is expected for some unattended or scheduled invocations and MUST NOT be treated as malformed audit data.
  • userIntent may contain sensitive or personal data and should be handled under the server operator's privacy, retention, and access-control policy.
  • userIntent is client-asserted and MUST NOT be used as authorization evidence.

turnId

turnId correlates MCP requests that were caused by the same user turn.

A user turn means one user input or initiating event that may produce multiple MCP client-to-server requests.

Semantics:

  • turnId is an opaque client-generated string.
  • Clients and servers MUST NOT parse semantics from the value.
  • UUIDv4 or ULID are reasonable non-normative formats.
  • The same turnId MAY be reused across multiple MCP requests caused by the same user turn.
  • turnId is not an idempotency key.
  • turnId does not replace JSON-RPC id, which identifies an individual request/response message.
  • turnId does not replace progressToken, which tracks progress for a specific long-running request.
  • turnId does not imply ordering between requests and does not identify a stable tool-call attempt.
  • Servers MAY echo only turnId in response _meta["io.modelcontextprotocol/aiInvocation"] to help downstream correlation of asynchronous or streamed responses.
  • Servers SHOULD NOT echo userIntent, invocationReason, or model in responses unless explicitly required by local policy, to avoid re-propagating potentially sensitive text on the response path.
  • Future SEPs may define additional correlation fields; this SEP standardizes only turn-level grouping.

Example with multiple requests in one turn:

{
  "method": "tools/call",
  "params": {
    "name": "list_tables",
    "arguments": {
      "database_id": "123"
    },
    "_meta": {
      "io.modelcontextprotocol/aiInvocation": {
        "invocationReason": {
          "text": "Need to inspect available tables before generating a query."
        },
        "model": {
          "name": "example-model"
        },
        "userIntent": {
          "text": "Show me 10 employees"
        },
        "turnId": "2f4f0c9e-8d72-4c66-9c6f-3f7f3e6a1c9f"
      }
    }
  }
}
{
  "method": "tools/call",
  "params": {
    "name": "execute_query",
    "arguments": {
      "database_id": "123",
      "query": "SELECT * FROM employees LIMIT 10"
    },
    "_meta": {
      "io.modelcontextprotocol/aiInvocation": {
        "invocationReason": {
          "text": "The employees table was identified and the user requested sample employee rows."
        },
        "model": {
          "name": "example-model"
        },
        "userIntent": {
          "text": "Show me 10 employees"
        },
        "turnId": "2f4f0c9e-8d72-4c66-9c6f-3f7f3e6a1c9f"
      }
    }
  }
}

Specification and schema impact

If accepted, adoption requires declaring io.modelcontextprotocol/aiInvocation in the canonical schema (schema.ts, with schema.json regenerated). A decision is needed on whether the envelope is expressed as a typed object on request _meta (a named metadata type) or documented as a free-form reserved key; there is no separate key-registry file in the repository today.

The optional server-side response echo of turnId (see the turnId section) carries the same key in result _meta — _meta["io.modelcontextprotocol/aiInvocation"] with only turnId populated — and should be specified alongside the request-side key so that request and response uses of the key stay consistent.

Rationale

Why _meta?

MCP already uses _meta for protocol metadata that is not part of the application-level tool arguments. Audit context such as model attribution, invocation reason, user intent, and turn correlation is not tool-specific input. Placing it in _meta avoids polluting tool schemas.

Why one namespaced envelope?

A single reserved key avoids claiming multiple bare _meta keys and keeps related AI invocation audit fields together. It also gives future revisions one clear extension point without expanding the top-level _meta surface.

Why not tool arguments?

Tool arguments are part of each tool's semantic contract. Audit fields such as user_query or invocation_reason are useful locally but force every server to define its own convention. They also make audit metadata look like business input.

Why not JSON-RPC id?

JSON-RPC id identifies a single request/response exchange. turnId groups multiple MCP requests caused by the same user turn. The cardinality is different.

Why not progressToken?

progressToken is for progress reporting within a long-running request. turnId is for correlating multiple requests caused by one user turn.

Why not OpenTelemetry only?

OpenTelemetry is useful for backend service tracing, and SEP-414 documents trace context propagation in _meta. However, AI invocation audit has AI-specific concepts such as user intent, AI-generated invocation rationale, and model attribution. turnId is also a user-turn grouping key, not a distributed trace identifier. This SEP does not require or prohibit deployments from additionally carrying trace context through OTel keys or baggage.

All proposals referenced below are open at the time of writing unless noted as merged.

SEP-2787: Tool call attestation (open)

SEP-2787 proposes signed tool-call attestation for MCP, binding invocation context (such as intent and tool/argument information) into a verifiable record. This SEP is narrower and intentionally non-cryptographic: it defines optional client-asserted input-audit metadata and does not define signatures, argument hashes, execution proofs, or server-authoritative decisions. The two are complementary rather than competing — a deployment that needs cryptographic assurance could carry signed attestations (SEP-2787) alongside this lightweight, unsigned input-audit context, where an unsigned invocationReason is the lightweight counterpart of an attested rationale.

SEP-414: OpenTelemetry trace context (merged)

SEP-414 documents using _meta to carry W3C Trace Context (traceparent, tracestate, baggage). It deliberately uses bare (unprefixed) keys as a narrowly-justified exception, because those names mirror an external wire-format standard. This SEP has no such external wire-format to match, so it uses the MCP-reserved namespaced key rather than bare keys; SEP-414 is therefore a useful contrast that explains when bare keys are and are not appropriate. SEP-414 also addresses distributed-trace propagation, which is complementary to, and distinct from, AI-invocation audit semantics.

_meta key reservation: SEP-1788 (open), PR #775 (merged), PR #2758 (merged)

These define and refine how _meta keys are reserved. This SEP follows the current rule established by PR #2758 by requesting a key under the MCP-reserved io.modelcontextprotocol/ namespace.

SEP-2643: Structured Authorization Denials (open)

SEP-2643 defines one server-side decision/denial path — a structured record of an authorization denial. This SEP is the complementary client-asserted input half ("why the AI asked"); it does not define decision or denial records and is designed to compose with server-side records such as SEP-2643.

SEP-2061: Action Security Metadata for MCP Tools (open)

SEP-2061 covers tool side-effect classification and action security metadata, which this SEP lists as a Non-Goal; that work belongs there.

SEP-2448 and SEP-2028: telemetry (open)

SEP-2448 (server execution telemetry) and SEP-2028 (_meta-to-HTTP-header forwarding for distributed tracing) address operational and trace telemetry through _meta. They are complementary to, and distinct from, client-asserted AI-invocation audit semantics.

Backward Compatibility

This proposal is backward compatible.

All fields are optional. Servers that do not understand this _meta entry may ignore it. Clients that do not provide it remain valid.

Existing server-specific conventions, such as tool arguments named user_query or invocation_reason, may continue to work. Implementations may accept both existing custom fields and the standardized _meta form during migration.

Security Implications

This proposal introduces audit metadata, not authorization metadata.

Security considerations:

  • invocationReason may be inaccurate, incomplete, or hallucinated.
  • userIntent may contain sensitive or personal data.
  • model is client-asserted and may be spoofed.
  • turnId is correlation metadata and MUST NOT be used as an idempotency key or authorization token.
  • turnId enables cross-request linkability; operators should treat it under the same privacy and retention policy as other audit identifiers.
  • Free-text fields may be persisted into audit stores and displayed in administrative tools.

Servers MUST NOT use invocationReason, userIntent, or model as authorization evidence. Authorization should rely on deterministic server-side context such as authenticated principal, scopes, tool metadata, policy, environment, approval state, and server-side checks.

Audit consumers that summarize or export this metadata should preserve the distinction between client-asserted context and server- or intermediary-witnessed facts. Derived reports or counters must not promote aiInvocation fields into authorization, execution, or outcome evidence.

Servers SHOULD apply local policy for logging, redaction, retention, and access control before persisting these fields.

Servers SHOULD neutralize log-forging vectors (for example CR/LF and other control characters) before persisting these free-text fields, and audit viewers SHOULD apply contextual output-encoding when rendering them to prevent stored-XSS.

Implementations SHOULD enforce per-field and total byte limits for this audit context to avoid excessive log growth or denial-of-service via large metadata.

Non-Goals

This SEP does not define:

  • server-authoritative decision records (cf. SEP-2643, structured authorization denials),
  • stable toolCallId semantics,
  • agentRunId or sessionId,
  • retry or resumability semantics,
  • response-echo semantics richer than the optional turnId echo,
  • approval lifecycle state machines,
  • invocation reason taxonomy,
  • tool side-effect taxonomy (see SEP-2061),
  • execution proofs and tamper-evident audit bundles,
  • SIEM schemas,
  • authorization or policy decision mechanisms.

Those are valuable follow-up topics, but they should not block standardizing the minimum client-asserted input-audit context.

Reference Implementation

TadpoleDBHub has implemented a server-specific version of this audit context for DB/MCP execution audit.

Current implementation data point:

  • the original user request is captured as user_query,
  • per-call AI rationale is captured as invocation_reason,
  • model attribution is captured from client/transport metadata,
  • request/session IDs are used to join AI audit, MCP service audit, SQL execution audit, and events.

This validates the operational need, but also shows the drawback of server-specific argument injection. A protocol-level _meta convention would avoid polluting tool schemas and make cross-server audit tooling easier.

Additional implementation feedback from the discussion:

  • agent-guard-style systems distinguish input audit from server-side decision records,
  • multiple implementers independently identified a future need for stable tool-call identity and longer-lived agent/session correlation,
  • independent implementers offered to review the SEP against live or prototype audit implementations.

Open Questions

  1. Should this convention apply to all client-to-server requests, or only AI-selected operations such as tools/call?
  2. What length guidance should be recommended for invocationReason.text and userIntent.text?
  3. Should a future revision define a userIntent hash/reference field, and if so with a self-describing format (for example sha256:<hex>)?
  4. Should the optional turnId response echo remain MAY, or be strengthened to SHOULD for asynchronous/streamed transports?

Acknowledgments

This proposal grew out of MCP Discussion #2704. Thanks to the participants there for shaping the key framing — separating client-asserted input audit from server-authoritative decision records, the turnId user-turn correlation model, and the privacy guidance for userIntent.

AI Assistance Disclosure

This SEP draft was prepared with AI assistance for structure, wording, and review. The proposal direction, implementation experience, and final technical judgment were reviewed and edited by the author.

SEP-2848 Asynchronous Approval for Tool Calls

Draft · Extensions Track · Created 2026-06-02

Imported from upstream. Proposed as modelcontextprotocol#2848 by @mcguinness on 2026-06-03, 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 defines an extension that lets an MCP server gate a tool call on an out-of-band approval without keeping the original request open. When the server's authorization decision is a denial that is requestable (something can still approve it), the server returns a task handle (SEP-2663) in place of the tool result instead of failing the call. The task stays working while the deployment's resolver decides out of band; on approval the server re-checks policy, executes the tool, and makes the result available through tasks/get.

The approval protocol runs on the server's approval backend and never crosses the MCP wire: the client sees only a task ID it can poll and cancel, and handles no authorization artifact. Binding the backend's durable, resumable handle to a durable MCP task ID lets a poll resume after a restart. The backend is pluggable, with the OpenID AuthZEN Access Request and Approval Profile (ARAP) as a worked example (see Example binding). The feature is an experimental extension per SEP-2133, layered on tasks.

Motivation

A requestable denial is resolved by whatever the deployment designates, often not a human, and sometimes several resolvers in sequence (see Use cases). What is constant is that the decision happens out of band and asynchronously, and may outlive the request, the connection, and the process that made the call. MCP already has asynchronous execution (tasks, SEP-2663), connection-level authorization (OAuth 2.1), and externalized per-call policy; what is missing is the binding that says a call is denied now but approvable later by something else, and ties that pending decision to a durable task. The input paths tied to the original request do not survive a disconnect, so without this binding a server facing a requestable denial either fails the call and loses the workflow, or blocks until something times out. This SEP defines the binding and the narrow, MCP-observable behavior around it.

Use cases

The MCP-facing shape is identical across resolvers:

  • Human reviewer approves out of band, possibly the next day (for example wire_transfer above a threshold).
  • Supervising agent or orchestrator approves programmatically in seconds.
  • Automated policy or risk engine clears once a condition or grant provisions.
  • External ITSM or IGA system decides by its own, possibly automated, rules.
  • Four-eyes or break-glass: a second principal authorizes a sensitive operation.

For the no-human resolvers there is no session, so submission input is filled by the agent against the backend's schema (see Collecting submission input).

Specification

Relationship to existing specifications

This SEP composes existing pieces and adds the binding between them:

  • SEP-2663 (Tasks extension, io.modelcontextprotocol/tasks) supplies the durable, pollable primitive: server-directed CreateTaskResult (resultType: "task"), the methods tasks/get, tasks/update, tasks/cancel, notifications/tasks, and the statuses working, input_required, completed, failed, cancelled.
  • SEP-2322 (Multi Round-Trip Requests) supplies the request round-trip channel (inputRequests / inputResponses carried in requestState) for pre-task submission input; in-task input uses SEP-2663's tasks/update.
  • SEP-2133 (Extensions) supplies the extension and capability framing.
  • SEP-2643 (Structured Authorization Denials) supplies the portable denial envelope, io.modelcontextprotocol/authorization, which classifies the failure and states the remediation posture. A server that also implements SEP-2643 emits it as the denial classification while a call is denied; this extension's io.modelcontextprotocol/tool-approval-disposition carries the execution outcome. Envelope behavior below applies only to servers that implement SEP-2643.
  • An approval backend supplies the approval lifecycle: it accepts an approval request idempotently under a caller-supplied submission key, returns a durable resumable handle, resolves the request out of band into a terminal state (granted, denied, expired, cancelled, or failed), and on a grant returns approval material the server uses as an input to re-evaluation. The server, not the backend, performs that re-evaluation through its authorization decision so the decision stays authoritative. The backend bounds the approval by an expiry and MAY support cancellation. This SEP references that abstract contract, not any single backend. The OpenID AuthZEN ARAP is a worked example (see Example binding: AuthZEN ARAP).

The MCP server is the Policy Enforcement Point (PEP) and the only party that speaks the approval protocol; the client speaks ordinary MCP. The backend is pluggable, and a deployment binds it to whatever provides the contract above. Whether the generic execution and disposition semantics should live here or move into SEP-2643 or the tasks extension is open (see Limitations).

The composition order is:

  1. the server evaluates every tools/call against its authorization policy;
  2. a requestable denial enters the approval lifecycle: submit the request, bind the backend's handle to the MCP taskId, and return a working task;
  3. on resolution the server re-evaluates with the approval as an input;
  4. that decision is authoritative: an approval is an input to a new decision, not a standing grant.

Server-side flow (informative)

Non-normative. The only MCP messages are between Client and Server; everything to the right of the server is internal to the PEP and never crosses the MCP boundary.

Approval backendAuthorization decisionMCP Server (PEP)ClientApproval backendAuthorization decisionMCP Server (PEP)ClientOut-of-band resolution(human, agent, policy, or external system)tools/callauthorization checkdeny, requestablesubmit approval request (binding material)durable handle (bound to MCP taskId)CreateTaskResult (resultType: "task", status: working)resolved (callback or poll)re-evaluate with approvalallowexecute tool, task = completed (result available)tasks/getCompletedTask with result (CallToolResult)

The backend's constructs (the requestable-denial signal, binding material, the durable handle, and the approval input to re-evaluation) are defined by the backend and referenced, not redefined, by this SEP. For the AuthZEN ARAP mapping, see Example binding: AuthZEN ARAP.

Extension identifier and capability

The extension identifier is io.modelcontextprotocol/tool-approval (reversed-domain prefix per SEP-2133; illustrative, to be confirmed with the owning group). It builds on the tasks extension, so a server MUST also support io.modelcontextprotocol/tasks. Both are declared in the server's extensions capability:

{
  "capabilities": {
    "extensions": {
      "io.modelcontextprotocol/tasks": {},
      "io.modelcontextprotocol/tool-approval": {}
    }
  }
}

Task creation is server-directed (SEP-2663): the client only declares the tasks extension, and the server decides per request whether to return a CreateTaskResult. A client that has not declared tasks but issues a call the server can satisfy only through approval MUST receive either a -32003 (Missing Required Client Capability) error naming io.modelcontextprotocol/tasks, or a normal CallToolResult with isError: true carrying requestable guidance (a degraded path with no async resume). Since -32003 reads as a capability gap rather than "go obtain access," a server that wants non-tasks agents to act on a requestable denial SHOULD prefer the degraded CallToolResult with next-step guidance.

Declaring io.modelcontextprotocol/tool-approval is an optional client signal that it understands approval semantics (for example, to render approval-specific UI). Correct behavior does not require it, since an approval-gated task is an ordinary tasks-extension task.

Client maturity (non-normative). The canonical flow needs a client that drives io.modelcontextprotocol/tasks, which is experimental and not yet in shipping clients, so end-to-end value and interop testing depend on tasks-client adoption. In the interim a server MAY also expose approval as ordinary tools (for example request_access / get_request_status) carrying the same next-step fields (a hint, the request schema, a poll interval); this SEP does not standardize that shape.

Denied-but-requestable returns a task

When the server evaluates a tools/call and the decision is "deny":

  1. If the denial is not requestable, the server returns the normal denied tool result: a CallToolResult with isError: true describing the denial. (Per SEP-2663, an isError result delivered through a task is a completed task, not a failed one.)
  2. If the denial is requestable and the client declared the tasks extension, the server MUST NOT execute the tool. Instead it:
    • records a durable submission-intent with a fresh, opaque idempotency key before contacting the backend, submits the approval request under that key, and persists the returned handle (see Crash-consistent creation);
    • creates the durable MCP task bound to that handle and returns a CreateTaskResult (resultType: "task") with status: "working" and a human-readable statusMessage;
    • if it implements SEP-2643, includes the denial envelope in the result _meta under io.modelcontextprotocol/authorization (reason: "insufficient_authorization", remediation: "available", a remediationHints entry of type: "task"), and MUST repeat that envelope on every tasks/get result while the task is non-terminal, so a client resuming after a restart with only the taskId still reads the pending classification;
    • MAY include an io.modelcontextprotocol/model-immediate-response value in _meta so the model receives an immediate string.

The server MUST NOT surface the backend's binding material or endpoints to the client (for example, in the ARAP binding, binding_token, evaluation_id, the approval object, and access-request endpoints). These are internal to the PEP.

Crash-consistent creation. Submitting to the backend and creating the MCP task are two writes, and a crash between them must not orphan an approval or double-submit. Deduplication decides first, before any submission, whether a repeated tools/call reuses an existing non-terminal task (see At-most-once and deduplication). When the server does submit, whether an initial request or a chained request (see Completion), it MUST generate a fresh, opaque, high-entropy idempotency key for that logical submission, and MUST record a durable submission-intent holding that key plus the exact submission body and requester context needed to reproduce an equivalent request, before it contacts the backend. It then submits under that key. A conforming deployment MUST configure its backend to treat a repeat of the key with an equivalent body as the same request, return the same handle, and retain the key through reconciliation, so a retry after a crash recovers the original rather than creating a duplicate. The key identifies one submission, not the call, so distinct intents and successive chained requests never collide. The server MUST persist the returned handle before it returns CreateTaskResult, so the task is durable when returned (SEP-2663) and a callback that arrives first has a record to bind to. On restart the server MUST reconcile any submission-intent with no bound task, either by recovering the handle under its key or by cancelling the orphaned request.

At-most-once and deduplication. The server controls both task creation and execution, so it MUST execute the approved tool at most once per task. To keep a client retry (after a poll timeout or restart) from opening a second approval task, the server SHOULD return the existing task's CreateTaskResult when it recognizes a repeated requestable denial within one dedup scope: the same originating authorization context (the authenticated caller or subject the task is bound to), tool, and arguments. It matches against both non-terminal tasks and in-progress submission-intents, so a concurrent retry in the window between persisting the intent and creating the task does not open a second approval. This is best-effort: a client-local nonce in the arguments defeats it, identical arguments from two distinct intents collide, and MCP has no request-level idempotency key that would make it exact (see Limitations). So the at-most-once guarantee is per task, and a non-idempotent tool can still run more than once through distinct tasks.

Once a task reaches a terminal disposition it is final, permanently fenced against execution, and MUST NOT be reopened. A later identical tools/call is a new intent that the server evaluates fresh; it does not automatically create a new approval request and re-enters the approval lifecycle only if that fresh evaluation itself returns a requestable denial. A terminal denied-not-executed therefore carries remediation: "unavailable" in the full SEP-2643 sense: the client is not invited to retry it.

Retention. Retention has two phases. While the task is pending, createdAt plus ttlMs MUST cover the current pending deadline plus resultRetention, where the pending deadline is the backend request deadline before a grant and the approval validity during post-grant retry, and resultRetention is a server-configured duration for how long a terminal result stays retrievable; the server MUST NOT delete or expire a working task before its pending deadline. At terminalization the server MUST extend ttlMs if needed so it covers terminalizedAt plus resultRetention. A ttlMs of null means unlimited (SEP-2663) and satisfies both boundaries with no extension. Because the backend often sets or extends the window after submission, the creation-time ttlMs is provisional; when the server learns a later deadline it MUST extend ttlMs (reported on tasks/get and notifications/tasks). A server SHOULD bound the maximum window it accepts so pending tasks do not accumulate without limit.

Model-facing text is untrusted. statusMessage and any model-immediate-response are fed to the model and MUST NOT carry unsanitized text from untrusted sources; they are a prompt-injection surface. A working or input_required task MUST NOT be presented to the model as a completed action.

The task handle is not authority

The taskId is a reference to a pending decision, never a grant. Authority is the authorization decision, re-derived at execution against the captured call binding (see Completion). The server keeps three kinds of state per task, with different mutability:

  • Immutable call binding, captured on the first tools/call and never changed (carried in requestState per SEP-2322 across any pre-task synchronous input round-trip, then promoted unchanged into the task): the tool name; the complete tool arguments, stored in full (subject to the durable sensitive-state requirement in Security Implications), since execution runs from them long after the call and a digest cannot reconstruct them, and optionally a digest over a canonical serialization (for example JCS canonical JSON, RFC 8785, with a named hash such as SHA-256) alongside them for integrity checking; and the originating authorization context (the requester principal's identity, the subject or resource acted on, and any delegation), captured so a different caller is rejected. An approval is bound to this identity.
  • Mutable approval-workflow state: the current backend handle and request id, a monotonically increasing submission generation, and the current approval expiry and window. These change legitimately, for example when a re-evaluation returns request and the server opens a new request (see Completion), or when the backend extends the expiry. The server retains enough handle history to recognize a superseded generation.
  • Mutable execution state: the single execution claim and the terminal disposition, set once the task reaches execution (see Completion).

The server MUST check the immutable call binding before executing, and MAY record the policy id and version for audit and drift detection (recorded, not pinned). Execution runs from the captured binding, not from a fresh client call, so there is no "current call" to trust at execution time. An MRTR retry that resumes the original call, echoing it with requestState, MUST match the immutable call binding: the server MUST reject it if the tool, arguments, or originating caller differ. A tasks/update is different: it carries no tool or arguments, so the server authorizes the caller against the taskId and validates the outstanding input keys instead. An independent new tools/call is a new intent evaluated fresh; only a changed binding is guaranteed not to match dedup, while an identical independent intent is indistinguishable from a retry and MAY collide.

Collecting submission input

The approval request may require inputs before or during the approval: a justification, a ticket reference, or fields the backend defines (its submission schema and forms; for example, in the ARAP binding, request_schema_url, form_url, request_catalogs_url). The server collects these over the tasks input channel (SEP-2322), not a standalone elicitation/create, since SEP-2260 disallows unsolicited server-to-client requests and SEP-2663 routes task input through inputRequests / tasks/update:

  • Before submission (synchronous). The server returns resultType: "input_required" on the original tools/call. The client then retries the tools/call with requestState and inputResponses (per SEP-2322); the server validates that retry against the pre-task immutable call binding it captured on the first call and carried in requestState, submits the approval request under Crash-consistent creation, and returns the CreateTaskResult on the retry. A single JSON-RPC response cannot carry both input_required and the task. Use this when the backend will not accept the request without the inputs.
  • During the workflow (in-task). The server sets the task to input_required with outstanding inputRequests; the client answers with inputResponses via tasks/update, and the task returns to working.

Who answers is a property of the client: an interactive client renders the request for a human, an autonomous agent answers from context against the backend's request schema. An in-task round-trip requires a client that returns to the task; an agent that persisted its taskId can resume polling after a restart (SEP-2663) and answer via tasks/update, but a client that did not persist it or will not return cannot, so deployments expecting long gaps SHOULD collect all inputs synchronously before submission. The mapping from the backend's request schema onto the inputRequests schema is not pinned here and is currently implementation-specific.

Lifecycle and task status

The backend resolves the approval request into a terminal state (granted, denied, expired, cancelled, or failed); a grant triggers the server's re-evaluation, which yields allow, retry, request, or deny (see Completion). Both feed the mapping below. Each row maps to an MCP task status and a disposition; a SEP-2643-aware server also carries an envelope while the call is denied. The envelope classifies the denial (present only while denied); the disposition records what happened to execution. They are independent axes.

Backend state Task status SEP-2643 envelope Disposition
Awaiting a decision working available, task hint none yet
Needs submission input (in-task) input_required available none yet
Re-evaluation returns retry working (wait, then re-evaluate) available none yet
Re-evaluation returns request working (new request) available none yet
Allowed, executed once completed none approved-executed
Backend denied, or re-evaluation deny/none completed, isError unavailable denied-not-executed
Approval or request expired unresolved completed, isError unavailable denied-not-executed
Executed, tool errored completed, isError none execution-error
Execution claimed, outcome undeterminable completed, isError none outcome-unknown
JSON-RPC error, or a backend/infra failure surfaced as one (retryable) failed (carries the SEP-2663 error object) none —
Cancelled by client (tasks/cancel) or backend cancelled none —

While working, the server tracks the approval by polling the backend's status source or receiving its callback. The taskId and every backend handle generation are durably bound to the task, and only the current generation is actionable, so a server that restarts rehydrates the task from the persisted handles and keeps serving tasks/get. Per the table, failed carries the SEP-2663 error object and covers a JSON-RPC protocol error or a backend/infra failure surfaced as a JSON-RPC internal error; unlike a denial such a failed is retryable, and it is not an authorization outcome. A task MUST enter failed only before an execution claim (see Completion); any failure after the claim MUST be a completed CallToolResult with execution-error or outcome-unknown, so a possible side effect is never hidden behind a retryable failed.

Three clocks apply and are distinct: the backend's request deadline (how long the request stays open for a decision), the approval's validity once granted, and MCP retention (createdAt plus ttlMs, always measured from createdAt even when the server extends it per Retention). When the request deadline or approval validity passes without a usable approval, the task completes as a terminal denial (denied-not-executed, with the body noting expiry) and the server fences it against later execution. MCP retention is separate: once ttlMs elapses the server MAY delete a task that has not been execution-claimed (SEP-2663). An execution-claimed task MUST NOT be deleted before it terminalizes: at the claim the server extends retention through an execution deadline, and when that deadline arrives it terminalizes the task as outcome-unknown (if no stronger result is available), retains that result for resultRetention, and only then MAY delete it. A terminal task MUST NOT change to failed, which would erase its disposition.

Execution disposition

io.modelcontextprotocol/tool-approval-disposition is this extension's machine-readable outcome. It is a string and MUST be present in the _meta of the CallToolResult carried in a completed task's result field, on every completed approval task, independent of SEP-2643. Its value is one of:

  • approved-executed: the tool was authorized and executed once.
  • denied-not-executed: the tool did not run (denied, expired, or unresolved), so no side effect occurred.
  • execution-error: the tool was authorized and ran but failed; a side effect may have occurred.
  • outcome-unknown: execution was claimed but the server cannot determine whether invocation or the side effect completed.

The outer tasks/get result carries the Task, whose status conveys completed/failed/cancelled; the disposition refines a completed task's execution outcome and is not set on failed or cancelled tasks. Future values MAY be registered. A client that does not recognize a disposition value MUST treat the operation as possibly executed and MUST NOT retry it automatically.

Completion: re-evaluate, then execute

On approval the server MUST re-evaluate authorization with the approval as an input, so the decision stays authoritative at enforcement time. An approval is an input to a new decision, not a standing grant, and this re-evaluation is server-internal. It runs against the immutable call binding and current policy; a recorded policy version is for audit, not pinning, so a policy change yields a fresh decision. The re-evaluation resolves to one of four outcomes:

  • allow: proceed to execution.
  • retry (the approval is valid but a backing grant has not provisioned, or a transient hold): the task stays working. The server waits the backend's retry-after hint, or bounded exponential backoff with jitter if none is given, then re-evaluates, and MUST stop once the approval expires. A retryable outcome MUST NOT be completed as a denial.
  • request (this approval does not suffice, but a fresh approval request might): the task stays working. The server first atomically marks the current generation consumed and records a new submission-intent with its own fresh submission key (the immutable call binding is unchanged), then contacts the backend under Crash-consistent creation and attaches the returned handle to the new generation. Ordering it this way closes the window where a stale callback could still act: a callback or poll result for a consumed generation MUST NOT cause re-evaluation or execution, even when it is authentic, since the decision has moved on. A server MUST bound the number of chained requests per task so this cannot loop indefinitely; on reaching the bound it completes the task as a terminal denial.
  • deny or none: the task completes as a terminal denial.

Because execution can occur long after the request, an approval is not a substitute for caller authentication. The intent, approval scope, and the originating caller's identity come from the immutable binding; every other authorization input MUST be current at execution: whether that caller is still a valid principal, and the subject and resource state, risk and environmental attributes, the downstream credential, and policy. Re-evaluating against captured risk or environment would defeat the risk-clearing case, so those MUST be re-read, not replayed. The server MUST validate the immutable call binding, and if the tool implementation, its schema, or the authorization mapping changed materially while the task was pending, it MUST evaluate against that change or fail closed. If the principal is no longer valid, a required credential has expired, or the action is materially stale, the server MUST NOT execute (it completes as a terminal denial, or keeps the task working if the condition is transient).

Execution MUST be fenced so a crash cannot double-execute a side effect. A task that proceeds to execution MUST make a single atomic compare-and-set from pre-execution to execution-claimed; this happens at most once and is mutually exclusive with cancellation and expiry (a task that is denied, cancelled, or expired makes no claim). The claim is the irrevocable authorization-enforcement point: all validity checks, the identity and freshness checks above and the approval expiry, MUST hold at the moment of the claim, and a cancellation or expiry that precedes the claim wins (the exact cancellation boundary, see Cancellation). The claim does not terminalize the task: its observable status stays working until the final result is stored. Once the claim succeeds the server invokes the tool at most once and MUST carry it through even if the approval expiry elapses during invocation, since authorization was enforced at the claim. If it cannot determine whether invocation or the side effect completed (a crash after the claim, before or after invoking), it MUST NOT re-invoke, and the task completes outcome-unknown.

Every completed approval task MUST carry io.modelcontextprotocol/tool-approval-disposition (see Execution disposition). The denied-versus-executed distinction MUST also be stated in the CallToolResult body, not only in _meta: the content MUST state whether the tool ran, did not run, or may have run with the outcome unknown, and it MAY also appear in structuredContent where the tool's outputSchema permits, so the model behind a client that understands only io.modelcontextprotocol/tasks reads it. That client cannot distinguish outcomes programmatically without the disposition, which is why the disposition is required. Body, disposition, and, for a SEP-2643-aware server, the envelope MUST agree. The envelope appears only on a denial: because its presence asserts the operation was not performed, it MUST NOT appear on an execution-error or outcome-unknown result, where the server cannot make that assertion. The server MUST bound any downstream credential, and any authorization-decision or downstream-response cache it keeps, by the approval's expiry; this does not apply to the task's retained terminal result, which follows Retention.

Cancellation

A tasks/cancel request attempts the same atomic transition as the execution claim, from a cancellable pre-claim state to cancelled. Only a winning transition moves the task to cancelled and durably fences it against execution; if the execution claim already won or the task is already terminal, the cancel is a no-op. On a winning cancel the server MUST cancel the underlying approval request when the backend supports it (backend cancellation is optional; in the ARAP binding it is offered only when a cancellation endpoint is present) and otherwise MUST record the cancellation locally and ignore any later approval. Either way it is the local fence, not backend cancellation, that guarantees the tool does not run. Per SEP-2663, notifications/cancelled MUST NOT be used to cancel a task.

A cancel can arrive after approval but before execution. The server resolves the race by the atomic execution claim (see Completion): if the cancel wins the compare-and-set, the server honors it, moves the task to cancelled, and does not invoke (no side effect); if the claim wins, the cancel is ineffective and the task completes with its execution disposition. The server MUST NOT both execute the tool and report cancelled.

What the client carries

After task creation the client carries a server-generated taskId only and MUST NOT be required to read, store, or forward any authorization artifact. This extension adds meaning, not a new result shape (see Backward Compatibility): a tasks-only client reads the denied-versus-executed distinction from the CallToolResult body, an approval-aware client from the disposition (see Completion, Execution disposition).

This extension does not emit SEP-2643's optional authorizationContextId. That field is a non-authoritative correlation handle the client would echo on retry, but this client forwards no authorization artifact and the taskId already correlates the denial with its resolution. The authoritative binding between an approval and the exact call is the server-side immutable call binding, internal to the PEP and distinct from that handle.

End-to-end example

The JSON below abbreviates the SEP-2575 per-request metadata (protocol version, client information, and client capabilities) that a real request carries.

Client calls a tool (having declared the io.modelcontextprotocol/tasks extension in its per-request capabilities):

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "wire_transfer",
    "arguments": { "to": "acct-991", "amount": 50000 }
  }
}

The server gets a requestable denial, submits the approval request, and returns a task handle in place of the tool result:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "task",
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "status": "working",
    "statusMessage": "Transfers over $25,000 require treasury approval.",
    "createdAt": "2026-06-02T10:30:00Z",
    "lastUpdatedAt": "2026-06-02T10:30:00Z",
    "ttlMs": 604800000,
    "pollIntervalMs": 15000,
    "_meta": {
      "io.modelcontextprotocol/authorization": {
        "reason": "insufficient_authorization",
        "remediation": "available",
        "remediationHints": [{ "type": "task" }]
      },
      "io.modelcontextprotocol/model-immediate-response": "This transfer is pending treasury approval. You can continue other work and check back."
    }
  }
}

A tasks/get while the approval is pending returns the same working task with the envelope repeated. After the workflow resolves out of band, the server re-evaluates, executes, and a later tasks/get returns the completed task with the tool result:

{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "resultType": "complete",
    "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
    "status": "completed",
    "createdAt": "2026-06-02T10:30:00Z",
    "lastUpdatedAt": "2026-06-03T09:05:00Z",
    "ttlMs": 604800000,
    "result": {
      "content": [
        { "type": "text", "text": "Transfer of $50,000 to acct-991 completed." }
      ],
      "isError": false,
      "_meta": {
        "io.modelcontextprotocol/tool-approval-disposition": "approved-executed"
      }
    }
  }
}

Had it been denied, the task would complete with isError: true, the envelope classifying it (remediation: "unavailable"), and disposition denied-not-executed, and the body stating the tool did not run.

Example binding: AuthZEN ARAP (non-normative)

This section maps the abstract approval-backend contract above onto the OpenID AuthZEN Access Request and Approval Profile (ARAP), one backend a deployment can bind. Other backends map the same contract to their own constructs. Nothing here crosses the MCP wire: the ARAP constructs are internal to the PEP, and the client sees only the MCP task.

Abstract contract (this SEP) AuthZEN ARAP construct
requestable denial a decision carrying context.access_request
approval request submission an Access Request to the Access Request Service
durable, resumable handle the ARAP Task Handle
approval binding material binding_token, evaluation_id
idempotent submission key the Idempotency-Key header
re-evaluation with the approval as input an Access Evaluation with the approval object at context.approval
re-evaluation allow decision: true
re-evaluation not allowed decision: false with next_action: retry or request (non-terminal, task stays working) or none (terminal denial)
authoritative status source the ARAP Task Status Endpoint
submission schema and forms request_schema_url, form_url, request_catalogs_url
cancellation (optional) the ARAP cancellation endpoint, when present
backend terminal states denied and expired map to disposition denied-not-executed; cancelled to the cancelled task; failed to the failed task; partial is bulk-only and does not apply to a single tool call

Mapping a tools/call to an AuthZEN decision is itself a profile, the AuthZEN MCP profile (COAZ), which is a draft, so that mapping is not yet stable.

The concrete ARAP flow, everything internal to the PEP except the client-facing MCP messages:

Access Request ServiceAuthZEN PDPMCP Server (PEP)ClientAccess Request ServiceAuthZEN PDPMCP Server (PEP)ClientOut-of-band approval workflowtools/callAccess Evaluation (COAZ mapping)deny + context.access_request (requestable)submit Access Request (Idempotency-Key, binding_token)Task Handle (bound to MCP taskId)CreateTaskResult (status: working)approved (Task Status Endpoint or callback)Access Evaluation (context.approval)decision: true (allow)execute tool, task = completedtasks/getCompletedTask with result

Rationale

Server-directed creation makes the client trivial. Under SEP-2663 a tasks client already handles a CreateTaskResult for any call and the server decides when to return one, so an approval-gated server needs no new result shape and no per-tool opt-in.

Build on tasks, stay backend-agnostic. Tasks already gives a durable state machine with polling, cancellation, input, and retention, and an approval workflow is a long-running task over it. Because approval is brokered server-side and surfaced only as task status, the MCP contract is identical whichever party or backend decides. The SEP encodes "decided out of band," not "human in the loop."

Why an extension, not Informational. It adds wire-observable surface plain tasks does not: a required io.modelcontextprotocol/tool-approval-disposition _meta field, a requestable denial that MUST return a task, the denied-versus-executed distinction carried in the CallToolResult body, and authorization artifacts that MUST NOT cross the wire. The mandatory disposition field is the clearest reason this is an extension rather than Informational guidance.

Limitations and Open Questions

  • Cross-principal resumption is not portable. MCP task access is bound to a server-defined originating context, and tasks/list was removed by SEP-2663 (with sessions removed by SEP-2567), so a different principal cannot reclaim a task. Same-principal resumption across restart works to the extent the server recognizes the returning caller.
  • Someone has to consume the result. For a side-effecting tool the effect may land with no consumer observing it, so deployments MUST ensure a durable consumer polls or the effect is independently auditable.
  • A lost taskId has no recourse. With tasks/list and sessions removed, an agent that did not persist its taskId cannot rediscover a possibly-executed task, so clients MUST persist task IDs durably.
  • No general request idempotency in MCP. Cross-task duplicate suppression is therefore best-effort; a general key belongs in the core or the tasks extension, and this extension should adopt one if it is added.
  • Placement of the generic disposition. The generic execution-outcome values (execution-error, outcome-unknown) could move into SEP-2643 or the tasks extension, leaving the approval-specific values (approved-executed, denied-not-executed) here. A cross-SEP question for the sponsors.
  • Extension versus Informational. A sponsor who judges the normative surface too thin may prefer an Informational SEP.

Backward Compatibility

Additive for new tools and servers: a tasks client that does not implement this extension still observes a valid task lifecycle and a final completed/failed/cancelled task, with no new wire format. For an existing tool that previously returned a synchronous denial, enabling approval gating does change the observed tools/call response set (a CreateTaskResult, or -32003 / a degraded requestable CallToolResult, where a plain denial used to appear), so it is a behavior change for upgraders, not purely additive.

Reference Implementation

Required before Final (this introduces observable protocol behavior). Per SEP-2133 an Extension SEP also needs at least one official-SDK reference implementation, and named Extension Maintainers, before review; both are open prerequisites here. Planned: an MCP server fronting an approval backend (for example the AuthZEN ARAP) that returns CreateTaskResult for approval-gated calls and re-evaluates before execution, with a human-resolved and an automatically-resolved path; and a client built only on io.modelcontextprotocol/tasks, to show it interoperates with no approval-specific code.

A conformance scenario plus a sep-NNNN.yaml traceability file is required for Final, covering these MCP-observable statements:

  • a requestable denial returns a task without executing the tool;
  • approval resumes execution at most once;
  • a terminal denial is a completed task whose CallToolResult body (not only _meta) marks it denied-not-executed, distinct from an execution error;
  • a changed envelope (tool, arguments, principal, or subject/resource) requires a new approval;
  • a cancelled or expired task cannot later execute;
  • result retrieval does not expose backend artifacts;
  • submission input flows through the task input channel.

Security Implications

  • Denial remains denial. The server MUST NOT execute on the requestable denial alone and MUST re-evaluate after approval; the client gets no proof, so assurance beyond that is out of band.
  • Artifacts stay server-side. The backend's binding material never reaches the client, preventing replay or substitution through the client surface.
  • The task handle is not a bearer token. Authority is re-derived at execution, so a taskId never authorizes a different or mutated call (see The task handle is not authority).
  • Task access control. Task IDs are unguessable single-task handles; the server MUST authorize tasks/get, tasks/update, and tasks/cancel against the task's originating context and MUST NOT leak approval contents or approver identity through statusMessage.
  • Confused deputy. An approval applies only within the originally evaluated scope; the server MUST NOT let a client steer execution to a different operation.
  • Callback authenticity. A backend callback MUST be authenticated, and unless it carries an enforceable result the server MUST treat the backend's status source as authoritative and fetch before executing. A forged or replayed callback MUST NOT cause execution.
  • Expiry. Before an execution claim, after expiry the server MUST stop honoring the approval and durably fence the task against a late approval or callback; the claim is the enforcement point, so an approval valid at the claim carries through (see Completion).
  • At-most-once execution. A durable execution claim guards against a crash re-invoking a side-effecting tool, with outcome-unknown when the result cannot be confirmed; cross-task deduplication is best-effort.
  • Untrusted model-facing text. statusMessage and model-immediate-response MUST be sanitized, and a pending task MUST NOT be presented as completed.
  • Approval-request amplification. Approval fatigue is a known bypass; deployments MUST rate-limit submission per caller and SHOULD bound outstanding pending tasks, returning a non-requestable denial on a limit so the agent does not retry-loop.
  • Out-of-band resolution. Authentication, eligibility, and separation of duties for whoever decides are the backend's, outside MCP.
  • Durable sensitive state. Crash recovery and delayed execution require persisting the submission body, requester context, and complete tool arguments, which may hold credentials, payment details, PII, and approval-binding artifacts. The server MUST protect these records for confidentiality and integrity, scope access to the owning task, keep them out of ordinary logs, and delete them when the task's retention ends. It SHOULD NOT persist bearer credentials inside arguments; where unavoidable, they MUST be encrypted and held for the minimum lifetime.

References

SEP-3004 Tamper-Evident Audit Record Contract

Accepted · Standards Track · Created 2026-06-02

Imported from upstream. Proposed as modelcontextprotocol#3004 by @scottrhodes on 2026-07-02, where it was still closed unmerged 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.

The text in force is the vendored specification, at extensions/audit-record/spec, not this page. This page is the 2026-07-02 proposal text: it names canonical_form_version without pinning a value. The pinned value is audit-record-contract/1, and the vendored specification is the normative statement of it.

Implemented in omcp 2026-09-27. The reference verifier, the specification text, and the conformance vectors are vendored at extensions/audit-record, with four rejecting vectors added for requirements that had accepting cases only. The reference chapter is published at Tamper-Evident Audit Records.

Reviewed 2026-09-25 — accepted. The document carries every required section, and a prototype a reviewer can run is cited at https://github.com/notboatanchor/gif. The conformance vectors and the reference chapter that acceptance required are now in place.

It stays Accepted, not Final, deliberately. The normative text is the author's, and it is pinned here to an untagged commit. This moves to Final when @notboatanchor tags v1.0.0 upstream and the vendored copy is re-pinned to that tag — the author's call on when the text is settled, not ours.

Abstract

This SEP specifies an interoperability primitive: a canonical byte form and an append-only hash-chain construction that independent implementations produce identically and any third party can verify, with governance-specific context carried in registered extensions rather than in the core. What must agree across implementations is the bytes, not the semantics — so the construction is specified once and referenced by many, each layer's context riding as a registered extension on top.

The need is concrete: multiple MCP governance proposals require decisions to be recorded in a tamper-evident audit trail, but none defines what such a record is. PR #2809 makes a tamper-evident admission record a SHOULD without defining the record's shape or a shared, verifiable form for it; PR #2624 states as a compliance consideration that audit trails "MUST" be tamper-proof, with no checkable guarantee; caller-governance layers emit incompatible shapes. The guarantee is named at every seam and given a shared definition at none.

This SEP defines one vendor-neutral audit record contract: a minimal protected core; a type-keyed extensions mechanism so each layer attaches context under one integrity construction; deterministic sorted-JSON canonicalization aligned with PR #2809; an append-only hash chain; a verification procedure; and a structured attestation manifest for the part that is not protocol-observable.

Motivation

Audit is the one governance guarantee every layer asserts and none defines. Across three independent proposals:

  1. Admission (PR #2809). A conforming host SHOULD append a tamper-evident record for every admission decision. The proposal specifies the admission decision and its trust model; it does not define the record's shape or a shared, cross-layer verifiable form for the tamper-evident guarantee — the gap this contract fills.
  2. Interception / runtime security (PR #2624). Compliance Considerations require that "audit trails are tamper-proof" — a deployer obligation, not a checked property; the proposal's own audit mechanism is non-blocking observability.
  3. Caller governance. Session- and persona-scoped governance layers emit per-action decision records, but in shapes that vary by implementation (field names, what is integrity-protected, whether the record is append-only at all).

These three emit differently shaped records — caller-governance carries a declared purpose and delegating principal; runtime-security carries detections, drift status, and data classes; admission carries the clearance decision. No single layer's record shape is the universal one. What they share is not a field set but a construction: an append-only, canonically-hashed, chained, independently-verifiable record. Specifying that construction once — with a minimal common core and a typed extension point for each layer's context — lets all three conformance-anchor to one definition without flattening their differences. SEP-2484 makes a conformance check — or a documented exclusion — the bar for Final; today the tamper-evident guarantee has no shared definition to write a check against at any layer. Specifying the record and its verification once gives every layer one definition to reference and one vector set to anchor to, instead of each re-deriving and re-testing "tamper-evident" alone.

This SEP specifies that construction once. It does not move the guarantee into any one layer; it defines an orthogonal record contract the other proposals reference by forward-reference today and upgrade from SHOULD to MUST when this SEP lands.

Specification

The key words MUST, MUST NOT, REQUIRED, SHALL, SHOULD, SHOULD NOT, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119.

This contract constrains an audit record and the record chain it belongs to. It does not constrain the storage technology, the wire protocol used to read records, or which governance layer authors them. Any implementation that produces records satisfying §2.1–§2.7 and §2.9 conforms (§2.8 is reserved and out of scope for v1), regardless of mechanism.

2.1 The Audit Record — Protected Core

An audit record is a structured object describing one governed event. Every conforming record, whatever extensions it carries, MUST contain the following core fields, all of which are integrity-protected (§2.4):

Field Type Requirement
event_id opaque unique id MUST — unique within the chain
occurred_at RFC 3339 timestamp (UTC, Z, millisecond precision — §2.3) MUST — assigned by the recorder's clock, MUST NOT be settable by the governed caller
principal_id opaque id MUST — the governed identity the event is attributed to
event_type string from a registered vocabulary MUST
tool_name string | null MUST be present; null where no target applies
outcome allowed | denied | deferred | error MUST — the abstract disposition of the event (§2.1.1)
extensions object keyed by registered type id MUST — at least one registered extension (§2.2); at most one entry per type id
previous_hash hash digest | null MUST — links to the prior record (§2.4); null only for the first record in a chain segment
event_hash hash digest MUST — the integrity digest of this record (§2.4)

The core is deliberately emitter-neutral: identity, time, principal, the acted-on target, the disposition, and the chain linkage. Layer-specific accountability context does not belong in the core; it belongs in the extensions object under a registered type id (§2.2). Implementations MUST ignore unknown top-level fields rather than rejecting a record, to permit forward extension.

2.1.1 The outcome disposition enum

outcome is a closed, abstract disposition vocabulary, not a per-layer status code. The cross-profile anchor is the terminal/non-terminal axis:

  • allowed — the governed event proceeded (terminal).
  • denied — the governed event was refused (terminal).
  • deferred — the governed event is held pending a further decision (non-terminal; e.g. a quarantine hold pending re-approval).
  • error — the governance evaluation itself failed (terminal; the record SHOULD carry diagnostic context in an extension).

Domain-specific dispositions, reason codes, and severities MUST NOT extend this enum; they live inside the emitting layer's extension data, bound to the base outcome (an extension supplies decision evidence that informs the core outcome, never a competing outcome — e.g. a quarantine decision that holds the event maps to deferred, a terminal quarantine maps to denied). The classification of an event_type or extension field as security-relevant is registration prose, not a record field — keeping it out of the digest.

2.2 Registered Extensions

The record contract is extended only through registered extensions, never through new chain constructions. A record carries an extensions object — a JSON object keyed by registered type id, each key's value being that extension's typed data: extensions: {"<type>": <data>, ...}. Extension data is canonicalized into the same preimage as the core (§2.3) and is therefore integrity-protected by the same chain — there is exactly one chain construction, shared across all extensions, and the type id is itself a preimage key, so an extension's type cannot be swapped without breaking verification.

Structural rules:

  • A conforming record MUST carry at least one registered extension (the emitting layer's own context) and MAY carry several — e.g. a caller-governance record that also carries runtime-security evidence chains both under one digest.
  • The keyed-object form makes a duplicate type structurally unrepresentable: at most one entry per type id, keys sorted by the same every-level key-sort as the rest of the record (§2.3).
  • Type ids are flat names from a curated registry (the IANA-media-type pattern), not a permissionless namespace; reverse-DNS-style ids are not used. Type ids and registered field names form a controlled ASCII registry vocabulary (§2.3).

An extension registration defines: the type id; its REQUIRED and OPTIONAL data fields and their types (and any closed value vocabularies); the event_type vocabulary it recognizes (§2.9); and how its decision evidence binds to the core outcome (§2.1.1). A record's data for each declared extension MUST satisfy that registration's required fields.

This SEP registers and fully specifies one extension, records the converged shape of a second whose normative registration text is contributed by its implementer, and names a third contributed by reference:

  • caller-governance (specified here). REQUIRED: purpose_declared (the declared purpose/intent captured at event time). OPTIONAL: session_id, invoked_by_principal_id (the delegating principal), flagged, sources_touched, sensitivity_encountered, output_disposition, human_actor_id. All values defined by this profile MUST be JSON strings, except flagged, which MUST be a JSON boolean; an OPTIONAL field MAY instead carry null under the presence rule below. This keeps the body within the base record's string/boolean/null restriction (§2.3).

    • Presence of OPTIONAL fields. An OPTIONAL field that is absent means the emitter does not record it; an OPTIONAL field carried as null means the emitter records it and it had no value for this event. These are distinct facts, and an emitter MUST NOT use the two forms interchangeably for the same field. (The known-answer records under Conformance carry invoked_by_principal_id: null — recorded, no delegating principal — and omit sources_touched.)
    • sources_touched encoding. When non-null, the value MUST be the canonical serialization of a JSON array of strings, at every cardinality including a single source — never a bare delimiter-joined list, which cannot unambiguously represent a source name containing the delimiter. Each element MUST be a well-formed Unicode string (no unpaired surrogate code units) that satisfies the §2.3 protected-string rules (well-formedness, NFC, U+0020 trim, control-character rejection, length cap) and MUST be non-empty; elements are then deduplicated and sorted in ascending order of their UTF-8 byte sequences (equivalently, Unicode code-point order). The array is serialized with no insignificant whitespace, escaping only " as \" and \ as \\, and that serialization is the field's string value. A recorded-but-empty set is carried as null, never as "[]".
  • runtime-security — converged shape; normative registration text contributed by Maaz (Interlock), the runtime-security implementer (final text 2026-06-14; cross-ref PR #2624), who independently reproduced the two-extension known-answer digest from the published rule. The runtime-security object summarizes a runtime policy disposition for the event and composes with other extension bodies without interaction. All values defined by this profile MUST be JSON strings (no numeric, boolean, null, array, or nested-object values), keeping the body within the base record's string-only canonicalization and outside any number-canonicalization concern (a profile convention, not a core rule — §2.3). Fields:

    • drift_status (REQUIRED, string enum) — the runtime's evidentiary state regarding tool-surface drift: none (no drift relative to the approved surface evidenced), observed (a change evidenced, without a determination that it is policy-relevant), confirmed (a policy-relevant change evidenced). A consumer that does not recognize a value MUST treat the field as absent (no claim made), not infer a default.
    • severity (REQUIRED, string enum) — info | low | medium | high, in non-decreasing order of severity. Numeric scores are out of scope for this extension and MUST NOT be carried in the canonical body.
    • quarantine_decision (REQUIRED, string enum) — the disposition the runtime applied: release (permitted to proceed), hold (held pending review), quarantine.
    • policy_id (REQUIRED, string) — identifies the policy that produced the disposition, of the form <scope>/<name>@<revision>, where <scope> is an emitter-qualified namespace (e.g. a reverse-DNS name, a URN, or a registered prefix) sufficient to disambiguate the policy across emitters. It is vendor-neutral: it names a policy, not a product, an engine, or a detection method (e.g. example.org/runtime-drift@3).
    • evidence_hash (OPTIONAL, string) — when present, a cryptographic digest expressed as <algorithm>:<hex> (e.g. sha256:<hex>) committing to an out-of-band evidence record. It is an opaque commitment only: this contract does not inline the evidence, prescribe its schema, or require any particular detection method, and the base record commits to it as opaque bytes without verifying it; a consumer wishing to rely on the evidence resolves and verifies it out of band. OPTIONAL because a drift_status: none event may have nothing to commit to.

    Outcome binding (§2.1.1): the disposition is decision evidence informing the base outcome, never a competing or independent outcome field; the base outcome MUST reflect the actual enforcement result. A confirmed drift with quarantine_decision: quarantine held pending review maps to outcome: deferred (as in the /2 conformance fixture); a terminally-blocked disposition maps to outcome: denied. Other combinations follow the base record's outcome semantics. (This prevents a record whose base outcome is allowed from simultaneously carrying a quarantine disposition: the extension explains why; the base outcome states what happened.)

  • admission-control (named; field set contributed — the clearance decision and its inputs). Cross-ref PR #2809.

New extensions are added by registration without altering §2.1, §2.3, or §2.4. An extension MUST NOT redefine a core field or introduce a second canonicalization.

2.3 Canonicalization

Integrity (§2.4) is computed over a deterministic canonical form of the record's protected fields (the core minus event_hash, plus the full extensions object). The canonical form is sorted-key canonical JSON, aligned with the canonicalization used by PR #2809's clearance assertion so that adopters process clearance assertions and audit records on one code path and one vector matrix. A canonicalization MUST:

  • Sort object keys lexicographically — in ascending order of their UTF-8 byte sequences, equivalently Unicode code-point order — at every level, including the extensions object and each extension's nested data, and serialize with no insignificant whitespace. The sort basis is declared rather than inherited from a host language's default; on the controlled ASCII registry vocabulary (§2.2) every common basis coincides.
  • Canonicalize extensions in the type-keyed object form extensions:{"<type>":<data>, ...} — this exact representation is the preimage. Implementations MUST NOT substitute an alternative representation (e.g. an array of {type, data} pairs): different representations of the same content produce different digests, and the keyed object is what makes one-entry-per-type structurally unrepresentable and rides the same key-sort with no special-case rule.
  • Restrict protected values to strings, booleans, and null — each of which has exactly one canonical JSON form. Bare numbers are excluded from protected bodies in this version (1 vs 1.0 vs 1e0 have no agreed single form; a number-canonicalization rule is deferred to a later version — encode numeric content as a string in the meantime). null is part of the core form (every segment head carries previous_hash: null); booleans serialize as true/false. An extension MAY adopt a stricter all-strings convention for its own data (runtime-security does), but that is the extension's convention, not a core rule.
  • Encode null distinguishably from empty string (null vs "").
  • Serialize occurred_at as RFC 3339 in UTC with millisecond precision and a literal Z (the Date.toISOString() form, e.g. 2026-06-02T12:00:00.000Z), so timestamp digests are reproducible across implementations regardless of the recorder's native timestamp storage.
  • Normalize protected string values to bound the coupling of integrity to free-text fields: Unicode NFC, trim leading/trailing U+0020 (ASCII space) only, reject control characters, require well-formed Unicode (no unpaired surrogate code units), and enforce a length cap (baseline 8192 UTF-16 code units, measured on the normalized value). A protected string failing normalization makes the record non-conforming. The trim charset is deliberately U+0020 only, not the full Unicode whitespace class: ASCII space is the only commonly-accidental edge whitespace, while non-control Unicode whitespace (NBSP U+00A0, the U+2000–U+200A range, ideographic space U+3000, etc.) is preserved as significant content. Naming the charset is load-bearing for cross-implementation reproducibility — an unqualified "trim" diverges across implementations (SQL btrim(x, ' ') strips U+0020 only; a typical host .trim() strips the full whitespace class). Control characters (Unicode category Cc: U+0000–U+001F and U+007F–U+009F) — including the C0 set (tab U+0009, LF U+000A, CR U+000D) — are rejected, not trimmed; a leading or trailing control character renders the record non-conforming rather than being stripped. Well-formedness matches the §2.2 element rule: a value carrying an unpaired surrogate code unit makes the record non-conforming (RFC 8259 §8.2 leaves receiver behavior for unpaired surrogates unpredictable — a parser may reject the record, substitute U+FFFD, or pass the code unit through, and the latter two silently change the digest). Registry vocabulary is exempt: extension type ids and registered field names are a controlled ASCII registry vocabulary (§2.2) and pass through the canonical form verbatim — they are object keys, not values, and the registry (not normalization) is what constrains them. The exemption is safe precisely because the vocabulary is registry-controlled ASCII; an unregistered type id is non-conforming under C-REC-1 regardless of its bytes.
  • Escape minimally and encode as UTF-8. Within a serialized string value, escape only " as \" and \ as \\; every other character, including non-ASCII, is serialized literally, never as a \uXXXX escape (normalization has already rejected every character JSON cannot carry literally). The canonical form hashed in §2.4 is the UTF-8 encoding of that serialization. This is the same convention the sources_touched encoding names (§2.2); left unstated, a host JSON serializer that escapes non-ASCII by default produces a different preimage — and a different digest — from one that does not, for any value outside ASCII.
  • Exclude event_hash (the output) and the reserved anchor_witness (§2.8, unused in v1) from the canonical body.

The canonical form is identical across extensions; only the data each type id keys differs. A fixed two-extension known-answer test — one record carrying both caller-governance and runtime-security, reproducible from a shell with sha256sum — pins how two extensions hash side by side. (The fixed value, with its canonical preimage, is given under Conformance; the companion vectors reproduce it and the full matrix programmatically.)

2.4 Integrity Construction (Record Chain)

Records form an append-only hash chain. For each record:

  • event_hash MUST equal H( canonical_form( protected_body ) ), where H is a collision-resistant hash function, canonical_form is §2.3, and protected_body is the core (less event_hash) plus the full extensions object, and includes the record's own previous_hash.
  • previous_hash MUST equal the event_hash of the immediately preceding record in the chain segment, or null for the first record in a segment.

H MUST be SHA-256 at baseline. An implementation MAY use a stronger function; the function in use MUST be recorded in the attestation manifest (§2.7) so a verifier is unambiguous (algorithm agility). Implementations MUST NOT use a hash function with a known practical collision. The digest values carried in the record (event_hash, previous_hash) MUST be expressed as lowercase hexadecimal: digest comparison (§2.6) is then exact string equality, and a digest re-entering the preimage as the next record's previous_hash has a single byte form across implementations.

A chain "segment" is a contiguous run of records over which previous_hash threading is continuous. Implementations MAY segment chains (e.g. per time window or partition); each segment MUST be independently verifiable and the segmentation boundary MUST be discoverable to a verifier.

2.5 Append-Only Requirement

Once committed to the chain, a record MUST NOT be modified or deleted by any caller — including a privileged operator — without that alteration being detectable by the verification procedure (§2.6). Concretely:

  • The record store MUST reject in-place UPDATE and DELETE of committed records through its normal access paths.
  • Any out-of-band alteration (e.g. by a database superuser) MUST change the altered record's canonical form and therefore break chain verification at or after the altered record.

The enforcement mechanism is implementation-defined and is explicitly NOT constrained by this SEP; it is declared in the attestation manifest (§2.7). Conforming mechanisms include, and are not limited to: storage-engine permission revocation plus row-level policy denying mutation; write-once media; an append-only log service; or a ledger.

2.6 Verification Procedure

A conforming chain MUST be verifiable by an independent party in possession of the records, by the following procedure:

  1. For each record, recompute H( canonical_form( protected_body ) ) and compare it to the stored event_hash. A mismatch indicates the record was altered after commitment (in any protected field, core or extension).
  2. For each record after the first in a segment, confirm previous_hash equals the prior record's event_hash. A mismatch indicates insertion, deletion, or reordering.

The procedure runs identically whether executed by an adopter attesting to its own trail or by a third party with read access to an exported record set. It MUST be deterministic: the same record set MUST always produce the same verdict.

2.7 Attestation Manifest (Structured Attestation)

The append-only enforcement of §2.5 is a storage property, not a wire-observable one — so gating it at the protocol surface would be theater. It is therefore attested, but attestation MUST be structured: a bare "trust us" claim is not conformant. A conforming implementation MUST publish a machine-readable attestation manifest declaring:

  • storage_mechanism — the append-only enforcement mechanism (§2.5).
  • chain_algorithm — the hash function in use (§2.4).
  • canonical_form_version — the canonicalization version (§2.3).
  • verification_procedure_ref — a resolvable pointer to a reproducible implementation of §2.6 that runs over an exported record set and emits a deterministic pass/fail verdict.

The machine-checkable vectors (the companion vector set) verify the observable record (schema, extension validity, canonical form, chain threading, emission). The manifest plus the referenced verifier make the integrity construction attested-but-auditable rather than attested-in-the-abstract: a reviewer can re-run the declared verification over the records and reproduce the verdict.

2.8 External Anchoring (Reserved; Out of Scope for v1)

Anchoring a chain to an external witness (e.g. RFC 3161 timestamp, version-control commit, object-store checksum, public ledger) addresses a distinct, weaker-priority threat — an auditing organization rewriting its own history — that is orthogonal to the within-system chain construction. Selecting a witness scheme is deferred to avoid binding v1 to an ecosystem pick.

A field name anchor_witness is reserved for this purpose. In v1 it MUST be absent or null and is NOT included in the integrity preimage (§2.3); a follow-on anchoring extension defines its semantics and whether it enters the preimage. Anchoring's absence MUST NOT affect conformance to §2.1–§2.7.

2.9 Emission Completeness

This SEP defines the record and its construction, not the policy of which events a given governance layer must record — that belongs to each referencing proposal / extension registration. A referencing proposal that adopts this contract:

  • MUST define the set of event_type values it requires to be recorded and the conditions under which each is emitted;
  • MUST record each such event as a conforming record (§2.1–§2.4);
  • and the recording of an event MUST NOT fail the governed operation's response path solely because record emission failed (audit emission is non-blocking; a failed emission MUST itself be detectable, e.g. recorded as an error record or surfaced out-of-band).

Rationale

Why a separate record contract, rather than audit inside each layer. Audit is asserted at several seams (admission, interception/runtime-security, caller-governance) and given a shared, verifiable definition at none; each names the guarantee without defining a record shape the others can verify against. What those layers share is not a field set but a construction — an append-only, canonically-hashed, chained, independently verifiable record. Specifying that construction once, with a minimal common core and a typed extension point per layer, lets all of them conformance-anchor to one definition without flattening their differences. This follows the project's convergence principle (one well-defined way to solve a problem) rather than fragmenting audit across N incompatible shapes.

Key design decisions and alternatives considered.

  • Type-keyed extensions object, not a {type, data} array. The keyed-object form makes a duplicate extension type structurally unrepresentable, makes the type id a preimage key (so an extension's type cannot be swapped without breaking verification), and rides the same every-level key-sort with no special-case rule. An array form was rejected: different representations of the same content produce different digests, and the array invites duplicate-type and ordering ambiguity.
  • Protected values restricted to string / boolean / null; no bare numbers. Each of these has exactly one canonical JSON form. Bare numbers (1 vs 1.0 vs 1e0) have no agreed single form; rather than bind v1 to a number-canonicalization scheme, numeric content is encoded as a string and a number rule is deferred to a later canonical-form version. The stricter all-strings convention adopted by the runtime-security extension is its own profile choice, not a core rule.
  • Abstract outcome enum, not per-layer status codes. A closed disposition vocabulary (allowed/denied/deferred/error) keyed on the terminal/non-terminal axis stays stable across profiles; domain-specific reason codes and severities live in extension data and bind to the base outcome as evidence, never as a competing outcome. This keeps the cross-profile anchor fixed while letting each layer carry its own semantics.
  • Attested-but-structured append-only enforcement, not wire-gating and not "trust us". Append-only enforcement (§2.5) is a storage property, not a protocol-observable one; gating it at the wire would be theater. A bare "trust us" attestation is non-conformant. The chosen middle path is a machine-readable attestation manifest plus a referenced, reproducible verifier that re-runs over an exported record set — so a reviewer can reproduce the verdict without the guarantee depending on an unobservable claim.
  • Sorted-JSON canonicalization aligned with PR #2809, not a new canonicalizer. Adopters process clearance assertions and audit records on one code path and one vector matrix; alignment, not a blocking dependency.
  • Trim charset is U+0020 only, not the full Unicode whitespace class. Naming the charset is load-bearing for cross-implementation reproducibility: an unqualified "trim" diverges (btrim(x, ' ') strips ASCII space only; a typical host .trim() strips the full class). Non-control Unicode whitespace is preserved as significant content; control characters are rejected, not trimmed.
  • External anchoring reserved, out of scope for v1. Anchoring to an external witness addresses a distinct, weaker-priority threat and would bind v1 to an ecosystem pick; a reserved anchor_witness hook leaves the door open without committing the core.

Evidence of community process. The contract was developed in the MCP Security Interest Group (#security-ig) with the author of the admission proposal (PR #2809) and the runtime-security implementer, Syed Maaz Ahmed (Interlock; cross-ref PR #2624). The runtime-security extension's normative registration text was contributed by Maaz rather than authored top-down. The two-extension known-answer digest reproduces byte-for-byte from the published rule plus stock sha256sum across independent implementations — the cross-implementation agreement the construction is meant to produce.

On the "make it an extension" question. A reasonable reading of the project's composability and standardization principles is "build audit on top, as an extension, rather than in the specification." This contract is positioned as an interoperability primitive — a canonical byte form that independent implementations must agree on for cross-vendor verification — not as a protocol feature or as governance-in-core. The governance semantics ride in registered extensions, off the core; the only thing standardized is the construction multiple layers already converge on. Positioned this way, the record contract is the cross-cutting evidence layer beneath the other proposals: admission (#2809) decides whether a server may be used, interception/runtime-security (#2624) decides whether an admitted surface drifted, and caller-governance decides whether a caller may invoke — three different decisions, all three needing the same tamper-evident record to be accountable. Standardizing that record once is what lets the three compose without each re-inventing "tamper-evident"; it is the interop seam between them, not a governance feature competing with any of them.

Backward Compatibility

This SEP is purely additive and introduces no backward incompatibilities.

  • No existing MCP message changes shape. The contract defines an orthogonal record and its verification; it adds no new wire messages and no changes to existing schemas, and it does not dictate a database engine, transport, or runtime architecture. The record and its verification are off-wire (evaluated over an exported record set), so there is no protocol-surface incompatibility. An implementation that does not adopt the contract is unaffected, and an unextended host or server behaves exactly as today.
  • Effect on referencing proposals is opt-in and non-breaking.
    • Admission (PR #2809). A proposal whose tamper-evident admission record is a SHOULD MAY, if this SEP lands, forward-reference it and upgrade that obligation to a MUST at its own discretion; the canonicalization here is the sorted-JSON form already used by #2809's clearance assertion, not a new one.
    • Runtime security (PR #2624). This contract supplies the checkable record shape that #2624 states as a compliance consideration; adopting it requires no change to an engine's existing runtime enforcement. Until a referencing proposal chooses to upgrade, nothing changes for it.
  • Forward-compatible versioning. Layer-specific context is isolated in the type-keyed extensions object, so new profiles register without altering the core. The canonical-form version is carried in the attestation manifest (§2.7), not in the hashed bytes, so a later canonicalization revision (for example, adding number canonicalization, or activating the reserved anchor_witness hook, §2.8) can be introduced as a new canonical-form version without invalidating chains produced under an earlier one.
  • No deprecations. This SEP removes or deprecates nothing.

Conformance

Conformance is split along what is protocol-observable, consistent with SEP-2484's documented-exclusion model.

Machine-checkable (vectors provided). Given a set of records and a manifest:

  • C-REC-1 — every record carries the §2.1 core with conforming types and a legal outcome disposition (§2.1.1); its extensions object carries at least one registered type id, no unregistered type ids, and each extension's data satisfies its registration's required fields and closed vocabularies.
  • C-REC-2 — the canonical form (§2.3) is deterministic, injective over the protected body (core and extension data), uses the mandated type-keyed extensions representation, restricts protected values to string/bool/null (no bare numbers), normalizes equivalent strings, and rejects control characters and unpaired surrogate code units; null encodes distinguishably from empty string; registry vocabulary (type ids, registered field names) passes through verbatim.
  • C-REC-3 — event_hash equals the hash of the canonical form of the protected body (§2.4), including a fixed known-answer test for cross-implementation interop — among them a two-extension record pinning how multiple extensions hash side by side.
  • C-REC-4 — previous_hash threading is continuous within a segment; a mutated or removed record is detected (§2.6) — including mutation of an extension field such as purpose_declared.
  • C-REC-5 — declared-required event_type values are present for a scripted sequence of governed events (§2.9).
  • C-REC-6 — the extension mechanism is emitter-neutral: records carrying different (and multiple) extensions chain under one construction, and extension data is integrity-protected by the same chain.
  • C-REC-7 — the attestation manifest (§2.7) is structured (declares mechanism, algorithm, canonical-form version, and a verification-procedure pointer); a bare attestation is non-conformant.

A normative, runnable vector set for C-REC-1…7 is published with the reference implementation, in the GIF repository under mcp-server/conformance/audit-record-contract/ (https://github.com/notboatanchor/gif/tree/e1f02a95506e81e7766c3ba3a684ecad7cfff12f/mcp-server/conformance/audit-record-contract). From mcp-server/, npm run vectors yields 23 vectors — 23 passed, 0 failed (zero-dependency alternative: node --experimental-strip-types conformance/audit-record-contract/run.ts). Because the integrity guarantee is off-wire (§2.7), these are record/verifier vectors evaluated over an exported record set — not wire-protocol scenarios — providing the machine-checkable evidence SEP-2484 requires for Final, paired with the structured attestation (§2.7) for the part that is not protocol-observable.

Two-extension known-answer test (self-contained). The cross-implementation interop anchor referenced in C-REC-3 is fixed here so it can be reproduced with no repository and no dependence on any implementation — a single segment-head record (previous_hash: null) carrying both registered extensions, pinning how two extensions hash side by side under one digest. The identifiers are vendor-neutral constants; event_hash is the computed output and is excluded from the preimage.

field value
event_id 99999999-9999-9999-9999-999999999999
event_type tool_call
occurred_at 2026-06-06T12:00:00.000Z
outcome deferred
previous_hash null
principal_id aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
tool_name export
extensions › caller-governance flagged=false, invoked_by_principal_id=null, purpose_declared="reconcile June invoices", session_id="55555555-5555-5555-5555-555555555555"
extensions › runtime-security drift_status="confirmed", evidence_hash="sha256:b2c547e2…e2d9be", policy_id="example.org/runtime-drift@3", quarantine_decision="quarantine", severity="high"

The canonical form (§2.3) of the protected body is the exact byte string:

{"event_id":"99999999-9999-9999-9999-999999999999","event_type":"tool_call","extensions":{"caller-governance":{"flagged":false,"invoked_by_principal_id":null,"purpose_declared":"reconcile June invoices","session_id":"55555555-5555-5555-5555-555555555555"},"runtime-security":{"drift_status":"confirmed","evidence_hash":"sha256:b2c547e2c8f17eafc72ef5c2d4d7b6b4d0f7437ab52bae573a9af14ff5e2d9be","policy_id":"example.org/runtime-drift@3","quarantine_decision":"quarantine","severity":"high"}},"occurred_at":"2026-06-06T12:00:00.000Z","outcome":"deferred","previous_hash":null,"principal_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","tool_name":"export"}

whose SHA-256 is:

f733fed9cc757165f810b778e4baba1f51a45504988e937707aaab4361b2f064

Reproduce from any shell — paste the preimage block above between the quotes:

printf '%s' '<canonical preimage above>' | sha256sum
# -> f733fed9cc757165f810b778e4baba1f51a45504988e937707aaab4361b2f064

Because the digest is a property of the canonical bytes alone, any implementation adopting §2.3 lands on the same value. The published vector set additionally pins the single-extension caller-governance digest (d494769c…) and the full C-REC-1…7 matrix.

Attested-but-structured (not protocol-observable). The append-only enforcement of §2.5 is attested via §2.7. A conformance harness verifies the consequence (chain verification fails after an out-of-band mutation, C-REC-4) and the structure of the attestation (C-REC-7), rather than the storage mechanism itself.

Open Questions

(The design decisions resolved during #security-ig review — extension representation, canonical form, the outcome vocabulary, value types, and the attested-but-structured verification surface — are recorded in the Rationale's "Key design decisions and alternatives considered.")

  • Q-A — Normative registration text for admission-control. The runtime-security registration was contributed by its implementer and is folded into §2.2; admission-control remains contributed by reference (PR #2809). This SEP fixes the mechanism and the caller-governance extension.
  • Q-B — Anchoring extension. Semantics of the reserved anchor_witness (§2.8), deferred to a follow-on.
  • Q-C — Number, sponsor, and venue. Homed under #security-ig; a formal PR cross-references PR #2809 and PR #2624. Sponsor being secured.

Security Implications

A hash chain provides tamper-evidence, not tamper-prevention: it guarantees that alteration of committed records is detectable, not impossible. Detection depends on at least one trustworthy copy of a prior event_hash — the motivation for the reserved external-anchor hook (§2.8) against adversaries who can reach the record store. Because purpose_declared and other extension fields are inside the chain (§2.2), the privileged-actor-rewrites-intent vector is closed: altering a recorded reason breaks verification. The integrity guarantee is only as strong as the protected field set; fields an extension registration chooses to leave out of its data object are unprotected. Timestamps are recorder-assigned (§2.1) to prevent a governed caller from backdating records. String normalization (§2.3) bounds — but does not eliminate — the risk of homoglyph/encoding ambiguity in free-text fields; it does not address confidentiality of record contents, which may carry sensitive governance context and SHOULD be access-controlled independently of integrity protection. A verifier processing records from an untrusted source SHOULD bound canonicalization (§2.3) recursion depth: deeply-nested input is otherwise a stack-exhaustion vector, and conforming records are shallow, so a generous cap rejects only hostile input (the reference verifier caps depth accordingly).

Reference Implementation

An open-source reference implementation of this construction exists: the Governed Intelligence Framework (Apache 2.0, https://github.com/notboatanchor/gif). It implements the core + chain construction + the caller-governance context. Its canonical form matches this contract's rule (sorted-JSON, millisecond RFC 3339 timestamps, purpose_declared in the preimage). It implements this draft's extensions keyed-object shape (canon_version = gif-audit/2, migration 015; the predecessor single-profile shape gif-audit/1 merged in gif PR #28). Its audit trigger emits and reproduces the single-extension caller-governance known-answer digest (d494769c…) as its own self-test; the two-extension digest (f733fed9…, given under Conformance) is reproduced by the same canonical rule over the published preimage — and exercised synthetically in the conformance vectors — rather than minted by the live trigger. This is consistent with the digest being a property of the canonical bytes, not of any one implementation: a second implementer (the runtime-security implementation, cross-ref PR #2624) independently reproduced it from the rule alone. It produces caller-bound, append-only, SHA-256 hash-chained audit records; enforces append-only at the storage layer via privilege revocation plus row-level policy and a trusted hashing trigger; and ships runnable conformance scenarios for record emission, append-only behavior, and scope-gated read. Per the spec-vs-source discipline, the reference implementation does strictly more than this contract requires (it binds records to a persona/session governance model and a scope system not specified here), and the contract is written to admit implementation paths other than the reference one (storage engine, enforcement mechanism, and every extension beyond caller-governance are open).

Independent implementations of the same construction under other extensions exist — a runtime-security receipt (cross-ref PR #2624) and an admission record (cross-ref PR #2809) — which is the intended outcome: the construction is the invariant, the extension is the variation, and multiple implementation paths anchor to one definition.

Acknowledgments

This contract was developed in the open in the MCP Security Interest Group (#security-ig). The admission-control extension is named by reference to the Attested Tool-Server Admission proposal (ATSA, PR #2809).

References

  • PR #2809 — admission / clearance assertion proposal (tamper-evident admission record as a SHOULD; source of the shared sorted-JSON canonicalization).
  • PR #2624 — interception / runtime-security proposal (tamper-proof audit trail as a compliance consideration).
  • SEP-2484 — conformance suite (machine-checkable vectors as the Final gate).
  • RFC 2119 — normative keywords.
  • RFC 3339 — date/time format.
  • RFC 3161 — trusted timestamping (one external-anchor option, §2.8).

SEP-3094 Granular citations format

Draft · Standards Track · Created 2026-07-15

Imported from upstream. Proposed as modelcontextprotocol#3094 by @karth295 on 2026-07-15, 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

Many popular MCP clients (e.g. ChatGPT, Copilot) have bespoke formats for MCP servers to return rich citations. Other popular MCP clients (e.g. Claude) only render citation chips from first-party features such as public web search, making responses from MCP servers appear less trustworthy. Citations are an important mechanism for professionals and academic researchers to verify the accuracy of LLM-generated responses.

This SEP proposes standardizing a mechanism for MCP servers and clients to exchange granular, verifiable citations to resources or portions of resources — sentences, HTML elements, image regions, table rows. It reuses existing MCP and web standards for selectors and fragments, such as MCP resource URIs and schema.org citations. It explicitly tries to avoid creating bespoke formats that have not gone through rigorous review and real world usage.

Motivation

Building trust through verifiable responses

Professionals such as lawyers, auditors, and academic researchers are legally and ethically bound to produce work grounded in hard evidence. As AI models have become more capable and MCP servers/tools have become more agentic, professional work has been moving towards steering and reviewing AI outputs.

However, when professionals do not catch hallucinations, the consequences can be severe. The US court of appeals for the sixth circuit fined lawyers $30,000 for misrepresentations. Damien Charlotin's database underscores the depth of the problem with nearly 1200 hallucinated cases cited before July 2026.

Citations have been an integral part of the end-user experience of using LLM-based systems even as early as 2022. However, how citations are exchanged and presented is bespoke to tools and clients.

This proposal does not discuss methods to reduce hallucinations; it instead proposes formalizing commonly used patterns for servers and clients to present avenues for professionals to verify AI outputs for accuracy and completeness.

Varied MCP client support for exchanging citations

Here is a quick summary of support for citations among a few popular MCP clients in July 2026. Even among clients that support citations, the expected format varies.

MCP host Primary product(s) Citation chips Clickable links Notes
Anthropic Claude Claude Desktop, claude.ai No No Only plain markdown links are supported. MCP embedded resources are not shown to the user. First party web search chips cannot be driven by MCP servers.
OpenAI ChatGPT ChatGPT Connectors / Apps, Responses API remote MCP Yes Yes Specifically search and fetch tools that return a non-empty url field get inline citation chips linking to that URL.
Microsoft Copilot Microsoft 365 Copilot (declarative agents), Copilot Studio Yes Yes Firm-admin approved Plugins can return citations via a JSON format

Some MCP clients do not have any mechanisms for servers to push down citations to be rendered as citation chips. This makes MCP-based tools appear less trustworthy than first party tools, even though their responses may be more grounded in authoritative data.

Note that MCP apps can be used to render citations. However, this is unnecessarily heavyweight for MCP servers to implement for documents that are already addressible via URI.

Server-returned citations also vary widely, even within the same server provider.

Provider Tool Example response
Anthropic web_search
{
  "text": "Claude Shannon was born on April 30, 1916, in Petoskey, Michigan",
  "type": "text",
  "citations": [
    {
      "type": "web_search_result_location",
      "url": "https://en.wikipedia.org/wiki/Claude_Shannon",
      "title": "Claude Shannon - Wikipedia",
      "encrypted_index": "Eo8BCioIAhgBIiQyYjQ0OWJmZi1lNm..",
      "cited_text": "Claude Elwood Shannon (April 30, 1916 – February 24, 2001)..."
    }
  ]
}
Anthropic web_fetch
{
  "text": "the main argument presented is that artificial intelligence will transform healthcare",
  "type": "text",
  "citations": [
    {
      "type": "char_location",
      "document_index": 0,
      "document_title": "Article Title",
      "start_char_index": 1234,
      "end_char_index": 1456,
      "cited_text": "Artificial intelligence is poised to revolutionize healthcare delivery..."
    }
  ]
}
OpenAI web_search (url_citation)
{
  "type": "output_text",
  "text": "On March 6, 2025, several news outlets reported...",
  "annotations": [
    {
      "type": "url_citation",
      "start_index": 2606,
      "end_index": 2758,
      "url": "https://example.com/article",
      "title": "Example Article"
    }
  ]
}
OpenAI file_citation / container_file_citation
{
  "type": "output_text",
  "text": "Per the uploaded report, revenue increased 12% year-over-year.",
  "annotations": [
    {
      "type": "file_citation",
      "file_id": "file-abc123",
      "filename": "q3-earnings.pdf",
      "index": 0
    }
  ]
}
Perplexity Sonar
{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "Apple reported Q2 revenue of $95.4 billion [1], driven by Services growth [2]."
      }
    }
  ],
  "citations": [
    "https://www.apple.com/newsroom/pdfs/fy2025-q2/FY25_Q2_Consolidated_Financial_Statements.pdf"
  ],
  "search_results": [
    {
      "title": "[PDF] Consolidated Financial Statements - Apple",
      "url": "https://www.apple.com/newsroom/pdfs/fy2025-q2/FY25_Q2_Consolidated_Financial_Statements.pdf",
      "date": "2025-05-01",
      "snippet": "Net sales by reportable segment: Americas $40,315..."
    }
  ]
}
Perplexity Agent API
{
  "type": "output_text",
  "text": "Spain won Euro 2024, defeating England 2-1 in the final [1].",
  "annotations": [
    {
      "type": "url_citation",
      "start_index": 0,
      "end_index": 52,
      "url": "https://en.wikipedia.org/wiki/UEFA_Euro_2024_final",
      "title": "UEFA Euro 2024 final"
    }
  ]
}
Amazon Bedrock InvokeAgent
{
  "completion": "The refund policy allows returns within 30 days of purchase.",
  "citations": [
    {
      "generatedResponsePart": {
        "textResponsePart": {
          "text": "returns within 30 days of purchase",
          "span": { "start": 32, "end": 66 }
        }
      },
      "retrievedReferences": [
        {
          "content": {
            "type": "TEXT",
            "text": "Customers may return items within 30 days..."
          },
          "location": {
            "s3Location": { "uri": "s3://my-bucket/policies/refund.pdf" }
          }
        }
      ]
    }
  ]
}
Amazon Bedrock RetrieveAndGenerate / Knowledge Bases
{
  "output": {
    "text": "The refund policy allows returns within 30 days of purchase."
  },
  "citations": [
    {
      "generatedResponsePart": {
        "textResponsePart": {
          "text": "returns within 30 days of purchase",
          "span": { "start": 32, "end": 66 }
        }
      },
      "retrievedReferences": [
        {
          "content": {
            "type": "TEXT",
            "text": "Customers may return items within 30 days..."
          },
          "location": {
            "s3Location": { "uri": "s3://my-bucket/policies/refund.pdf" }
          },
          "metadata": { "x-amz-bedrock-kb-chunk-id": "abc123" }
        }
      ]
    }
  ]
}
Azure Responses API + Bing (url_citation)
{
  "type": "output_text",
  "text": "If you're searching for uplifting news from November 2025...",
  "annotations": [
    {
      "type": "url_citation",
      "start_index": 333,
      "end_index": 564,
      "url": "https://example.com/article",
      "title": "Example Article"
    }
  ]
}
Azure On Your Data (Azure AI Search)
{
  "role": "assistant",
  "content": "Contoso's Q3 revenue grew 12% year-over-year [doc1].",
  "context": {
    "citations": [
      {
        "content": "Q3 revenue was $4.2B, up 12% YoY...",
        "title": "Q3 Earnings Summary",
        "filepath": "reports/q3-2025.pdf",
        "url": "https://contoso.sharepoint.com/...",
        "chunk_id": "chunk-42"
      }
    ]
  }
}

Other examples of proposals to formalize citations

Agent Stack (formerly BeeAI) has an extension to the A2A protocol for citations, further highlighting the desire for standardization.

It uses this simple format:

    start_index: int | None = None
    end_index: int | None = None
    url: str | None = None
    title: str | None = None
    description: str | None = None

Specification

Tool results MAY include a citations field at the top level of the tool result, referenced from the content field by markdown link labels or text. Citation objects MUST conform to the MCP citation JSON-LD schema (or a derivative schema). If @context is not specified and the response includes citations clients MUST assume the format is citations.jsonld.

{
  "content": [ ... ],
  "citations": [
    {
      "id": "c1",
      "@context": "https://modelcontextprotocol.io/ns/citations.jsonld",
      ...
    }
  ]
}

Note that structuredContent MAY contain citations in this format, but this spec does not explicitly mandate that clients understand or respect the citations returned.

Binding citations to content

Servers bind citations to specific claims with a markdown link in the text, following CommonMark's link spec. Clients that do not support MCP citations often still understand and render markdown formatted links.

Note that not all markdown links imply that they are citations.

Servers will frequently emit reference-style links, as an LLM outputs the link label as a marker in a single pass generation. The link label with a colon elsewhere in the text pairs that marker with a destination and an optional title ([c1]: url "title"). CommonMark requires the title to be quoted; an unquoted title invalidates the whole definition, leaving the marker as literal text and printing the definition line to the user. Servers are RECOMMENDED to use reference-style links for citations, as it provides a unique marker.

It is emphatically the province and duty of the judicial department to say what the law is [c1].

...

[c1]: https://www.law.cornell.edu/supremecourt/text/5/137 "Marbury v. Madison, 5 U.S. (1 Cranch) 137 (1803)"

In this case, the reference-style marker c1 can be a key to the citations array.

Here is the same reference-style link, but with the title directly as the link text (Marbury v. Madison, 5 U.S. (1 Cranch) 137 (1803)).

It is emphatically the province and duty of the judicial department to say what the law is [Marbury v. Madison, 5 U.S. (1 Cranch) 137 (1803)][c1].

...

[c1]: https://www.law.cornell.edu/supremecourt/text/5/137

An inline link just contains the link text and destination.

It is emphatically the province and duty of the judicial department to say what the law is [Marbury v. Madison, 5 U.S. (1 Cranch) 137 (1803)](https://www.law.cornell.edu/supremecourt/text/5/137).

In this case, the link text is the key to the citations array: clients match it against the citation's name. A server using inline links SHOULD therefore set name to exactly the link text, and SHOULD ensure either that inline link text is unique, or that duplicates carry the same citation metadata.

Servers MAY key elements in the citations array based on link label (RECOMMENDED) or link text. When information in a markdown link does not match the information in citations, clients SHOULD prefer the information in citations.


{
  "content": [
    {
      "type": "text",
      "text": "It is emphatically the province and duty of the judicial department to say what the law is [1].\n\n...\n\n[1]: https://www.law.cornell.edu/supremecourt/text/5/137 \"Marbury v. Madison, 5 U.S. (1 Cranch) 137 (1803)\""
    }
  ],
  "citations": [
    {
      "id": "1",
      ...
    }
  ]
}

In this case, the citations array annotates [1] as a citation that should be rendered as such to the user.

Block-level citations

Servers MAY include citations that are not keyed by a marker in the text. Those citations are understood to apply to the returned content block(s) as a whole. Block-level citations do not require an id. Resources that cannot embed citation markers (e.g. image or audio) will always use block-level citations.

{
  "content": [
    {
      "type": "text",
      "text": "It is emphatically the province and duty of the judicial department to say what the law is."
    }
  ],
  "citations": [
    {
      ...
    }
  ]
}

Capability negotiation

Citations increase payload size and server-side work. Clients SHOULD advertise whether they render citations so servers can omit sending deeper metadata when they would not be used.

Client capabilities

Clients declare citation support in per-request io.modelcontextprotocol/clientCapabilities. The field is present on every client request (including tools/call and server/discover).

{
  "citations": {
    "render": {}
  }
}
Field Meaning
citations Client understands MCP citations array on tool results.
citations.render Client surfaces citations to the user (e.g. citation chips, hovercards, clickable links).

An empty render object indicates support with no additional settings. Additional sub-fields MAY be defined in future revisions.

Servers SHOULD omit the top-level citations array on tool results when the client did not declare any support for citations.

Clients that declared support for citations without citations.render MAY still pass them to an LLM as opaque, metadata.

Client rendering

When tool results contain citations, clients that declared citations.render SHOULD attempt to surface citations to the user. This SEP does not mandate a format for rendering citations, but a common method for graphical clients is to show citation chips with rich hovercards (title, description, thumbnail from target.schema:citation), with clickable links to the cited work. Clients SHOULD prefer target.schema:citation.schema:url when present (the cited work as a whole); otherwise they MAY fall back to target.source. Clients MAY use target.source instead when opening a deep link to the specific span (URL fragment or selector) the citation supports.

Clients MAY synthesize content returned in tool calls, but SHOULD still preserve citations in their synthesis as relevant.

Citation open behavior (embedded vs external)

Servers MAY return a hint in the display field suggesting the client display citations in a sandboxed in-app webview or to open an external browser.

Clients with the appropriate capabilities MAY honor this hint, but are not required to do so.

Opening citations within a web view allows supporting richer rendering of authoritative documents (e.g. spreadsheets), without needing to implement an MCP app.

MCP Citation JSON-LD schema (extension of schema.org CreativeWork)

MCP citations are JSON-LD objects that describe the cited work as a schema.org CreativeWork — the same kind of node that schema.org's citation property points at. name, description, url, text, thumbnailUrl, datePublished, author, and publisher are therefore ordinary schema.org properties carrying their usual meanings, and a server MAY narrow the citation to a more specific subtype such as WebPage, ScholarlyArticle, or ImageObject via type. Markdown links bind a citation to the span of the tool result it supports. See the Rationale section for alternatives considered, including the W3C Web Annotation Data Model and more granular selection within the tool response.

The MCP citation schema layers three extensions on top of schema.org:

  1. An id carrying the markdown link label that binds the citation to a span of the content. Because this is a local key within the tool result rather than an IRI naming the cited work, the context maps id to mcp:id, overriding schema.org's own aliasing of id to @id.
  2. A copy of MCP resources' mcp:annotations field to hint to clients whether to render a citation or simply use it as context for further investigation.
  3. A display hint for whether to open a citation in an embedded webview or an external browser.

The JSON-LD context is canonically published at https://modelcontextprotocol.io/ns/citations.jsonld and referenced by the @context in examples. It is reproduced here so this SEP remains self-contained:

{
  "@context": [
    "https://schema.org/",
    {
      "@version": 1.1,

      "mcp": "https://modelcontextprotocol.io/ns#",
      "dcterms": "http://purl.org/dc/terms/",
      "xsd": "http://www.w3.org/2001/XMLSchema#",

      "id": { "@id": "mcp:id" },

      "display": { "@id": "mcp:display" },

      "mcp:annotations": {
        "@id": "mcp:annotations",
        "@context": {
          "audience": { "@id": "mcp:audience", "@container": "@set" },
          "priority": { "@id": "mcp:priority", "@type": "xsd:decimal" },
          "lastModified": { "@id": "dcterms:modified", "@type": "xsd:dateTime" }
        }
      }
    }
  ]
}

Here is a clearer Typescript schema, illustrating a commonly used subset of the schema.org fields. Note that servers and clients may exchange more fields of schema.org's CreativeWork, or extend the protocol further. Although id, name, and url are each optional, at least one of them is required to produce a valid citation.

interface Citation {
  // Optional citation ID, potentially referring to a markdown link label, e.g. `[c1]`.
  id?: string;

  // Optional citation title, potentially referring to a markdown link text, e.g. `[name](url)`.
  // This is the suggested display title for the MCP client.
  name?: string;

  // URL to the cited source. The URL may express granularity via URL parameters. Common formats include:
  // Text fragments (subset of text): https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment/Text_fragments
  // Media fragments (subset of images, audio, or video): https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment/Media_fragments
  // PDF fragments (subset of documents): https://www.rfc-editor.org/info/rfc8118/#section-3
  // There are also vendor-specific formats such as:
  //   Excel (OOXML): #<sheet>!<cell>
  //   Google Sheets: #gid=<sheet_id>&range=<cell_or_range>
  // URLs are recommended to contain version information when possible, such as Wikipedia's oldid.
  url?: string;

  // Quoted text of the cited content, if applicable. Often useful when URL cannot granularly link to relevant text,
  // such as for a PDF or Microsoft word document.
  text?: string;

  // Short human-readable summary of the cited work, for display alongside the title.
  // Unlike `text`, this is not a quotation from the source, so a client cannot verify it against the cited content.
  description?: string;

  // Visual for the citation.
  thumbnailUrl?: string;

  // Published date of the cited content.
  datePublished?: string;

  // Author(s) of the cited content. schema.org's `author` ranges over
  // https://schema.org/Person and https://schema.org/Organization.
  author?: SchemaAgent | SchemaAgent[];

  // Publisher(s) of the cited content. schema.org's `publisher` has the same range as `author`.
  publisher?: SchemaAgent | SchemaAgent[];

  // Open-behavior hint; advisory (default "auto")
  display?: "embedded" | "external" | "auto";

  // MCP client context hints (mcp:annotations)
  "mcp:annotations"?: {
    audience?: ("user" | "assistant")[];
    priority?: number;
    lastModified?: string;
  };
}

// schema.org types an author or publisher as a Person or an Organization. A bare string is out of range but
// widely accepted in practice, so servers MAY send one.
type SchemaAgent =
  | string
  | {
      type?: "Person" | "Organization";
      name?: string;
      url?: string;
    };

Examples

Only one of id, name, or url is required for a citation. Fields the markdown link already carries are not repeated in citations; the JSON adds only what markdown cannot express.

Minimalist: reference-style marker

The marker carries no display text, and its definition already supplies the destination, so the citation needs only the id that joins it to the marker.

{
  "content": [
    {
      "type": "text",
      "text": "It is emphatically the province and duty of the judicial department to say what the law is [c1].\n\n[c1]: https://www.law.cornell.edu/supremecourt/text/5/137\n"
    }
  ],
  "citations": [
    {
      "id": "c1"
    }
  ]
}

This is the form where the title does belong in the text. The link text carries it and the definition carries the destination, so the citation again needs only its id.

{
  "content": [
    {
      "type": "text",
      "text": "It is emphatically the province and duty of the judicial department to say what the law is [Marbury v. Madison, 5 U.S. (1 Cranch) 137 (1803)][c1].\n\n[c1]: https://www.law.cornell.edu/supremecourt/text/5/137\n"
    }
  ],
  "citations": [
    {
      "id": "c1"
    }
  ]
}

An inline link has no label, so the citation is keyed by its link text in name. The destination stays in the link.

{
  "content": [
    {
      "type": "text",
      "text": "It is emphatically the province and duty of the judicial department to say what the law is [Marbury v. Madison, 5 U.S. (1 Cranch) 137 (1803)](https://www.law.cornell.edu/supremecourt/text/5/137)."
    }
  ],
  "citations": [
    {
      "name": "Marbury v. Madison, 5 U.S. (1 Cranch) 137 (1803)"
    }
  ]
}
Granular text in a PDF

A PDF fragment (#page=) reaches a page but cannot address a span of text within it, so the citation quotes the passage in text. A client can use the quote to highlight the passage, and to confirm it still appears on the page when re-fetching.

{
  "content": [
    {
      "type": "text",
      "text": "The opinion is widely treated as [the origin of judicial review][c1]."
    }
  ],
  "citations": [
    {
      "id": "c1",
      "name": "Marbury and the Origins of Judicial Review",
      "url": "https://example.org/papers/marbury-judicial-review.pdf#page=12",
      "text": "Marbury is conventionally treated as the origin of judicial review in the United States."
    }
  ]
}
Region of an image

A Media Fragment (#xywh=) addresses a rectangle within an image. Percent units are resolution-independent.

{
  "content": [
    {
      "type": "text",
      "text": "The holding is [engraved in the Supreme Court building][c1]."
    }
  ],
  "citations": [
    {
      "id": "c1",
      "name": "Marbury v. Madison quotation, U.S. Supreme Court Building",
      "url": "https://upload.wikimedia.org/wikipedia/commons/b/b9/Marbury_v_Madison_John_Marshall_by_Swatjester.jpg#xywh=percent:4,24,58,56"
    }
  ]
}
Heading in a Word document

Word addresses a location inside a document by bookmark name, so a heading is cited through the bookmark at that heading — _Toc… when the bookmark was generated by a table of contents. The fragment reaches the heading, and the quoted text identifies the sentence beneath it.

{
  "content": [
    {
      "type": "text",
      "text": "Severance is set out in [Section 4, Termination][c1]."
    }
  ],
  "citations": [
    {
      "id": "c1",
      "name": "Employee Handbook",
      "url": "https://contoso.sharepoint.com/sites/hr/Shared%20Documents/Employee-Handbook.docx#Section_4_Termination",
      "text": "An employee terminated without cause receives four weeks of severance for each year of service."
    }
  ]
}
Cell in an Excel spreadsheet

Excel addresses a cell as #<sheet>!<cell>, the form Microsoft documents for links in Excel. The fragment reaches the cell, and text carries its value so a client can show the figure without opening the workbook.

{
  "content": [
    {
      "type": "text",
      "text": "Revenue is forecast at [$1,284,500 for FY25][c1]."
    }
  ],
  "citations": [
    {
      "id": "c1",
      "name": "FY25 Forecast",
      "url": "https://contoso.sharepoint.com/sites/finance/Shared%20Documents/FY25-Forecast.xlsx#Revenue!B7",
      "text": "1284500"
    }
  ]
}
Hovercard metadata

A client rendering a hovercard has room for more than a title. description summarizes the cited work in the server's own words; unlike text it is not a quotation from the source, so it is display metadata rather than something a client can check against the resource.

{
  "content": [
    {
      "type": "text",
      "text": "Judicial review in the United States traces to [an 1803 decision of the Marshall Court][c1]."
    }
  ],
  "citations": [
    {
      "id": "c1",
      "name": "Marbury v. Madison, 5 U.S. (1 Cranch) 137 (1803)",
      "description": "The Supreme Court decision that established the federal judiciary's power to strike down laws repugnant to the Constitution.",
      "url": "https://www.law.cornell.edu/supremecourt/text/5/137",
      "thumbnailUrl": "https://upload.wikimedia.org/wikipedia/commons/b/b9/Marbury_v_Madison_John_Marshall_by_Swatjester.jpg"
    }
  ]
}

Rationale

Standard vs extension

Citations are core to verifying AI outputs in professional work. However, if the maintainers believe that citations should be an extension, we can make that change.

Clients SHOULD vs MUST vs MAY render citations

While citations are an important part of professional work, some clients will not need to provide citations. For example:

  1. MCP clients that do not show output to end users (e.g. M2M usage of MCP).
  2. Narrow-purpose MCP clients, e.g. ones that primarily consume structured output
  3. Coding-specific MCP clients, such as Claude Code or Cursor. They typically use diffs in place of citations.

However, general purpose clients should still be encouraged to show citations. MAY is not strong enough language.

Annotate citations using an inline format such as pandoc's [@referenceId]

Pandoc has a citation syntax within markdown links, e.g. [@marbury1803], or [@marbury1803, p. 137]. There's a clear separation between regular links and citations with the @ in front. This means that "minimalist" citations could avoid even creating a citations array.

However, supporting arbitrary markdown links is an easier onboarding path for existing server implementations, including support for selecting within the body.

Why not include start/end markers?

Most of the examples of web search APIs (Anthropic, OpenAI, etc) return offsets within a wider work where a text quote was pulled. However, this SEP recommends using web text fragment URLs, which more directly help clients render the citation.

Offsets are less helpful for other file formats such as Excel or Word, and the SEP recommends using URLs to encode this information. URLs make it easier for humans to jump to and verify a citation.

However, if we do want to keep start/end offsets, we can add it to the JSON-LD for MCP citations.

schema.org citations vs Web Annotation Data Model

schema.org's citations are very widely used for SEO purposes. However, it is primarily designed to provide context to search engines. It has a relatively verbose and nested structure for citations.

The Web Annotation Data Model is primarily used in academic settings, but has richer support for granular selectors — such as a polygon on an image — that a URL fragment cannot express, and it keeps that structure in typed JSON rather than requiring clients to parse every convention out of a URL. It also separates the concept of Body (the portion of the tool result making the claim) and Target (the cited resource).

WADM's granular target selection would also allow for very token efficient verification of cited sources, e.g. only reading an element within a slide. However, this would require clients and servers to agree on formats for extreme granularity.

In the end, this SEP proposes using Markdown links rather than WADM's Body for easy adoption. And similarly, schema.org's citation format as it is already commonly used for hovercards.

Backward Compatibility

Clients that do not understand citations at all or have not implemented certain citation fields can safely ignore citation metadata. All fields in this proposal are additive only, and clients are not required to render any or all citations.

While clients may generate their own citations, this SEP does not provide a mechanism for them to directly pass them to servers. So older servers will not be affected.

Reference Implementation

I plan to open a PR to OpenWebUI as a reference implementation. That can happen in parallel with discussion on this SEP.

As discussed above, there are many bespoke implementations of rich citations, so there is evidence that the pattern is useful.

Security Implications

Access control for non-public resources

Servers should implement access controls for non-public resources.

Citation hallucination

Servers often use LLMs to generate tool responses, which can lead to hallucinated quotes or citations. And, malicious servers may use citations to make links look authoritative to encourage users to click.

This SEP only suggests clients MAY verify citations before rendering, but most clients will likely not spend the extra network bandwidth and/or tokens to do so.

Prompt injection via cited content

Cited content displayed inline could itself contain prompt injection payloads if a user later quotes the citation back to the model. This is the same risk surface as any other tool result.

Dereferencing citation URIs (SSRF)

Clients dereference server-chosen URIs when rendering citations: url on hover, on click, or when re-resolving a citation, and thumbnailUrl when loading hovercard images. Every such fetch is an outbound request to a URI the server chose. A malicious server can therefore point these fields at internal or local endpoints (file://, http://localhost, link-local addresses, cloud-metadata IPs such as 169.254.169.254), turning the client into an SSRF vector. Clients SHOULD restrict automatic dereferencing to an allow-list of schemes (typically https:// and MCP-resolved custom schemes via resources/read) and MUST NOT fetch private, loopback, or link-local addresses without explicit user action.

Clients MUST NOT attach ambient credentials (cookies, cached auth, or MCP access tokens) when fetching a citation target cross-origin; as noted above, MCP tokens are bound to their server and MUST NOT be replayed to arbitrary targets. Clients dereferencing https:// targets MUST validate TLS and refuse downgraded or mixed-content fetches, so a network attacker cannot forge the "verified source" a citation appears to confirm.

Rendering untrusted preview content

The display: "embedded" hint asks the client to open a citation in its in-app webview rather than an external browser. Because an embedded surface shares more of the client's context, an embedded hint MUST raise the sandboxing bar, not lower it: the same script-disabling, credential-omission, and no-referrer rules apply, and clients SHOULD isolate the webview from the host session. The hint is advisory — clients MUST fall back to an external window when the cited origin forbids framing (X-Frame-Options / CSP frame-ancestors) or the scheme is not web-fetchable.

Conversely, a cited origin may not wish its content re-surfaced as a preview inside arbitrary clients. MCP has no embedding-consent signal today (the web analog is CSP frame-ancestors), and providing one would require a server-declared field rather than client-side policy.

Content integrity

An earlier version of this proposal included a sha256 field for clients to verify whether content had changed from when the citation was generated. However, the sha256 field is not widely used, and it is also hard to know exactly what the sha256 is signing: e.g. is it signing the granular content (such as a text snippet) or the wider content.

This SEP punts on a decision to add integrity checks for the future as we see more adoption.

Because clients MAY re-verify citations against the live source or fetch url and thumbnailUrl, a response containing many citations can generate a burst of outbound requests to attacker-chosen URLs — a denial-of-service-by-proxy and tracking-beacon vector against third-party origins. Clients SHOULD rate-limit automatic dereferencing and MAY defer it until a user gesture (e.g. hovering a specific citation).

Open questions

Should server and clients advertise which citation formats/features?

This SEP defines baseline citations / citations.render capability flags (see Capability negotiation). It does not yet specify how clients and servers exchange support for individual fragment schemes (text fragments, media fragments, PDF #page=, and the vendor bookmark and cell forms).

Similarly, servers could advertise supported citation formats in the _meta response. That would provide a helpful hint for clients who actively generate citations.

How reliably do models retain citation markers when synthesizing tool responses?

This SEP recommends that MCP clients keep citations even when synthesizing a tool response to retain human verifiability of information. However, we will need to see empirical data on how well models retain citations through prompting alone, or whether models will need to be fine-tuned to retain citations.

Should we explicitly constrain to a subset of schema.org's fields?

schema.org technically allows nested citations and a myriad of fields through its hierarchy of classes, e.g.:

{
  "@context": "https://schema.org/",
  "@type": "DigitalDocument",
  "@id": "https://example.com",
  "name": "Annual Geological Analysis 2026",
  "encodingFormat": "application/pdf",
  "url": "https://example.com",
  "hasPart": {
    "@type": "ImageObject",
    "@id": "https://example.com#page=14&img=01",
    "name": "Satellite Mapping of Sector 7",
    "description": "High-resolution satellite topography view on page 14.",
    "embeddedTextCaption": "Figure 4.2: Sector 7 Topography Overview.",
    "thumbnailUrl": "https://example.com/thumbs/sector-7.png",
    "hasPart": {
      "@type": "ImageObject",
      "@id": "https://example.com#page=14&img=01&xywh=percent:42,15,48,21",
      "name": "Fault Line Delta-9",
      "description": "The region of the figure highlighting the active fault line displacement."
    }
  }
}

Similar to search engines, MCP clients will likely support a narrow portion of the spec, likely just enough to render a clickable hovercard. We may want to narrow the spec to just common fields, or just detach from schema.org entirely.

Further security analysis

HTTP and web browsers have put decades of thought into sandboxing, iframes, CORS, and XSRF. We should do a more detailed audit of security implications.

Acknowledgements

Appreciate the feedback from Robert Sanderson, one of the original authors of the Web Annotation Data Model.

Will add more reviewers and collaborators in here throughout the process!

SEP-3118 App-Rendered Elicitations for MCP Apps

Accepted · Extensions Track · Created 2026-07-22

Imported from upstream. Proposed as modelcontextprotocol#3118 by @krubenok on 2026-07-23, 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 — accepted. The document carries every required section, and a prototype a reviewer can run is cited at https://github.com/modelcontextprotocol/csharp-sdk/pull/1723. Conformance tests and the reference documentation chapter are still required before Final.

Abstract

This SEP extends MCP Apps so an MCP server can offer an app resource as the custom user interface for a core form elicitation. The elicitation remains a standard elicitation/create request with a complete requestedSchema; an optional _meta.ui.resourceUri identifies the MCP App that can render it.

Support is negotiated as an additive capability of the existing io.modelcontextprotocol/ui extension. A client that supports core form elicitation but not app-rendered elicitation receives the ordinary form request unchanged. A capable host loads the referenced app from the originating server, binds that app instance to the elicitation, and forwards the standard request over the MCP Apps bridge. The app returns a standard ElicitResult, which the host validates and uses as the corresponding MRTR inputResponses entry when retrying the originating request.

The same convention applies to legacy stateful elicitation/create exchanges and to InputRequiredResult multi-round-trip requests (MRTR) in protocol revision 2026-07-28. It does not introduce another elicitation mode or another extension identifier.

The MCP Apps Working Group and the maintainers of the ext-apps repository would own the resulting extension specification.

Motivation

Core form elicitation deliberately uses a constrained schema so any supporting client can render a safe, interoperable native form. That is the right fallback, but it cannot express interfaces such as visual option comparison, interactive previews, maps, calendars, or domain-specific review experiences.

MCP Apps can already provide those interfaces for tool results. It does not currently define how a server associates an app with a particular elicitation, how a host selects the correct app instance, or how the elicitation request and response cross the app bridge. Implementations therefore face several incompatible choices:

  • replace elicitation with an app-specific tool flow, losing the standard elicitation lifecycle;
  • forward the request to whichever app bridge happens to be active, which is ambiguous when multiple apps exist;
  • define a second extension solely for app-rendered elicitation, despite the feature requiring MCP Apps;
  • send app metadata without negotiation, leaving clients unsure whether it is optional.

The gap is more visible under the 2026-07-28 protocol revision. MRTR makes elicitation work on stateless transports by returning an InputRequiredResult and retrying the original request with inputResponses. An app-rendering convention should preserve that transport-independent semantic request rather than reintroducing a session dependency.

The desired result is progressive enhancement: one semantic form elicitation, a richer renderer when all parties support it, and a native form everywhere else.

Specification

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 when, and only when, they appear in all capitals.

Scope and ownership

This proposal modifies the existing MCP Apps extension identified by io.modelcontextprotocol/ui. It does not define a new extension identifier. The normative text resulting from this SEP belongs in the ext-apps specification and remains under the governance of the MCP Apps Working Group.

This proposal applies only to core form elicitations. URL-mode elicitations are intentionally excluded because their security boundary requires the sensitive interaction to occur outside the MCP client.

Client-to-server capability negotiation

A client that can render core form elicitations with MCP Apps MUST declare both:

  1. the core elicitation.form capability; and
  2. an elicitation member in its MCP Apps extension settings.

For protocol revision 2026-07-28, these capabilities are supplied in _meta["io.modelcontextprotocol/clientCapabilities"] on the originating request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "assign_account_manager",
    "arguments": {},
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {
        "elicitation": {
          "form": {}
        },
        "extensions": {
          "io.modelcontextprotocol/ui": {
            "mimeTypes": ["text/html;profile=mcp-app"],
            "elicitation": {}
          }
        }
      }
    }
  }
}

For protocol revisions that negotiate capabilities during initialization, the same capability object is used in ClientCapabilities.

The elicitation settings object is reserved for compatible future additions. An empty object indicates support for this SEP.

A server MUST NOT add the app-rendering hint unless the request's effective client capabilities include core form elicitation, MCP Apps with a supported app MIME type, and the MCP Apps elicitation member. On stateful transports, the effective capabilities are those negotiated for the session. On 2026-07-28 stateless transports, the request-scoped capabilities take precedence.

Server-to-client capability negotiation

A server that may attach an MCP App to an elicitation MUST advertise the same nested setting in its MCP Apps extension capabilities. In protocol revision 2026-07-28, the server advertises these capabilities through server/discover:

{
  "jsonrpc": "2.0",
  "id": "discover-1",
  "result": {
    "resultType": "complete",
    "supportedVersions": ["2026-07-28"],
    "capabilities": {
      "extensions": {
        "io.modelcontextprotocol/ui": {
          "elicitation": {}
        }
      }
    }
  }
}

For protocol revisions that use the initialize handshake, the server places the same setting in InitializeResult.capabilities.

App-rendered elicitation is negotiated only when the effective client capabilities and the discovered or initialized server capabilities both contain the nested setting.

Elicitation metadata

When app-rendered elicitation is negotiated, a server MAY add the following metadata to a form elicitation/create request:

{
  "method": "elicitation/create",
  "params": {
    "mode": "form",
    "message": "Review the portfolio and confirm an account manager.",
    "requestedSchema": {
      "type": "object",
      "properties": {
        "confirmed": {
          "type": "boolean",
          "title": "Confirm assignment"
        }
      },
      "required": ["confirmed"]
    },
    "_meta": {
      "ui": {
        "resourceUri": "ui://portfolio/assign-manager"
      }
    }
  }
}

_meta.ui.resourceUri has the same syntax and resource-resolution semantics as the existing MCP Apps tool metadata member:

  • it MUST be an absolute ui:// URI;
  • it MUST identify an MCP App resource served by the MCP server that originated the elicitation;
  • the resource MUST use a MIME type negotiated by the client, initially text/html;profile=mcp-app.

The server MUST include a complete, independently renderable core requestedSchema. The app metadata is a presentation hint and MUST NOT alter the meaning, validation rules, or response shape of the elicitation.

The ui metadata object is inherited from MCP Apps. Other MCP Apps metadata fields may be added in the future without changing the core elicitation schema.

App and host bridge capabilities

MCP Apps uses ui/initialize to negotiate the capabilities of a particular app instance and host. This SEP adds an elicitation member to both McpUiAppCapabilities and HostCapabilities:

interface McpUiAppCapabilities {
  // Existing members omitted.
  elicitation?: {};
}

interface HostCapabilities {
  // Existing members omitted.
  elicitation?: {};
}

An app declares appCapabilities.elicitation when it can receive and answer a forwarded elicitation/create request. A host declares hostCapabilities.elicitation when it can forward the request to the bound app and accept the standard response.

The host MUST NOT forward an elicitation until both members have been negotiated for that app instance. An app that declares this capability MUST handle standard form elicitation/create requests and return a standard ElicitResult.

Host rendering and routing

When a host processes an MRTR InputRequiredResult containing a form elicitation with _meta.ui.resourceUri, it:

  1. MUST verify that the client-to-server capability was negotiated;
  2. MUST resolve the resource from the server that originated the elicitation using the existing MCP Apps resource-loading rules;
  3. MUST create or select an app instance explicitly bound to this elicitation and resource;
  4. MUST complete the MCP Apps ui/initialize handshake;
  5. MAY forward the unchanged elicitation/create request to the app if both bridge capabilities were negotiated;
  6. MUST validate an accepted response's content against requestedSchema; and
  7. MUST place the resulting standard ElicitResult under the corresponding key in inputResponses and retry the originating request with the server-provided requestState, if any.

The host MUST NOT route an elicitation to an app merely because its bridge is currently active. The binding is scoped to the originating server, elicitation request, resource URI, and app instance. An implementation MAY reuse an already loaded instance only when that complete binding is preserved.

The host SHOULD tear down an app instance created solely for an elicitation after acceptance, decline, cancellation, timeout, or request cancellation. Sequential and concurrent elicitations are supported; concurrent requests require distinct bindings even if an implementation reuses the same underlying app resource.

The app's response action (accept, decline, or cancel) retains its core elicitation meaning. The app MUST NOT directly retry the server operation or bypass the host's response handling.

Native fallback

App rendering is optional progressive enhancement.

If the client supports core form elicitation but does not declare MCP Apps elicitation support, the server MUST omit _meta.ui.resourceUri and send the ordinary form elicitation.

If an app resource cannot be loaded, the app fails initialization, the app does not declare its bridge capability, or the host otherwise cannot use the app, the host SHOULD render the same requestedSchema with its native form UI. It SHOULD NOT convert the failure into decline or leave the request pending indefinitely.

A host that encounters app metadata it does not understand MUST ignore the metadata according to the core _meta extensibility rules and process the form elicitation normally.

Servers that require semantics not expressible by requestedSchema are not compatible with this fallback contract and MUST NOT present those semantics as an app-rendered form elicitation.

MRTR and transport behavior

This proposal does not change the MRTR wire format. In protocol revision 2026-07-28, the elicitation/create input request, including optional MCP Apps metadata, appears inside the inputRequests map of an InputRequiredResult:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "manager-assignment": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "Review the portfolio and confirm an account manager.",
          "requestedSchema": {
            "type": "object",
            "properties": {
              "confirmed": {
                "type": "boolean"
              }
            },
            "required": ["confirmed"]
          },
          "_meta": {
            "ui": {
              "resourceUri": "ui://portfolio/assign-manager"
            }
          }
        }
      }
    },
    "requestState": "assign-account-manager:v1"
  }
}

After collecting the app or native form response, the client retries the original request with the standard inputResponses and requestState:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "assign_account_manager",
    "arguments": {},
    "inputResponses": {
      "manager-assignment": {
        "action": "accept",
        "content": {
          "confirmed": true
        }
      }
    },
    "requestState": "assign-account-manager:v1",
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {
        "elicitation": {
          "form": {}
        },
        "extensions": {
          "io.modelcontextprotocol/ui": {
            "mimeTypes": ["text/html;profile=mcp-app"],
            "elicitation": {}
          }
        }
      }
    }
  }
}

The server MUST process the retry identically regardless of which renderer collected the response.

On legacy stateful transports, the same semantic request may be delivered as a direct server-to-client elicitation/create request. No app-rendering state may be assumed to survive on the server between MRTR rounds.

Rationale

Extend MCP Apps instead of defining a dependent extension

App-rendered elicitation has no meaning without MCP Apps. The current extension framework does not define extension dependency semantics, version constraints, or profiles. A second extension would therefore duplicate negotiation rules while being unable to formally require io.modelcontextprotocol/ui.

Adding a nested capability to MCP Apps keeps governance, bridge behavior, resource loading, and security policy in the extension that owns them. The separate io.modelcontextprotocol/ui-elicitation identifier used by the early prototype remains useful for private experimentation but is not proposed for standardization.

Preserve a complete form schema

Treating the app as a renderer rather than a new elicitation mode gives every form-capable client a working path and keeps validation authoritative at the host. It also prevents a server from using custom UI to smuggle a response shape that cannot be understood by non-app clients.

Use an explicit resource binding

Forwarding elicitation requests over the currently active app bridge, as explored in ext-apps PR #531, is attractive for already-rendered apps but does not determine which app should handle a new elicitation. An explicit resourceUri works when no app is active, supports prefetch and review, and removes ambiguity when several apps are open.

This proposal is compatible with bridge forwarding once the resource-bound instance is selected.

Reuse the standard request and response

The app does not need a custom submission method. Receiving elicitation/create and returning ElicitResult preserves action semantics, cancellation, schema validation, SDK types, and MRTR retry behavior. It also follows MCP Apps' existing practice of reusing standard MCP messages over the app-host bridge.

Backward Compatibility

This proposal is additive.

  • Clients without MCP Apps continue to receive ordinary form elicitations.
  • MCP Apps clients that do not implement this proposal do not advertise the nested capability and continue to use native forms.
  • Servers that do not implement this proposal send unchanged core elicitation requests.
  • Unknown _meta fields remain ignorable.
  • The request and response schemas remain those of core elicitation.

Implementations must not infer support from the presence of MCP Apps alone. Requiring the nested capability prevents an older Apps host from accidentally receiving a request it cannot route.

Security Implications

An app-rendered elicitation processes server-provided code and user-provided data at a trust boundary. All existing MCP Apps sandbox, Content Security Policy, permission, and auditing requirements continue to apply.

In addition:

  • The host MUST fetch the resource from the originating MCP server and MUST NOT allow the metadata to select an app from another server connection.
  • The host MUST bind the app instance to the originating server, elicitation request, and resource URI to prevent confused-deputy routing.
  • The host MUST treat the app as untrusted and MUST validate accepted content against requestedSchema outside the app sandbox.
  • The app MUST NOT receive capabilities or server access beyond those negotiated through MCP Apps.
  • Hosts SHOULD make the identity of the originating server visible to the user and preserve their normal consent and cancellation controls.
  • Hosts and servers SHOULD avoid placing secrets in form-mode elicitation. Sensitive credential, payment, and third-party authorization flows should use URL-mode elicitation and its separate trust boundary.
  • Implementations SHOULD apply timeouts and release bound app instances when the elicitation or originating request is cancelled.

App rendering does not make the app's validation authoritative and does not grant the app permission to call server tools. Any app-initiated tool call remains subject to the existing MCP Apps capabilities and host approval policy.

Reference Implementation

A draft implementation is available in modelcontextprotocol/csharp-sdk PR #1723. It builds on the C# SDK 2.0.0 preview and includes:

  • experimental, strongly typed MCP Apps capability and metadata APIs;
  • request-scoped capability checks for protocol revision 2026-07-28;
  • stateless MRTR request and typed retry helpers;
  • a minimal MCP server, host, and app sample;
  • tests proving both app-enhanced and native form fallback paths.

The implementation is marked experimental using the SDK's MCP Apps diagnostic while this SEP is under discussion.

For prototype users that need independently negotiated deployment before this proposal is adopted, the earlier io.modelcontextprotocol/ui-elicitation branch is retained. It is not the proposed final protocol design.

SEP-3149 Require `token_endpoint_auth_methods_supported` in Client ID Metadata Documents

Draft · Standards Track · Created 2026-07-27

Imported from upstream. Proposed as modelcontextprotocol#3149 by @max-stytch on 2026-07-28, 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 makes the token_endpoint_auth_methods_supported metadata parameter a required property of MCP Client ID Metadata Documents (CIMD), and creates a deprecation path for the singular token_endpoint_auth_method parameter, mirroring the guidance in OpenID Connect RP Metadata Choices 1.0. This enables a CIMD document to be published once and consumed by many authorization servers with differing capabilities. It also deprecates the implicit public-client default that applies when a document declares no authentication method at all. Both deprecations point at the same end state: every CIMD document declares its authentication methods explicitly in token_endpoint_auth_methods_supported. This SEP is agnostic about which authentication methods a given client advertises. It specifies the shape and handling of the field, not its contents.

Motivation

MCP's Client ID Metadata Document mechanism, introduced in SEP-991, carried over the singular token_endpoint_auth_method from RFC 7591 client metadata. It appears in the specification today only in an example document, never in normative prose.

A CIMD document is published at a single URL and consumed by every authorization server the client ever connects to, and those servers do not have uniform capabilities. A single-valued token_endpoint_auth_method gives a client no way to express "any of these, depending on what you support."

The broader OAuth/OIDC ecosystem has already solved this capability negotiation problem by moving from single-valued to multi-valued parameters. RP Metadata Choices 1.0 supplements token_endpoint_auth_method with token_endpoint_auth_methods_supported, and RFC 8414 uses that same name for the mirror-image capability on the authorization server. Adopting it in CIMD documents brings MCP in line with both.

Specification

CIMD Document Requirements

MCP clients publishing a Client ID Metadata Document MUST include the token_endpoint_auth_methods_supported metadata parameter. Its value is a JSON array of strings drawn from the OAuth Token Endpoint Authentication Methods registry established by RFC 7591 Section 4.2.

This extends the metadata document's required-property list in Client Registration, which currently reads:

The metadata document MUST include at least the following properties: client_id, client_name, redirect_uris

The array MUST contain one or more authentication methods the client supports, in no particular order of preference. The restrictions on token endpoint authentication methods in the Client ID Metadata Document specification continue to apply: the array MUST NOT include client_secret_post, client_secret_basic, client_secret_jwt, or any other method based around a shared symmetric secret.

Authorization servers MUST support reading token_endpoint_auth_methods_supported from a CIMD document. Documents that omit it are handled as described in Deprecated Declarations.

For illustration, a client that supports two registered methods might publish:

"token_endpoint_auth_methods_supported": ["none", "private_key_jwt"]

Method Resolution

The client and authorization server determine the set of mutually supported token endpoint client authentication methods by intersecting the client's token_endpoint_auth_methods_supported array with the authorization server's token_endpoint_auth_methods_supported metadata value.

  • The client MUST select and use a method from the intersection.
  • When the client's singular token_endpoint_auth_method is present and mutually supported, the client SHOULD use that method to preserve compatibility with authorization servers that treat the singular field as binding.
  • When the singular method is absent or unsupported, the client MAY select another method from the intersection.
  • After reading token_endpoint_auth_methods_supported from the CIMD document, an authorization server SHOULD accept any method in the intersection, unless its local security policy disallows that method for the client.
  • If the intersection is empty, the client MUST NOT proceed, and the authorization server MUST reject the client.
  • Authorization servers MUST reject any method outside the intersection.

For example:

Client methods Authorization server methods Result
["private_key_jwt", "none"] ["private_key_jwt", "none"] Client chooses a shared method
["private_key_jwt", "none"] ["none"] Use none
["none"] ["private_key_jwt", "none"] Use none
["private_key_jwt", "none"] ["client_secret_basic"] No compatible method

Deprecated Declarations

Two existing declarations are deprecated but remain supported during the transition to token_endpoint_auth_methods_supported:

  1. The singular token_endpoint_auth_method parameter, retained for backwards compatibility with authorization servers that do not support the array.
  2. Declaring no authentication method at all, retained for backwards compatibility with documents published before the array was required.
  • Clients MUST publish token_endpoint_auth_methods_supported.

  • Clients SHOULD additionally publish token_endpoint_auth_method, set to their preferred single value from that array, for as long as the field remains deprecated rather than removed. This follows OpenID Connect RP Metadata Choices 1.0, Section 2:

    To facilitate interoperability with implementations not supporting this specification, deployments SHOULD include the single-valued metadata parameter alongside the corresponding multi-valued metadata parameter with their preferred single value.

  • When both parameters are present, token_endpoint_auth_method MUST be a member of token_endpoint_auth_methods_supported. The array is authoritative; the singular value is the client's backwards-compatible preference when it is mutually supported.

  • Authorization servers MUST NOT reject a client solely because the singular preferred method is unsupported when another method in the array is mutually supported.

  • Authorization servers MAY fall back to token_endpoint_auth_method when the multi-valued parameter is absent — for example, when reading a document published before this requirement took effect.

  • Authorization servers MUST treat a document carrying neither parameter as declaring ["none"] until the deprecation period elapses, and MUST reject such a document thereafter.

The following examples show how the singular field behaves during the deprecation period:

Client methods Singular preference Authorization server methods Result
["private_key_jwt", "none"] private_key_jwt ["private_key_jwt", "none"] Use preferred private_key_jwt
["private_key_jwt", "none"] private_key_jwt ["none"] Try none
["private_key_jwt", "none"] none ["private_key_jwt", "none"] Use preferred none
["none"] private_key_jwt ["none"] Reject invalid metadata
Omitted none ["none"] Use legacy none
Omitted Omitted ["none"] Use deprecated none
Omitted Omitted ["private_key_jwt"] Reject incompatible client

Lifecycle

Per the feature lifecycle policy, this SEP proposes the following deprecation entries:

Feature Deprecation SEP Deprecated in Migration path Earliest removal
token_endpoint_auth_method SEP-3149 (revision on merge) token_endpoint_auth_methods_supported First revision released on or after one year from deprecation
Implicit none for documents declaring no method SEP-3149 (revision on merge) token_endpoint_auth_methods_supported First revision released on or after one year from deprecation

Rationale

token_endpoint_auth_methods_supported was chosen over inventing an MCP-specific field name because it is already the standard name for this capability within OAuth/OpenID ecosystems.

This SEP intentionally does not recommend a specific set of authentication methods. Which methods a client can support is determined by its deployment model, and a blanket recommendation would be wrong for some large class of clients.

The client selects a mutually supported method without requiring the authorization server to communicate a selected method back to the client. Preserving the singular preference when possible keeps existing authorization-server behavior intact.

The field is required (MUST) rather than recommended (SHOULD) because explicit behavior is crucial for authentication mechanisms. A required field gives servers a single place to look and makes conformance checkable. The cost — that existing documents must add one property — is trivial.

Alternatives considered

Making token_endpoint_auth_methods_supported optional (SHOULD)

  • Rejected. A SHOULD leaves authorization servers unable to depend on the field, so they must keep consulting the singular field indefinitely and the lowest-common-denominator problem persists for any client that skips it. Requiring the field while deprecating (not removing) the old one achieves the transition without an interoperability break.

Publishing multiple metadata documents, one per token_endpoint_auth_method

  • Rejected because it defeats the purpose of CIMD. The metadata URL is the client_id, so publishing one document per authentication method splits a single logical client into several distinct client identities. User consent records, revocation, audit logs, and rate limits would fragment across those identities, and the same application would appear to users as different clients depending on which authorization server they connected through.
  • The maintenance burden compounds the problem: every additional method multiplies the documents that must be hosted and kept in sync. Other independently varying properties, such as callback identifiers, multiply that cardinality further. New URLs also complicate authorization-server allowlists and existing registrations, while query-string variants can be rejected because Client ID Metadata Document identifiers SHOULD NOT include query strings. Multiple documents can serve as a migration fallback, but one document keeps the client's identity stable as its capabilities evolve.

Backward Compatibility

Existing CIMD documents remain accepted by conformant authorization servers during the deprecation period.

What changes. CIMD documents that omit token_endpoint_auth_methods_supported become non-conformant with that revision. Every client publishing a CIMD document must add the property, even if it only ever supported one authentication method.

What does not change, for now. Every document that is valid today continues to register successfully:

  • Documents omitting the array but declaring token_endpoint_auth_method work via the fallback above.
  • Documents declaring neither parameter are treated as ["none"], which is the behavior authorization servers already implement in practice and the posture those clients already had.
  • Authorization servers that have not yet implemented the multi-valued parameter keep working, because clients SHOULD continue publishing the singular field.

Clients do not need a separate signal indicating whether an authorization server understands the array. When the singular preferred method is supported, using it preserves compatibility with legacy authorization servers. When it is unsupported, a client can try another mutually supported method from the array: an updated authorization server can accept it, while a legacy server may still reject the client. That legacy server would already have rejected the unsupported singular method, so this fallback improves compatibility without creating a new failure case.

What changes at removal. Both accommodations are deprecated, so this is a deferred break rather than an avoided one. When the deprecation period elapses, a document that declares no authentication method is rejected instead of read as ["none"], and clients that never added the array stop registering. The window exists so that the fix — publishing one additional property — can be made on the client's own schedule rather than under an upgrade deadline.

Ordering. Because the requirement lands on clients while the fallback remains available to servers, deployments can migrate independently: a client can add the array before any given server reads it, and a server can start reading it before every client publishes it. Neither side needs to coordinate with the other.

Security Implications

This proposal does not introduce or rank authentication mechanisms. It allows a client to advertise multiple existing methods while preserving the authorization server's ability to enforce its local security policy.

Reference Implementation

ChatGPT's public Client ID Metadata Document advertises two methods and publishes one as its backwards-compatible singular preference:

{
  "token_endpoint_auth_methods_supported": ["none", "private_key_jwt"],
  "token_endpoint_auth_method": "private_key_jwt"
}

Codex advertises and prefers only its supported public-client method:

{
  "token_endpoint_auth_methods_supported": ["none"],
  "token_endpoint_auth_method": "none"
}

Both examples maintain a stable client identifier while exposing a singular preference for authorization servers that do not support the array.

A publicly runnable client/server implementation and conformance tests remain to be linked.

Open Questions

  • Should implicit ["none"] and token_endpoint_auth_method be deprecated, or should allowances be made to not break existing CIMD clients?

Acknowledgments

TBD.

SEP-3279 Negotiated tool result variants

Draft · Standards Track · Created 2026-08-18

  • Status: Draft
  • Type: Standards Track
  • Created: 2026-08-18
  • Author(s): Kyle Rubenok (@krubenok)
  • PR: SEP-3279
  • Target revision: 2026-12-01
  • Related: SEP-2200

Imported from upstream. Proposed as modelcontextprotocol#3279 by @krubenok on 2026-08-19, 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 adds negotiated, audience-directed variants to tool results. A tool declares the result representations it can produce. A client selects the representations needed for a particular tools/call request. The server returns exactly those representations, and the host routes each one according to its declared audience.

The core protocol defines a small set of audience and format combinations based on current MCP usage: content or structured data for an assistant, content or structured data presented to a user without automatic model inclusion, and structured data for programmatic processing. MCP extensions may define additional audiences under their extension identifiers. For example, the MCP Apps extension can define io.modelcontextprotocol/ui for data delivered to an App View.

Each audience has an independent result contract. Structured results for an assistant, a programmatic consumer, and an App View may use different schemas and contain different information. They describe the same tool execution and must not contradict one another, but they are not required to be serialized copies or lossless projections of one another.

This replaces the ambiguous relationship between CallToolResult.content and CallToolResult.structuredContent with request-time selection and explicit routing. It retains a versioned compatibility path for clients and servers using the 2026-07-28 result shape.

Motivation

The 2026-07-28 protocol defines two sibling tool result fields:

  • content, a required array of content blocks
  • structuredContent, an optional JSON value validated by Tool.outputSchema

The specification also recommends serializing structuredContent into a TextContent block for backwards compatibility. Implementations disagree about the meaning of these fields and which one to use. Some clients send only content to the model. Some replace content with structuredContent. Some send both. Others retain structured data only for application code.

This disagreement has visible costs. A survey of 18 open source clients found that six would show the model an empty or placeholder result if a server stopped mirroring structured data into content. Ten of the clients either discard the mirror or send both copies into model context. SDKs also expose different defaults for producing the mirror.

SEP-2200 tried to reduce the ambiguity by defining content as model-oriented and structuredContent as machine-oriented. Core maintainer feedback identified that approach as incomplete and deferred it in favor of fixing the underlying result shape. Changing guidance for two fields that are always returned together would still leave servers and clients to infer which behavior the other side expects. The requested direction was a polymorphic result selected by the client.

Several established scenarios now require more than a choice between text and JSON:

  • Conversational clients need concise natural language or multimodal content for model context.
  • Some clients intentionally provide structured JSON to the model.
  • Code-mode clients and orchestration layers need typed data for programmatic processing outside model context.
  • Hosts may render media or structured data for the user without adding that output to model context.
  • MCP Apps need structured data and private View metadata that can differ from both the model representation and a code-mode representation.

The current fields encode format but not destination. Treating all structured data as a single host-facing value does not solve the problem. The host contains several possible consumers, and those consumers may require different schemas, redaction, and presentation.

Goals

This SEP has the following goals:

  • Make result selection deterministic when a conforming client does not make an explicit selection.
  • Ensure the host always routes each received output to the consumer identified by its audience field, rather than inferring the destination from the output format or content.
  • Let a tool provide natural language, structured model input, programmatic data, user-visible media or structured data, and extension data without treating them as equivalent copies, duplicating model context, or sending redundant payloads over the wire.
  • Define only the core variants backed by current common scenarios.
  • Let negotiated MCP extensions add audiences for specialized consumers without expanding the core vocabulary for each one.
  • Provide a migration path for the 2026-07-28 result shape and existing SDK behavior.

This SEP does not attempt to define every future result format or presentation target.

Terminology

A result variant is one representation of a completed tool execution. It has an audience, a format, and a payload.

An audience names the consumer for which a result variant was produced. Audience is a routing contract, not a statement about which protocol process receives the bytes. The MCP client receives the complete response and routes each variant to its intended consumer.

A format describes the payload encoding defined by the core protocol. This SEP defines content and structured.

The programmatic audience identifies structured output for client-controlled program logic outside automatic model or user presentation. It covers code mode, orchestration, middleware, and typed integrations without naming a specific component inside the host.

An extension audience is a non-core audience defined by an MCP extension. The extension defines its identifier, permitted formats, routing, and handling rules.

The fallback output is the assistant/content variant returned when the client does not make an explicit output selection.

Specification

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 when, and only when, they appear in all capitals.

Result audience

The core protocol defines these audiences:

Audience Meaning
assistant The host may provide the output to the model as tool result context.
user The host may render the output to the user without automatic model use.
programmatic Client-controlled program logic consumes the output.

The core audience field MUST be one of these values or the identifier of an MCP extension that the client and server negotiated for the current request.

The existing Annotations.audience field remains a hint attached to individual content blocks. ToolResultOutput.audience is a normative routing instruction for the whole output. Clients MUST NOT infer the output audience by inspecting block annotations. If a block annotation conflicts with its containing output, the output-level audience controls routing.

Result format

The core protocol defines two formats:

  • content contains a ContentBlock[] value.
  • structured contains a JSON value. If the variant declaration has a schema, the value MUST conform to that schema unless the tool result has isError: true.

The core protocol permits these combinations:

Audience content structured Scenario
assistant Yes Yes Conversational or structured model context
user Yes Yes Host-native presentation
programmatic No Yes Code mode and programmatic orchestration

Other combinations require an MCP extension. This keeps the core set tied to current scenarios without preventing future formats or consumers.

A user/structured schema describes the data shape, not its visual layout. A client SHOULD request this variant only when it can present the declared schema using host-controlled UI. A server that requires a specific layout, custom interaction, or server-provided rendering logic should use an extension audience such as io.modelcontextprotocol/ui.

Schema types

The following TypeScript is illustrative. The protocol schema remains the source of truth.

export type CoreToolResultAudience = "assistant" | "user" | "programmatic";

/**
 * A core audience or a negotiated MCP extension identifier.
 */
export type ToolResultAudience = CoreToolResultAudience | string;

export type ToolResultFormat = "content" | "structured";

export interface ToolResultOutputSelector {
  audience: ToolResultAudience;
  format: ToolResultFormat;
}

export interface ContentToolResultVariant extends ToolResultOutputSelector {
  format: "content";
}

export interface StructuredToolResultVariant extends ToolResultOutputSelector {
  format: "structured";
  schema?: { $schema?: string; [key: string]: unknown };
}

export type ToolResultVariant =
  ContentToolResultVariant | StructuredToolResultVariant;

export interface ContentToolResultOutput extends ToolResultOutputSelector {
  format: "content";
  content: ContentBlock[];
}

export interface StructuredToolResultOutput extends ToolResultOutputSelector {
  format: "structured";
  structuredContent: unknown;
}

export type ToolResultOutput =
  ContentToolResultOutput | StructuredToolResultOutput;

Although ToolResultAudience is represented as a string for extension compatibility, arbitrary server-defined audience names are not valid. A non-core audience MUST be defined by an MCP extension, and its identifier MUST follow the extension's namespacing rules.

Tool declaration

Tool gains an outputVariants field:

export interface Tool extends BaseMetadata, Icons {
  name: string;
  description?: string;
  inputSchema: { $schema?: string; type: "object"; [key: string]: unknown };
  outputVariants: ToolResultVariant[];
  annotations?: ToolAnnotations;
  _meta?: MetaObject;
}

The following rules apply:

  • A tool MUST NOT declare the same audience and format pair more than once.
  • Every tool MUST declare an assistant/content variant.
  • The assistant/content variant MUST provide a usable model representation of the successful tool result. It MUST NOT be only a placeholder that directs the model to an output it cannot receive.
  • A tool MAY declare additional variants, including assistant/structured when it can provide suitable structured data.
  • Each variant SHOULD be tailored to its declared audience and format rather than mechanically encoding another variant.
  • A structured variant SHOULD declare a JSON Schema for its payload. It MAY omit the schema when the payload is intentionally unconstrained or a negotiated extension defines the contract elsewhere.
  • A server MUST NOT include an extension audience in outputVariants unless both the client and server advertised support for the defining extension and satisfied any audience-specific negotiation rules defined by that extension.
  • The former top-level outputSchema maps only to the legacy structuredContent field. It does not define the schema for every structured variant.
  • A client MAY decline to request a structured variant that does not declare a schema.

The per-variant schema is necessary because structured outputs for different audiences are independent contracts. A tool may use the same schema for several audiences, but schema equality MUST NOT be inferred from their shared format.

Request-time selection

CallToolRequestParams gains an optional requestedOutputs field:

export interface CallToolRequestParams extends InputResponseRequestParams {
  name: string;
  arguments?: { [key: string]: unknown };
  requestedOutputs?: ToolResultOutputSelector[];
}

For a request using protocol version 2026-12-01 or later, a client SHOULD send requestedOutputs when it requires an output other than, or in addition to, the assistant/content fallback. It MAY omit requestedOutputs to accept the fallback.

A client MUST NOT send requestedOutputs on a request using an earlier protocol version.

The client MUST select only variants advertised by the tool. It MUST NOT request an extension audience unless both client and server advertised support for the defining extension and completed any audience-specific negotiation it requires.

The client MUST NOT include the same selector more than once in requestedOutputs.

requestedOutputs is protocol control data selected by the client or host. It is not part of the tool's model-generated arguments. A client SHOULD choose it from the active execution mode, available consumers, local policy, and negotiated extensions. When a client presents an output schema to a model for code generation or result planning, it SHOULD present the schema for the variant it will request rather than every schema advertised by the tool.

If requestedOutputs is absent, the server MUST return assistant/content. This fixed fallback preserves the current required content result while allowing clients with a specific execution mode to make an explicit selection.

The client MAY request more than one variant. Multiple variants are intended for calls with more than one active consumer. An Apps-capable conversational host, for example, can request one output for the assistant and one for the App View. Servers do not return multiple formats merely because they are available.

If the client requests an undeclared or unnegotiated variant, the server MUST reject the request as invalid parameters. It MUST NOT silently substitute another variant.

Tool call result

For tools that declare outputVariants, CallToolResult contains an outputs array:

export interface CallToolResult extends Result {
  outputs: ToolResultOutput[];
  isError?: boolean;
}

For a successful call, the server:

  • MUST return exactly one output for every requested selector
  • MUST NOT return an output that the client did not request
  • MUST use the audience and format from the matching selector
  • MUST validate each structured output against the schema for that variant
  • MUST NOT mirror a structured output into a content output unless the client requested both

The order of outputs has no semantic meaning.

The server may compute one internal result and project it into several outputs. The wire protocol does not require outputs to originate from the same in-memory value.

Routing requirements

After receiving a valid result, the client or host routes each output as follows:

  • assistant outputs are eligible for model context. The host MAY transform them to match the model provider's tool-result format.
  • user outputs are eligible for user presentation. A user/content output provides typed content blocks for the host to render. A user/structured output provides structured data for host-controlled presentation, such as a table, property view, or JSON inspector. The host MUST NOT automatically add either format to model context.
  • programmatic outputs are returned to client-controlled program logic. The host MUST NOT automatically add them to model context or render them to the user.
  • Extension outputs follow the routing rules defined by the negotiated extension.

Routing an output to an additional audience is a local host action and is outside the server's contract. Hosts SHOULD require an explicit product policy or user action before doing so. They MUST NOT treat one audience as a fallback for another merely because the formats match.

Relationship between outputs

Outputs from one tools/call response describe the same tool execution. They MUST NOT contradict one another about the externally observable outcome of that execution.

Servers SHOULD tailor each output to its audience and format. Outputs may express the same result differently, such as concise prose for a model, typed records for programmatic use, or presentation data for an App View.

Outputs for different audiences are not otherwise required to be equivalent. In particular:

  • One output may contain fields omitted from another.
  • One output may summarize another.
  • Structured outputs for different audiences may have different schemas.
  • A content output does not need to serialize any structured output.
  • A server may apply audience-specific redaction or aggregation.

This distinction is intentional. A code-mode client may need stable identifiers, pagination state, and provenance. An App View may need chart series and display state. A model may need a concise answer and a small set of citations. Requiring these to be the same JSON value recreates the problem this SEP is intended to solve.

Result metadata

CallToolResult continues to inherit a single _meta field from Result. A server MAY vary its contents based on requestedOutputs, as it may for any other result field.

Result-level _meta is not an output and has no implicit audience. A client MUST NOT forward it to the model, user, programmatic caller, or an extension consumer unless the core protocol or a negotiated extension defines handling for the relevant metadata entry. Data intended as part of an audience's result belongs in that output's content or structuredContent.

An extension MAY define a namespaced result-level _meta entry and explicit rules for handling or forwarding it. This allows the Apps extension, for example, to retain View-only control metadata in CallToolResult._meta while the dedicated io.modelcontextprotocol/ui output carries the App's result data.

Errors

When isError is true, the server SHOULD return outputs matching the requested selectors when it can produce a useful audience-specific error representation.

Structured error outputs are not required to conform to the success schema. An extension MAY define a separate error schema for its audience.

Clients MUST NOT substitute an error output from one audience for another. If the server cannot produce any requested error representation, it SHOULD return a protocol-level error rather than a successful result with an empty outputs array.

Core examples

Conversational client

A conversational client requests only natural language model context:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search_customers",
    "arguments": { "query": "Globex" },
    "requestedOutputs": [{ "audience": "assistant", "format": "content" }],
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-12-01",
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "outputs": [
      {
        "audience": "assistant",
        "format": "content",
        "content": [
          {
            "type": "text",
            "text": "I found 37 Globex customer records. The three most recently active accounts are Globex Retail, Globex Energy, and Globex Labs."
          }
        ]
      }
    ]
  }
}

Code-mode client

A code-mode client requests stable machine data without paying for a text mirror:

{
  "name": "search_customers",
  "arguments": { "query": "Globex" },
  "requestedOutputs": [{ "audience": "programmatic", "format": "structured" }]
}
{
  "resultType": "complete",
  "outputs": [
    {
      "audience": "programmatic",
      "format": "structured",
      "structuredContent": {
        "items": [
          { "customerId": "cust_1042", "displayName": "Globex Retail" },
          { "customerId": "cust_2088", "displayName": "Globex Energy" }
        ],
        "nextCursor": "eyJvZmZzZXQiOjJ9"
      }
    }
  ]
}

Structured model input

A client that wants JSON in model context requests assistant with the structured format:

{
  "name": "search_customers",
  "arguments": { "query": "Globex" },
  "requestedOutputs": [{ "audience": "assistant", "format": "structured" }]
}

This output has the schema advertised for assistant/structured. It need not use the schema or payload returned for programmatic/structured.

User-visible media

An image generation client can request several renderable results without automatic model inclusion:

{
  "name": "generate_images",
  "arguments": { "prompt": "A sunrise over Puget Sound", "count": 2 },
  "requestedOutputs": [{ "audience": "user", "format": "content" }]
}
{
  "resultType": "complete",
  "outputs": [
    {
      "audience": "user",
      "format": "content",
      "content": [
        { "type": "image", "data": "...", "mimeType": "image/png" },
        { "type": "image", "data": "...", "mimeType": "image/webp" }
      ]
    }
  ]
}

The content array may contain any number of supported blocks and may mix text, images, audio, resource links, and embedded resources. Video and other media without a dedicated content block can use a resource link or embedded blob with an appropriate MIME type. The host renders the result but does not tokenize it merely because it came from a tool call.

Host-native structured presentation

A client with a generic JSON renderer can request structured data for user presentation:

{
  "name": "search_customers",
  "arguments": { "query": "Globex" },
  "requestedOutputs": [{ "audience": "user", "format": "structured" }]
}
{
  "resultType": "complete",
  "outputs": [
    {
      "audience": "user",
      "format": "structured",
      "structuredContent": [
        { "displayName": "Globex Retail", "region": "NA" },
        { "displayName": "Globex Energy", "region": "EMEA" }
      ]
    }
  ]
}

The host chooses how to present the value using its own UI. The server does not control the layout. A tool that requires a custom table, chart, or interactive experience should use a negotiated extension audience.

MCP Apps integration

This SEP recommends that the MCP Apps extension define a dedicated io.modelcontextprotocol/ui audience. The extension would allow the structured format and define the audience as data delivered to the App View associated with the called tool.

An Apps-capable host can request both the assistant and View representations:

{
  "name": "search_customers",
  "arguments": { "query": "Globex" },
  "requestedOutputs": [
    { "audience": "assistant", "format": "content" },
    { "audience": "io.modelcontextprotocol/ui", "format": "structured" }
  ],
  "_meta": {
    "io.modelcontextprotocol/protocolVersion": "2026-12-01",
    "io.modelcontextprotocol/clientCapabilities": {
      "extensions": {
        "io.modelcontextprotocol/ui": {
          "mimeTypes": ["text/html;profile=mcp-app"]
        }
      }
    }
  }
}

The server can return a concise model representation and a richer View representation:

{
  "resultType": "complete",
  "outputs": [
    {
      "audience": "assistant",
      "format": "content",
      "content": [
        {
          "type": "text",
          "text": "I found 37 Globex customer records and displayed them in an interactive table."
        }
      ]
    },
    {
      "audience": "io.modelcontextprotocol/ui",
      "format": "structured",
      "structuredContent": {
        "columns": ["displayName", "region", "lastActivity"],
        "rows": [
          {
            "customerId": "cust_1042",
            "displayName": "Globex Retail",
            "region": "NA",
            "lastActivity": "2026-08-17T21:10:00Z"
          }
        ],
        "facets": { "region": ["NA", "EMEA", "APAC"] }
      }
    }
  ],
  "_meta": {
    "io.modelcontextprotocol/ui": {
      "datasetVersion": "v18",
      "continuationToken": "view_opaque_token"
    }
  }
}

The host provides only the assistant output to the model. It forwards only the io.modelcontextprotocol/ui output to the View. Under protocol version 2026-12-01, a client without the Apps extension can omit requestedOutputs, receive the assistant/content fallback, and never receive the View payload.

This does not change the existing Apps result flow for earlier protocol versions. An Apps-capable client using protocol version 2026-07-28 or earlier sends no requestedOutputs; the server returns the legacy content and optional structuredContent fields, and the host forwards the legacy CallToolResult to the View as defined by the Apps extension. The requested core protocol version determines which result shape applies.

An App-enhanced tool declares both assistant/content and its App output. The Apps extension may define a tool as App-only, for example through _meta.ui.visibility: ["app"], but the tool still declares the core assistant/content fallback:

{
  "name": "refresh_dashboard",
  "inputSchema": { "type": "object" },
  "outputVariants": [
    {
      "audience": "assistant",
      "format": "content"
    },
    {
      "audience": "io.modelcontextprotocol/ui",
      "format": "structured",
      "schema": {
        "type": "object",
        "properties": {
          "datasetVersion": { "type": "string" }
        },
        "required": ["datasetVersion"]
      }
    }
  ],
  "_meta": {
    "ui": {
      "resourceUri": "ui://dashboard/index.html",
      "visibility": ["app"]
    }
  }
}

The Apps extension owns the meaning and enforcement of App-only visibility, including preventing the tool from being exposed to the model. An App or Apps-capable host calling an App-only tool requests io.modelcontextprotocol/ui/structured explicitly. If it omits requestedOutputs, the server returns the same assistant/content fallback used by every other tool.

The Apps extension must define the exact ui/notifications/tool-result projection for the new shape. The recommended projection is the selected io.modelcontextprotocol/ui output rather than the entire CallToolResult. This prevents unrelated assistant or programmatic outputs from crossing the View boundary.

Extension audiences

An extension may define one or more result audiences when it has distinct consumers or routing rules that the core audiences cannot express.

An extension that defines an audience MUST specify:

  • the audience's full identifier
  • which core or extension-defined formats are permitted
  • how tools advertise schemas for that audience
  • how clients route the output
  • whether and how the output may enter model context
  • how namespaced result-level _meta entries are handled
  • fallback behavior when the extension is not negotiated
  • compatibility behavior across extension versions

Clients and servers declare connection-level support through the defining extension's entries in ClientCapabilities.extensions and ServerCapabilities.extensions. Core MCP does not define a separate server-wide list of extension audiences.

An extension with one audience SHOULD use its extension identifier as the audience identifier. An extension MAY define additional namespaced audience identifiers. If any audiences are optional within the extension, the extension MUST define how clients and servers negotiate each one through its per-extension capability settings.

The extension capability declarations establish which audiences the connection supports. A Tool's outputVariants establishes which of those negotiated audiences that tool can produce. An extension audience may appear in outputVariants or requestedOutputs only when both client and server advertised the defining extension and completed any audience-specific negotiation it requires.

Extensions must not redefine the semantics of the core audiences.

Backward compatibility

This SEP changes the tool result wire shape for the 2026-12-01 protocol revision. MCP's per-request protocol version and capability declarations provide the compatibility boundary.

Supporting protocol version 2026-12-01 does not require a server to implement earlier versions. A server that does not implement the requested version rejects it through the protocol's normal versioning mechanism. The projection rules below apply to servers and SDKs that choose to support both versions.

The requested protocol version, not the presence or absence of a field, determines the Tool and result shapes:

Requested protocol version Tool declaration tools/call request CallToolResult
2026-12-01 or later outputVariants Optional requestedOutputs outputs
2026-07-28 or earlier Legacy optional outputSchema No requestedOutputs content, optional structuredContent

A client that receives a Tool without outputVariants under protocol version 2026-12-01 MUST treat the response as invalid. It MUST NOT silently switch to legacy semantics. To use an earlier shape, the client repeats the request under an earlier protocol version supported by the server.

Servers handling older requests

A server handling tools/list under protocol version 2026-07-28 or earlier MUST return the legacy Tool shape and MUST omit outputVariants. It MAY expose one structured variant through the legacy outputSchema. For a new tool, the server SHOULD use the schema from assistant/structured when that variant is declared and MUST NOT automatically project a user, programmatic, or extension schema into outputSchema unless a negotiated extension defines a compatible legacy projection.

A server handling tools/call under protocol version 2026-07-28 or earlier MUST return the legacy content and optional structuredContent fields. It SHOULD construct content from assistant/content. If the corresponding legacy Tool declaration included outputSchema, any successful structuredContent value MUST conform to that schema.

For a tool that already supported the requested older version, the server SHOULD preserve its established outputSchema and structuredContent behavior. For a new tool without a safe structured projection, the server MAY omit both fields and return only content.

The server SHOULD retain the mirroring behavior required or recommended by the requested protocol version. It MUST NOT assume an old client understands outputs.

Clients calling older servers

A client calling a server under protocol version 2026-07-28 or earlier MUST use the legacy Tool and request shapes and MUST NOT send requestedOutputs. It MUST accept the legacy result shape. It may apply local selection rules to content and structuredContent, but it cannot assume the server produced either field for a particular audience.

An outputVariants field received under an earlier protocol version is not a negotiation signal and does not enable the new request or result shapes.

SDK migration

SDKs should expose the new API as explicit declarations, selectors, and result unions. They should keep legacy constructors behind version-aware adapters rather than preserving automatic mirroring in the new API.

For new-protocol requests, SDKs MUST NOT generate an unrequested text mirror. For old-protocol requests, SDKs may continue to generate the mirror needed for compatibility.

Tool migration

SDKs may help existing tools migrate with these inferences:

  • A legacy tool maps content to assistant/content.
  • A legacy tool that produces structured data suitable for model context should also declare assistant/structured.
  • A legacy tool with outputSchema may expose programmatic/structured only when its implementation can produce structured data without relying on client-side inference.
  • A legacy tool that already provides model text and App data should declare assistant/content and io.modelcontextprotocol/ui/structured when the Apps extension is negotiated.
  • A tool that the Apps extension defines as App-only still declares assistant/content and adds io.modelcontextprotocol/ui/structured for explicit App requests.

SDKs must not infer that one legacy outputSchema is suitable for assistant, programmatic, and extension audiences.

Security implications

Audience selection separates model-visible, user-visible, programmatic, and extension data for routing. It does not make tool results trustworthy. Clients must continue to treat server output as untrusted input.

Audience selection is a routing mechanism, not an authorization or confidentiality boundary. Clients receive every requested output and MUST enforce their own authorization, isolation, and data-handling policies. Servers MUST NOT rely on an audience declaration to protect sensitive data.

Servers should minimize every output for its audience. Audience-specific schemas do not authorize the server to disclose data the client or user is not permitted to access.

Clients should validate structured outputs against the schema for the requested variant. A client must not validate an App output against the programmatic schema or accept one audience as a substitute for another.

Rationale

Keep a model-compatible baseline

The current protocol guarantees a content result shape that every tool client can process. Requiring assistant/content as the fallback for every tool preserves that lowest common denominator while making its intended consumer explicit. A basic conversational client can call any tool deterministically without understanding structured output or selecting among variants.

assistant/structured remains recommended when a tool has a natural structured representation for model context, but requiring every tool to invent one would create low-value wrapper schemas. Conversely, structured data prepared only for code, host-native presentation, or an App does not replace a usable model representation.

Keep the fallback fixed

Client capabilities describe the representations a client can consume, but they do not identify the active consumer for a particular call. One client may support conversational, programmatic, and App outputs at the same time. The client therefore selects requestedOutputs when it knows the intended consumers.

When the client makes no selection, a fixed assistant/content fallback preserves existing behavior without adding another declaration or an extension-specific branch. Extension-aware callers already know when they need an extension output and can request it explicitly.

Separate audience from format

content versus structured answers how data is encoded. It does not answer who should receive the data. Keeping audience and format separate supports structured model inputs, host-native presentation of structured data, and user-visible media that stays out of model context.

The valid core combinations remain closed. This avoids adding speculative combinations merely because the two properties can form a larger matrix.

Select at request time

The client knows whether the current call serves a conversation, code mode, a user presentation, or a negotiated extension. The server knows which representations it can produce. Advertisement plus request-time selection lets both sides participate without giving either one unilateral control.

The server does not waste bytes on representations the client will discard. The client does not guess the purpose of fields after receiving them.

Allow explicitly requested multiple outputs

Most calls should request one output. A strict one-output rule would force MCP Apps to perform a second tool call or make the server choose between model context and View hydration. Multiple outputs are therefore permitted only when the client requests multiple audiences.

This keeps the common result polymorphic while supporting calls that genuinely have several consumers.

Give MCP Apps a dedicated audience

host is too broad. A host may contain a model adapter, a code executor, a renderer, an App View, and product-specific middleware. Marking structured data as host-facing does not identify which of those components should receive it.

The Apps extension already defines a distinct consumer and a negotiated routing path. Giving it the io.modelcontextprotocol/ui audience lets the extension own those semantics. It also lets an App schema differ from the schema for programmatic/structured or assistant/structured.

Keep schemas per variant

A single Tool.outputSchema implies that every structured consumer receives the same contract. That is false for several real tools. Per-variant schemas make the difference visible during tool discovery and let generated SDK types reflect the output the client actually requested.

Alternatives considered

Keep the current fields and revise guidance

This was the approach in SEP-2200. It improves prose but leaves selection implicit and asks clients to infer server intent after the response arrives. Core maintainer feedback rejected it as a partial fix.

Define content as assistant data and structuredContent as host data

This cannot distinguish direct structured model input, code-mode data, and App View data. It also retains a single outputSchema for consumers that may need different contracts.

Use only content block annotations

Annotations.audience applies to individual content blocks and currently uses the user and assistant roles. It does not annotate structuredContent, declare a structured schema, or let a client request a result shape before execution. Annotations remain useful within a content output but do not replace output negotiation.

Allow arbitrary server-defined variant identifiers

Arbitrary identifiers offer flexibility but give clients no interoperable routing semantics. This SEP defines a closed core set and reuses negotiated extension identifiers for additional audiences.

Require exactly one output

This matches the simplest interpretation of a polymorphic result. It does not support a single App tool call that needs both model context and View hydration. Explicit multi-selection is narrower than returning all available variants when the request omits a selection.

Require all variants to contain equivalent data

Semantic equivalence sounds safe but is difficult to define and blocks legitimate audience-specific schemas, redaction, summaries, and display state. The proposal requires consistency about the tool execution while allowing each audience to receive the representation it needs.

Open questions

  • Should requestedOutputs be required whenever a tool declares more than one variant, instead of allowing the fixed assistant/content fallback to apply?
  • Should structured error outputs have an optional per-variant error schema?
  • Should the Apps extension forward only its selected output to the View, or wrap that output in a reduced CallToolResult for compatibility with existing App SDKs?

Reference implementation and testing plan

A reference implementation should cover one server SDK and two client modes before this SEP moves to Final:

  • a conversational call requesting assistant/content
  • a code-mode call requesting programmatic/structured
  • a direct structured-model call requesting assistant/structured
  • a user-rendered media call requesting user/content
  • a host-native table call requesting user/structured
  • an Apps call requesting assistant/content and io.modelcontextprotocol/ui/structured
  • rejection of any tool declaration that omits assistant/content
  • an App-only call explicitly requesting io.modelcontextprotocol/ui/structured
  • an App-only call without requestedOutputs receiving assistant/content
  • rejection of undeclared and unnegotiated selectors
  • independent validation of programmatic and App schemas
  • omission of requestedOutputs when calling under protocol version 2026-07-28 or earlier
  • versioned projection of Tool declarations and results to the 2026-07-28 legacy shapes
  • rejection of a Tool without outputVariants under protocol version 2026-12-01
  • confirmation that unrequested outputs and metadata do not reach the model or App View

SDK conformance tests should verify both selection and routing. Schema-only validation cannot prove that a host kept a user, programmatic, or extension output out of model context.

References

Acknowledgments

This proposal builds on review and implementation feedback from Ola Hungerford, David Soria Parra, Shaun Jacobs, Cliff Hall, Dale Seo, Alex Hancock, the MCP Apps maintainers, and participants in the MCP contributor discussions about tool result routing.

SEP-3313 Structured Tool-Failure Classification (`ToolFailure`)

Draft · Standards Track · Created 2026-08-27

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:

  • Clients and agents can't reliably branch on failure type. An agent seeing isError: true with a text message has to pattern-match prose to decide whether to retry, ask the user to log in, or give up. This is fragile across servers and languages.
  • Retry logic is unsafe by default. Without a retryable signal tied to the tool's declared idempotentHint, clients either never auto-retry (leaving transient failures to the user) or retry unsafely.
  • Operators can't correlate a user's bug report with server logs. Enterprise operators debugging a failed tool call have no standard field to match what the user saw against what the server logged, so triage is slow and manual.
  • Diagnostic detail and model-facing guidance are conflated. Verbose diagnostic text wastes context when all the model needs is a short instruction ("ask the user to reconnect their account").

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

  • A client that does not understand this extension MUST continue to behave exactly as it does today (treat the result as an opaque error via isError and content).
  • A client that understands the extension SHOULD branch primarily on class:
    • capability / governance: MUST NOT auto-retry the call.
    • reliability: MAY auto-retry, but only when retryable is true AND the tool's idempotentHint permits it, and MUST NOT retry before retryNotBefore if present.
  • Any intermediary (proxy, gateway) relaying a CallToolResult MUST preserve _meta["io.modelcontextprotocol/toolFailure"] unchanged, including correlationId.

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:

  • Encode classification in the JSON-RPC Error.data field instead. Rejected: tool execution failures are explicitly required by the existing spec to travel as CallToolResult.isError, not as protocol-level errors, so the model can see and self-correct. Using Error.data would be inconsistent with that rule and would only apply to the narrower set of failures that are legitimately protocol-level (e.g. unknown tool).
  • A single closed enum of failure codes. Rejected per the class/code rationale above — a closed enum would need a spec revision every time a new failure shape emerged.
  • Bundling retry/idempotency "contract" fields (caller keys, dedup behavior, tri-state idempotentHint) into this proposal. Out of scope. Discussion #3188 is actively developing a separate, more detailed retry-contract proposal; this SEP only reuses the existing idempotentHint as a gating condition and leaves deeper retry semantics to that effort to avoid duplicating or preempting unsettled work.

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

  • correlationId is intended to be an opaque, server-generated identifier for log correlation. Servers MUST NOT encode sensitive information (session tokens, user identifiers, request payloads) in it — it should be treated like a trace/request ID.
  • agentGuidance is a short instruction string sent to the calling model. As with any server-controlled text reaching the model, clients SHOULD treat it as data, not as a command to the client itself, consistent with existing guidance on treating tool output as untrusted content.
  • This extension introduces no new authentication, authorization, or data validation surface; it only classifies failures that already occur under the existing protocol.

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

  • Should retryNotBefore also be exposable via an HTTP response header for transports where inspecting _meta before the body is fully parsed would help proxies/gateways? Raised informally by @YoadElkayam in the related discussion; not addressed here to keep this SEP scoped to the result schema.
  • Should there be a companion, non-normative documentation page (guide) in docs/ listing the baseline code vocabulary and example client dispatch logic, independent of whether/when this SEP is accepted? The original discussion raised "documentation-first before a formal SEP" as an option; this SEP and such a guide are not mutually exclusive.

Acknowledgments

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

SEP-3371 Consistent SDK Extension Points

Draft · Standards Track · Created 2026-09-18

Imported from upstream. Proposed as modelcontextprotocol#3371 by @sambhav on 2026-09-18, 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 requires Tier 1 Model Context Protocol (MCP) SDKs to provide public APIs for extension registration with capabilities and dependency checks, custom RPC methods, and middleware. Extension authors can use these APIs from independent packages, while applications choose which extensions to enable.

Terminology

Term Meaning
SDK Software development kit: a library for building MCP clients or servers.
Protocol extension A specification that adds capabilities, methods, or behaviour to MCP, identified by a namespaced name.
SDK extension point A public API that application or extension code uses to add behaviour.
Extension registration Enabling an extension locally with its declared capabilities and dependencies.
Capability A declaration that a client or server supports a feature, together with any settings.
RPC Remote procedure call: invoking a named operation on a peer.
Method A named operation. A request expects a response; a notification does not. Custom methods are defined by extensions.
Handler The local code that processes a method.
Method contract The allowed message types and exchanges for a method, as defined by the protocol or extension specification.
Middleware Code that wraps a step in sending or receiving a message.
Payload Message parameters or results, including metadata in _meta.
Envelope The JSON-RPC fields that identify and correlate messages, such as method, id, and jsonrpc.

Motivation

As the number of MCP extensions grows, bundling their implementations into SDKs creates a growing maintenance and coordination burden. SDK maintainers must keep extension-specific code, dependencies, tests, and documentation up to date alongside the core protocol. Each extension has its own maintainers and priorities, so supporting more extensions means coordinating with more teams whenever either the SDK or an extension changes.

Extension owners need to manage their features, fixes, compatibility, and support commitments throughout the extension's lifecycle: experimentation, stable releases, ongoing maintenance, deprecation, and retirement. When implementations are bundled into SDKs, that work depends on each SDK's review capacity and release schedule. An extension's priorities and timelines may differ from those of the SDKs that support it.

Keeping bundled code optional does not remove this coordination. Forks allow separate releases but require ongoing rebasing. Extensions built on separate SDK forks cannot be composed as independent packages on a single SDK. Applications must instead merge their changes into a shared fork and maintain that integration as each extension evolves. Public extension points let those packages compose without transferring SDK integration work to applications.

SEP-2133 provides a process for developing protocol extensions, but leaves SDK integration open. Some SDKs already support independent extension packages; the available APIs and their guarantees vary, as the SDK comparison shows.

This SEP gives SDK maintainers a common set of public extension points to maintain, while extension owners maintain and release their own packages. Applications can combine those packages on one SDK and update them independently within their declared compatibility requirements. This separation applies throughout the lifecycle of both official and third-party extensions, allowing the ecosystem to grow without making SDK maintainers responsible for every extension.

Specification

Tier 1 SDKs MUST meet the requirements below through documented public APIs. Other SDKs SHOULD meet them. These keywords follow BCP 14.

The requirements apply to clients and servers in the directions permitted by the applicable protocol version, transport, and extension specification. External packages must be able to use the public APIs without an SDK fork or an SDK-owned allowlist.

This SEP specifies the behaviour SDKs must support. SDK maintainers choose APIs that make extensions easy to enable and compose in their language. Existing APIs, builder options, functions, interfaces, or wrappers may satisfy the requirements; a new extension class or plugin framework is not required.

The protocol and extension specifications continue to define message formats, capability negotiation, and method contracts. Transport mechanisms and package loading are outside this proposal's scope.

Illustrative examples: All examples use concise TypeScript for readability. Names, types, and signatures are illustrative, not APIs required by this SEP or claims about an existing SDK. Each language should use its own idiomatic APIs; conformance depends on the supported behaviour.

The examples follow a catalog-search extension, with each showing one part of the integration. Configuration happens before startup, and session represents a connected peer. Imports, supporting types, schemas, and handler definitions are omitted.

1. Registration, capabilities, and dependencies

Applications enable extensions by identifier, with declarations of the capabilities they provide and the local dependencies they require.

SDKs must let applications compose multiple independently packaged extensions on the same client or server. A package must be able to encapsulate installation of its capabilities, custom RPC handlers, and middleware appropriate to that instance. Applications enable the package through the SDK's idiomatic configuration mechanism and configure its options and composition. The extension points below are building blocks for package authors; applications do not need to register each contribution separately.

// Defined by the search package.
function searchExtension() {
  return new Extension("com.example/search", {
    capabilities: { selectionNotifications: true },
    requires: { resources: {} },
    setup: registerSearchMethods,
    middleware: [searchMiddleware],
  });
}

// Application setup.
const server = new Server({
  extensions: [searchExtension(), auditExtension()],
});
server.addResource("catalog", { handler: readCatalog });
await server.start(); // Validate dependencies after configuration is complete.

Adding searchExtension() installs its declared capabilities and middleware and invokes its method-registration callback. Adding the catalog resource enables resource support before startup checks the extension's dependency. The following sections show the package's RPC setup and middleware.

Registering extensions

Applications and extension configuration code must be able to inspect the registered identifiers before startup.

Registration must:

  • Reject duplicate extension identifiers.
  • Leave no usable partial configuration after failure.

An SDK may satisfy the failure requirement by rejecting the entire configuration. Inspection can use a read-only configuration view.

SDKs may limit registration to startup; runtime installation and removal are optional.

Capabilities

SDKs must let extension packages declare support and settings as part of registration. Registration and capability declarations use the same extension identifier. SDKs must preserve settings and unrelated entries, and reject conflicting local settings for the same identifier.

Declarations use the existing ClientCapabilities.extensions and ServerCapabilities.extensions maps defined by SEP-2133. Each entry is keyed by a namespaced identifier such as com.example/search; third parties use a reversed domain they control, and io.modelcontextprotocol is reserved for official extensions. An empty settings object declares support; a missing entry does not. Local dependency declarations and payload metadata do not advertise peer support.

Extension code must be able to read the peer capability information available for an operation through the SDK's existing discovery, initialization, or request-context APIs, as appropriate for the protocol version. For example, in the 2026-07-28 protocol, clients can obtain server capabilities through discovery, while servers read client capabilities from the current request. The protocol and extension specifications determine how that information is used.

Checking local dependencies

An extension can require local capabilities or other registered extensions. For example, another extension could depend on search as well as core capabilities:

const requires = {
  resources: {},
  tools: { listChanged: true },
  extensions: { "com.example/search": {} },
};

This local declaration requires resource support, tool-list change support, and the local search extension. An empty object requires presence; a required boolean must be true. SDKs may represent the same requirements with native types or configuration APIs. The declaration adds no wire fields.

Before processing messages, the SDK must:

  1. Collect the completed application and extension configuration.
  2. Derive its capabilities, including those enabled by application registrations.
  3. Check every extension's dependencies, including those of its dependencies.

Validation happens at build or startup, so dependencies may be registered after the extensions that need them. Duplicate extension identifiers can be rejected immediately.

Missing extensions and missing, disabled, unknown, or inapplicable capabilities must fail validation. The error must name the extension and unmet requirement.

These checks do not install packages or enable capabilities. Applications and extension packages manage package version compatibility.

2. Custom RPC methods

SDKs must provide public APIs to register handlers for custom requests and notifications, and to call or send those methods. Extension packages use these APIs during setup, supplying parameter and result types, with schemas or codecs where needed. Method bindings are supplied by the package's setup code, separately from its capability declaration.

SDKs must document whether handlers receive parameters, a request object, or a separate context. The parameter schemas below describe Request.params; envelope and context access follow the SDK's API.

// Called for each server that enables search.
function registerSearchMethods(server: Server) {
  server.registerRequestHandler("com.example/search", {
    handler: searchCatalog,
    paramsSchema: SearchParamsSchema,
    resultSchema: SearchResultSchema,
  });
  server.registerNotificationHandler("com.example/search-selected", {
    handler: onSearchSelected,
    paramsSchema: SearchSelectedParamsSchema,
  });
}

// Peer usage, where permitted by the protocol and transport.
const result = await session.call(
  "com.example/search",
  { query: "invoices" },
  { resultSchema: SearchResultSchema },
);
await session.notify("com.example/search-selected", { query: "invoices" });

Handler registration must reject duplicate method names, including conflicts between requests and notifications, claims on core protocol methods, and replacement of existing handlers by applications or extensions. These conflicts can be rejected immediately. Failure must leave no usable partial configuration; if the configuration remains usable, existing handlers must remain unchanged.

Custom messages must use the SDK's normal dispatch, request correlation, error handling, applicable cancellation, and middleware.

3. Middleware

Applications and extensions can add middleware to the same chains, covering core and custom requests, responses, and notifications when sending and receiving, including messages delivered over streams. Installing middleware alone does not register a method or advertise a capability.

SDK request, response, and notification objects may carry context in addition to the protocol payload, such as authentication information, cancellation, peer capabilities, or information about the associated operation. Middleware must be able to access the applicable context already exposed by the SDK through its public APIs. SDKs may attach it to these objects or expose it separately. This local context is not automatically serialized into the message.

The following example wraps a handler that produces complete MCP messages. The loop waits for each message and yields it as it becomes available, whether there is a single response or a stream. Serialization and transport framing remain with the SDK.

function searchMiddleware(next: Handler): Handler {
  return async function* (request) {
    // Inspect or modify the request; stop here if needed.
    for await (const message of next(request)) {
      // Inspect or modify each response or notification.
      yield message;
    }
    // Run after normal completion.
  };
}

The SDK runs the chain and closes any active iterators when processing completes, fails, is cancelled, or stops early. Middleware can skip next and produce an outcome allowed by the method's contract. Error handling and cleanup use the language's usual mechanisms, omitted from the example for clarity.

Messages

Middleware may modify or replace a payload, or return early, within the method's existing contract, including supported intermediate results and follow-up exchanges. It cannot change the method's identity or allowed types. The SDK owns the envelope and request correlation.

Metadata in _meta, including on nested objects, is part of the payload. Public APIs must allow entries to be added, updated, and removed without changing generated core types, while preserving the protocol's rules for reserved fields. Typed APIs and serialization must preserve valid unknown metadata unless application or extension code changes it. Changes must be visible to downstream middleware and the message's recipient; extension payload metadata is not automatically copied into results.

Notifications and streaming

For a notification, delegating to next forwards it for sending or processing. In the iterator example, the SDK must consume the iterator even when it yields no messages, so the handler runs. Completion is local: it does not produce a protocol response. Middleware can inspect or modify the notification, or skip next to suppress it where the protocol permits. Suppressing one notification does not by itself terminate the surrounding operation or stream.

Middleware must also cover individual protocol messages as they are produced or consumed on a stream. Wrapping only the final result of a request is insufficient when notifications or other supported messages arrive before it. SDKs may use their sending and receiving message chains, stream wrappers, callbacks, or equivalent public APIs.

The example uses an iterator throughout for a consistent handler shape. SDKs may instead return a response object containing a stream, or let middleware wrap a writer or callback that receives each message. When next returns before a stream is consumed, per-message logic and end-of-stream cleanup belong in that stream's wrapper or equivalent callbacks.

Writer and iterator wrappers must preserve incremental delivery, message order, flow control, cancellation, errors, and cleanup through the SDK's normal lifecycle. Middleware must not require buffering the complete stream. Early termination follows the method and transport's existing rules.

These requirements cover protocol messages, not transport frames, byte chunks, or new streaming result types.

Composition

Applications must be able to compose middleware from multiple extensions and application code in an explicit order. Applications and extension configuration code must be able to inspect it before processing messages. Applications must be able to place middleware from their own code or another extension between contributions from one extension, without installing a contribution twice. Dependencies do not determine order, and sending and receiving chains may have separate orders.

An ordered configuration list or equivalent view is sufficient for inspection; middleware identifiers and a runtime registry are not required.

For A(B(handler)), the request passes through A, then B, then the handler. Results and errors return through B, then A. If B returns early, A receives its outcome and the handler is skipped.

SDKs must document:

  • How registration determines order, including repeated registrations and extension contributions.
  • Where middleware runs relative to parsing, validation, dispatch, and serialization.

Changes to SDK-provided request-local state must not leak into independent calls: for example, changing one call's metadata must not change another call's metadata through a shared object. Applications and extensions are responsible for shared state they introduce.

Packaging and publishing

SDKs SHOULD provide a minimal example showing how to package an extension, declare SDK compatibility, and enable it in an application.

For extensions maintained within the MCP organisation, SDK maintainers SHOULD document the expected package namespaces and naming conventions across release stages, including experimental, beta, and stable releases. They SHOULD coordinate with the Infrastructure Working Group to establish how extension maintainers obtain the access needed to publish those packages. Extension maintainers remain responsible for their packages and releases.

Third-party extensions choose their own package names, registries, and publishing processes. The SDK's public extension points must support them without requiring organisation-managed namespaces or publishing access.

Rationale

Why these extension points

Working group discussions identified a common set of integration needs covering most of the extensions under consideration. This proposal groups them into three extension points. The extension requirements map those designs to the proposed APIs and identify the remaining gaps.

  • Registration, capabilities, and dependencies let applications enable packages, declare their support, and check local requirements. Peer capabilities remain governed by the protocol's discovery and negotiation rules.
  • Custom RPC methods let extensions add operations while reusing the SDK's dispatch and message handling.
  • Middleware lets extensions compose changes around existing operations while preserving their method contracts.

Together, these APIs give SDK maintainers a small set of reusable integration points to maintain as new extensions appear. Extension authors should first compose these APIs, keeping extension-specific behaviour in their own packages.

Scope and exceptions

An extension that cannot be implemented through these APIs needs to work with SDK maintainers to identify the missing support and agree on an approach. Additional SDK integration may be justified, but should address a concrete requirement that composition cannot meet.

Work intended for the core protocol may also require changes to core schemas, capability definitions, and SDK types. Those changes belong in the core protocol process and may warrant direct SDK implementation. This proposal does not require core protocol development to fit within extension APIs.

API design choices

The SDK patterns in Appendix A show that the same behaviours can be implemented using different APIs.

  • Common behaviour, language-specific APIs. A shared plugin interface would constrain SDK design without improving interoperability between peers.
  • Middleware rather than handler replacement. Replacing handlers makes method ownership depend on registration order. Wrapping a stable handler lets applications combine behaviour explicitly.
  • Validation after configuration. A capability may be enabled later by the application. Checking the completed configuration avoids that ordering constraint. Dependencies determine which components must be present; middleware order determines how they process messages.

Backward Compatibility

This SEP adds no wire fields or methods. Some SDKs will need API changes, such as rejecting handler replacement; their maintainers determine the migration approach.

Individual extensions remain optional. The core protocol behaviour of applications that enable neither extensions nor middleware remains unchanged.

The extension points become a Tier 1 requirement. Rollout is agreed separately through SDK governance, with the SDK guidance and tiering documentation updated accordingly.

Reference Implementation and Verification

A complete reference implementation is still needed. The APIs in Appendix A implement parts of this proposal. The inline examples are pseudocode.

Shared conformance suite

The MCP conformance suite must include a shared set of scenarios for this SEP. Fixtures use each SDK's idiomatic public APIs; the suite checks the required outcomes rather than the example API shapes. Each SDK provides client and server fixtures that install at least two test extensions and standalone application middleware. One extension contributes multiple middleware functions so the suite can verify composition with application code. The suite supplies a reproducibly generated method name and nonce to ensure that custom dispatch is exercised.

The shared scenarios must verify:

  • Custom requests and notifications, including typed payloads, request correlation, unknown-method errors, and notification delivery without a response.
  • Capability declarations and settings, including preservation of unrelated entries.
  • Middleware mutation, explicit ordering, early returns, error propagation, metadata preservation, and isolation between concurrent calls.
  • Streaming message delivery before the stream completes, including middleware applied to notifications, with cancellation and cleanup preserved.
  • Baseline behaviour: unenabled extensions contribute no methods or advertisements, standalone middleware needs no extension registration, and core behaviour remains intact without either.

Run scenarios in every direction permitted by the selected protocol version and on the transports required for the SDK's tier. Fixtures must make the observations needed to check these outcomes available to the suite.

SDK-local checks and results

Each SDK must also use its native test framework to verify requirements that wire-level tests cannot establish: registration conflicts and failure handling, dependency checks after configuration, capability-setting conflicts, inspection of registrations and middleware order, access to SDK-provided request and response context, and preservation through typed APIs and codecs.

An SDK claiming conformance must pass all applicable shared scenarios and SDK-local checks. Report the SDK version, protocol version, transports, and results, with reasons for any scenarios marked inapplicable. Fixture review must confirm that the implementation uses public APIs. The SDK and Conformance working groups maintain the shared scenarios; rollout remains separate as described above.

These checks establish conformance of the SDK extension points. Individual extensions still need tests for their own behaviour.

Security Implications

Extensions and middleware run as trusted application code with access to protocol data. Applications should enable only code they trust; registration checks and explicit ordering do not sandbox it.

SDKs must retain protocol validation and authorization checks. Middleware does not replace transport authentication. Extensions should avoid placing credentials in _meta.

Appendix A: Prior art and SDK support

Existing SDKs provide different parts of this proposal. Python has extension registration and server middleware; Go has separate sending and receiving chains composed as nested handlers. The table and source notes below show the wider picture.

The same composition pattern appears in Django, Koa, ASP.NET Core, and Tower. This SEP applies that pattern to MCP messages.

Earlier extension integration discussions describe the maintenance and release costs of bundling experimental implementations, and the difficulty of combining independently maintained forks.

This assessment uses the revisions linked below, reviewed on 2026-09-18. Tier labels follow the official SDK listing used at that time.

Legend: ✅ a public API exists for the stated scope; ❌ a confirmed gap; ? not verified. Lower-level public APIs and wrappers count. These ratings describe available APIs, not conformance with this SEP.

The paired entries under Registration and capabilities report extension-ID registration / extension capability-map support, respectively. Middleware must operate on MCP messages, rather than only HTTP requests.

SDK / tier (source note) Registration and capabilities Custom RPC methods Middleware
TypeScript — Tier 1 (1) ? / ✅ ✅ ?
Python — Tier 1 (2) ✅ / ✅ ✅ ✅ server
C# — Tier 1 (3) ? / ✅ ✅ ✅ server
Go — Tier 1 (4) ? / ✅ ✅ requests¹ ✅
Rust — Tier 1 (5) ? / ✅ ✅ ✅ wrappers
Java — Tier 2 (6) ? / ❌ ✅ session API ✅ handler wrappers
Ruby — Tier 2 (7) ? / ✅ ✅ requests¹ ?
PHP — Tier 3 (8) ✅ / ✅ ✅ ?
Kotlin — Tier 3 (9) ? / ✅ ✅ ?
Swift — Tier 3 (10) ? / ❌ ✅ ?

¹ Custom requests are supported. Complete custom-notification registration and dispatch remain unverified.

Source notes

  • TypeScript (1): Custom-method registration replaces existing handlers. Client middleware wraps HTTP fetch; general MCP message middleware was not established.
  • Python (2): Extension registration checks custom request conflicts, but core notification conflicts only trigger warnings. Middleware is provisional and server-side; client coverage and mixed extension ordering remain unverified.
  • C# (3): Request registration is experimental and allows overriding built-ins. Filters cover notifications and early returns; client hooks and inspection of the final order remain unverified.
  • Go (4): Custom-method registration rejects core names but replaces duplicate custom handlers. Middleware documents nested sending/receiving chains; combined-order inspection, custom notifications, and reverse-call coverage remain unverified.
  • Rust (5): Dispatch has no extension registry. Service wrappers wrap received calls; full directional coverage needs adapters, and complete notification filtering remains unverified.
  • Java (6): Session constructors permit wrapped handlers through public handler maps; higher-level setup is private. Capability records lack extensions. Extension registration and higher-level middleware remain unverified.
  • Ruby (7): Method registration rejects existing names. Extension capabilities exist; message mutation and complete notification coverage remain unverified.
  • PHP (8): Extension registration permits some built-in overrides. Custom handlers replace rather than wrap handlers. General middleware, rollback, full conflict checks, and conflicting settings remain unverified.
  • Kotlin (9): Handlers replace existing handlers; guarded registration and message mutation remain unverified. Capability models include extension maps and use JsonObject for metadata.
  • Swift (10): Handlers replace method handlers and append notification handlers, with client equivalents. Client APIs do not establish general middleware; capability models lack extensions.

All ten SDKs expose _meta. Full metadata preservation and the other guarantees in Reference Implementation and Verification still need testing against released versions.

Appendix B: Extension requirements

This table shows which SDK APIs each extension uses. The links point to the revisions assessed on 2026-09-18.

Legend: ✅ used by the integration; ◯ optional; — not needed; ⚠️ useful but insufficient for the full behaviour; ? insufficient detail to assess. These describe API needs, not completed implementations.

The paired entries under Registration and capabilities report registration / capability declarations, respectively. Registration assumes installation through the proposed API. None of these designs requires another named local extension. Some experimental capability declarations would need to use the extensions map.

Extension Registration and capabilities Custom RPC methods Middleware
Apps ✅ / ✅ — ◯
Skills ✅ / ✅ ✅ —
Tasks ✅ / ✅ ✅ ⚠️
Interceptors (experimental) ✅ / ✅ ✅ ✅
Variants (experimental) ✅ / ✅ — ⚠️
Triggers/events (experimental) ✅ / ✅ ✅ ◯
Trust annotations (experimental) ✅ / — — ◯
Action metadata (experimental) ✅ / — — ◯
Grouping (exploratory) ? / ? ? ?

Apps, Skills, Tasks, and trust annotations use _meta; it is optional for Interceptors, Variants, and Triggers/events. Action metadata uses Tool.annotations instead. These payload needs are covered in the integration notes below.

Integration notes

  • Apps: Covers the MCP server connection and requires local resource support. The app–host bridge and UI hosting remain separate.
  • Skills: Adds skills/list, skills/get, and optional resources/directory/read. It needs core resource handlers but no custom notifications.
  • Tasks: Custom task requests and status notifications fit these APIs. Returning a task handle from a core method also needs support in that method's contract and SDK result types. The package still supplies storage and execution.
  • Interceptors: Custom requests support discovery and invocation; middleware applies interceptors to MCP calls. Agent lifecycle events still need host hooks. Remote interceptor order is separate from local middleware order.
  • Variants: The linked proxy wraps requests and results and redirects notifications. Some routing also needs session access, so payload middleware alone cannot support every proxy arrangement.
  • Triggers/events: Message types depend on the delivery mode. Webhooks, long-lived streams, and changes to timeout or concurrency handling may need additional APIs.
  • Trust annotations: Handlers can attach metadata to results or content blocks directly. Middleware is optional, and the draft requires no capability negotiation.
  • Action metadata: Adds fields to Tool.annotations. SDK types must preserve those fields; the _meta guarantee in this SEP does not cover them. Changes use the core list-change behaviour.
  • Grouping: The design does not yet specify enough of the message exchange to assess.

Authorization extensions and Server Card need transport integration beyond this proposal.

SEP-3392 Extension Lifecycle

Draft · Process · Created 2026-09-25

Imported from upstream. Proposed as modelcontextprotocol#3392 by @pja-ant on 2026-09-25, 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 gives MCP extensions a maturity lifecycle. An extension starts as Experimental, may advance to Beta, and then either becomes Stable as an extension or is promoted into the core specification. The repository name shows the stage (experimental-ext-<name>, beta-ext-<name>, ext-<name>). The stage decides which protocol changes are allowed, who approves them, and what stability implementers can expect.

Experimental and Beta extensions iterate with Extension Maintainer approval only, and may make breaking changes. Entering Beta and entering Stable each require an Extensions Track SEP. A Stable extension changes only when a new core protocol revision is released. Promotion to the core specification requires a Standards Track SEP.

This SEP updates the extension lifecycle defined in SEP-2133. It does not change how extensions are negotiated.

Motivation

SEP-2133 defines two kinds of extension: an experimental extension in an experimental-ext- repository with no stability promise, and an official extension in an ext- repository that graduated through an Extensions Track SEP. It has no notion of maturity, and two problems follow.

Official extensions carry no stability signal. An Extensions Track SEP is the only gate, and once an extension is official, SEP-2133 lets its maintainers change it at any time without Core Maintainer review. An implementer cannot tell whether an official extension is settled or still being reworked. Extensions signal maturity in different, undefined words: ext-auth uses specification/stable/ and specification/draft/, ext-tasks labels its schema snapshots "Stable" and "Development", and ext-skills publishes specification/stable/.

Change points for official extensions are undefined. SEP-2133 lets extension maintainers change an official extension whenever they choose, and requires a new identifier for every breaking change. Implementers need predictable moments when behavior may change. The protocol already provides one, because every request declares a protocol revision, and SEP-2663 already defines extension behavior per protocol revision.

SEP-2596 left "feature maturity tiers" as an open question and pointed to the alpha/beta/GA model in Kubernetes. This SEP answers that question for extensions.

Specification

Stages

Stage Repository Protocol changes Changes approved by Entry What implementers can expect
Experimental experimental-ext-<name> Any change, at any time, without notice Extension Maintainer Any Maintainer creates the repository Nothing. The design may change or disappear.
Beta beta-ext-<name> Any change, with breaking changes expected to be rare Extension Maintainer Extensions Track SEP Real usage is welcome. Breaking changes are documented.
Stable ext-<name> or an area repository such as ext-<area> Only with a new core protocol revision Extension Maintainer Second Extensions Track SEP Behavior is fixed within a protocol revision.
Core None (archived) As for the core specification Core Maintainers Standards Track SEP As for the core specification.
Extensions Track SEPExtensions Track SEPStandards Track SEPStandards Track SEPExperimentalexperimental-ext-*Betabeta-ext-*Stableext-*Corespecification(repositoryarchived)

An extension MUST NOT skip Beta, unless the Core Maintainers waive the requirement by a recorded vote. Extensions that are official when this SEP is finalized are classified in Transition.

Roles

An Extension Maintainer is a maintainer of the extension's repository who is listed in that repository's MAINTAINERS.md, as ext-tasks already does. As SEP-2133 provides for repository maintainers, the Core Maintainers appoint Extension Maintainers, who are normally the leads of the associated Working Group or Interest Group. Every extension MUST have at least one Extension Maintainer.

"Extension Maintainer approval" means a pull request approved by at least one Extension Maintainer who is not its author. Extension Maintainers SHOULD coordinate changes through the associated Working Group or Interest Group.

The Core Maintainers keep the authority SEP-2133 gives them over every extension, including the ability to modify, deprecate, archive or remove it. Lead Maintainers retain veto authority over each Core Maintainer decision in this SEP, per the governance roles.

Repositories

  • An Experimental or Beta repository MUST contain exactly one extension. A Stable repository MAY group related extensions in one area, as SEP-2133 allows, but an extension joins a grouped repository only when it becomes Stable.
  • The repository name MUST match the extension's stage, and is the only required marker of it. The Core Maintainers rename the repository, using their existing administrator access, when a stage change is approved. GitHub redirects the old URL, and the Extension Maintainers update the package names, module paths and links that the rename breaks.
  • Every extension MUST stay associated with a Working Group or Interest Group, as SEP-2133 requires.
  • The licensing, contributor license grant, trademark and antitrust terms of SEP-2133 apply at every stage.
  • Each extension MUST be listed on the extensions page of the MCP website with its current stage.

SDKs keep the autonomy SEP-2133 gives them. Extensions stay disabled by default and require explicit opt-in, at every stage.

Experimental

An Experimental extension is a prototype. Any Maintainer MAY create an experimental-ext-<name> repository, before or after a SEP is drafted.

  • Extension Maintainers MAY merge any change, including breaking changes, without notice, deprecation or Core Maintainer review.
  • Implementers MUST NOT expect two revisions of the extension to be compatible. They SHOULD pin to a tag or commit.

Core Maintainers retain the ability to archive an Experimental repository at any time.

Beta

A Beta extension is one the Core Maintainers are willing to have implementers try. The goal is real usage: implementers learn what needs to change, and the design iterates.

Entry. An Extension Maintainer, or the Working Group, submits an Extensions Track SEP under SEP-2133 and the SEP guidelines. It is reviewed like a Standards Track SEP, and SEP-2133 already requires it to identify the Working Group and Extension Maintainers and to have a reference implementation in an official SDK. When the SEP is accepted, the Core Maintainers rename the repository to beta-ext-<name>.

Iteration. After entry, Extension Maintainers approve changes without further Core Maintainer review.

  • Extensions SHOULD prefer additive changes, such as new optional fields or capability flags, over breaking changes.
  • Breaking changes are allowed but SHOULD be infrequent and SHOULD be batched.
  • Every breaking change MUST appear in a changelog with the revision that introduces it.
  • Extension Maintainers SHOULD announce a breaking change in the Working Group or Interest Group channel before merging it.
  • Extension Maintainers SHOULD maintain a public list of known implementations and the feedback received from them. This list is the evidence for the next stage.

Exit. A Beta extension advances to Stable or Core, or is archived.

Stable

A Stable extension is a released, maintained part of the MCP ecosystem that lives outside the core specification.

Entry. An Extension Maintainer, or the Working Group, submits a second Extensions Track SEP. It follows the same review and acceptance process as a Standards Track SEP. The SEP MUST:

  • Link the Beta SEP and repository, and show that the extension has been in real use, through the known-implementations list.
  • Show two independent implementations, at least one client-side and one server-side, that interoperate.
  • Carry a conformance scenario that meets the requirements of SEP-2484 for a Standards Track SEP, unless the extension has no observable protocol behavior. This SEP extends SEP-2484 to Extensions Track SEPs that move an extension to Stable.

When the SEP reaches Final, the Core Maintainers rename the repository to ext-<name>, or move the extension into an existing area repository, and the Extension Maintainers publish the first Stable revision.

Revisions. A Stable extension is published as revisions keyed to core protocol revisions, for example a specification/2026-07-28/ snapshot beside a specification/draft/ directory.

  • A published revision is immutable.
  • The extension MAY change only by publishing a new revision together with, or after, a new core protocol revision is released as Current. Proposed changes accumulate in draft/ and are visible to implementers before they take effect.
  • Extension Maintainer approval is sufficient for a new revision. The Core Maintainers do not review it.
  • Editorial fixes that do not change the requirements on any implementation MAY be made to a published revision at any time.

Compatibility. See Identifiers and compatibility.

Exit. A Stable extension may be promoted to Core or deprecated.

Core

An extension that belongs in the core protocol is promoted by a Standards Track SEP, reviewed like any other. The proposal MAY come from a Beta or a Stable extension. The SEP MUST cite the extension repository.

The feature becomes part of the core protocol in a specific core protocol revision R, for example 2027-01-01. Once R is released, the following rules apply:

  • R or later. For a request that declares R or later, the feature is core behavior. It applies whether or not the extension identifier is advertised, so a server responding under R or later does not need to advertise the extension or respond with it.
  • Earlier than R. For a request that declares an earlier revision, the extension applies as defined by its final revision. A client that still supports earlier revisions SHOULD keep advertising the extension identifier, so that a server that does not yet support R can fall back to the extension behavior.
  • When to stop. A peer stops advertising the extension when it no longer supports any revision earlier than R. There is no separate sunset period.
  • Differences. The core behavior MAY differ from the extension. The SEP MUST define the differences, so that each protocol revision has one defined behavior.

When the SEP reaches Final, the specification text lands in the core draft. The extension keeps its current stage until R is released as Current. The Core Maintainers then archive the extension repository, and its README points to the core specification section and the SEP. The archived repository keeps the extension's final revision, which remains the definition for earlier protocol revisions. From that point every change is a core specification change.

Ending an extension

  • Withdrawal or inactivity. An Extension Maintainer MAY withdraw an Experimental or Beta extension, and the Core Maintainers then archive its repository. The Core Maintainers MAY archive an Experimental or Beta repository that has had no merged change and no Working Group or Interest Group activity for six months.
  • Deprecation. A Stable extension is deprecated by an Extensions Track SEP, with the requirements, minimum window and Tier 1 SDK obligations that SEP-2596 sets for a core feature.

Archived repositories keep their name and history.

Identifiers and compatibility

This section replaces the rule in SEP-2133 that every breaking change MUST use a new identifier.

  • An extension's identifier is fixed when the extension is created and does not change when its stage changes.
  • Experimental and Beta. Extensions SHOULD prefer additive changes, such as new optional fields or capability flags in the settings object, over breaking changes. Breaking changes keep the same identifier, and the identifier carries no compatibility promise between revisions. An extension that makes a breaking change SHOULD add or increment a revision string in its settings object so that peers can detect a mismatch. An absent revision means the initial revision. A peer that receives a revision it does not know SHOULD NOT enable the extension.
  • Stable. Breaking changes appear only in a new revision, keyed to a core protocol revision. The behavior of the extension for a peer pair is the behavior defined for the protocol revision they negotiate. A revision MUST NOT change the behavior already defined for an earlier protocol revision. No new identifier is needed. A new identifier is used only when the old and new behavior must coexist under one protocol revision.
  • An extension specification MUST state which protocol revisions each of its revisions applies to.
  • Implementers of all stages SHOULD treat extension data received from a peer as untrusted, as SEP-2133 requires.

Transition

When this SEP reaches Final:

  • docs/extensions/overview.mdx and the Extensions Track description in docs/community/sep-guidelines.mdx are updated to match this SEP.
  • The extensions page lists each extension with its stage.
  • Existing repositories are classified as follows, and renamed if needed.
Repository Stage at Final
ext-apps, ext-auth Stable
ext-skills, ext-tasks Beta (renamed beta-ext-<name>)
ext-server-card Beta (renamed beta-ext-server-card), once SEP-2127 is accepted
experimental-ext-interceptors, experimental-ext-variants, experimental-ext-triggers-events, experimental-ext-tool-annotations Experimental (no change)
  • The classified extensions need no new SEP. Their existing SEPs count as the SEP for the stage assigned, and they keep their identifiers and wire behavior. ext-server-card uses SEP-2127 as its Beta SEP, so its classification takes effect when that SEP is accepted.
  • Extensions classified as Stable follow the revision rules from Final. Their next revision is keyed to the next core protocol revision, and their existing published specifications stay as they are.
  • ext-auth is a Stable area repository and keeps hosting several extensions.
  • A repository that hosts several Experimental extensions, such as experimental-ext-tool-annotations, keeps its name until one of its extensions requests Beta. That extension moves to its own repository first.

Rationale

Why put the stage in the repository name? The name is visible in every link, package reference and search result, and it cannot be changed without Core Maintainer administrator access. A README banner or label would duplicate the name and could disagree with it. The cost is renames, which GitHub redirects but which do not update package or module names. This SEP makes the Extension Maintainers responsible for those updates.

Why does Beta need a SEP, but iteration inside Beta does not? Beta is the first stage where the project's name stands behind an extension, so it gets the full review that SEP-2133 already requires for official extensions. Iteration after that follows the reasoning in SEP-2133: routing every change through Core Maintainer review would bottleneck extensions on a process that already takes months.

Why a second SEP for Stable? Stable is a stronger promise than Beta: behavior is fixed within a protocol revision. A second review lets the Core Maintainers judge the evidence from Beta (real usage, independent implementations and conformance) before making that promise.

Why can a Stable extension change only with a core protocol revision? Implementers need known dates for behavior changes. Because every request declares its protocol revision, tying extension revisions to core revisions gives a change a place to take effect without breaking peers on an earlier revision. It also matches current practice: ext-tasks snapshots its schema by core revision, and ext-skills pins its stable specification to 2026-07-28.

Why is Beta required before Stable? The purpose of Beta is real usage before the project commits to a design. The Stable evidence is easier to gather during Beta, and a Beta extension is cheap to change if the evidence shows a problem. The Core Maintainers can still waive the requirement.

Why does the extension stay advertised after promotion to Core? Protocol revisions are negotiated per request, so a client cannot know in advance whether a server supports R. Advertising the extension lets the same client work with servers on either side of R, and the core behavior needs no negotiation once both sides are on R.

Alternatives considered.

  • Beta by Core Maintainer vote instead of a SEP. Lighter, but it creates a second, unwritten approval path for official extensions and leaves no written record of the design.
  • Give Experimental and Beta extensions a distinct identifier, for example io.modelcontextprotocol.experimental/<name>. It makes the stage visible on the wire, but every stage change would force implementers to change code and identifiers. The revision setting gives most of the benefit at less cost.
  • Require a new identifier for every Stable breaking change (SEP-2133 as written). It leaves old and new behavior coexisting indefinitely and forces both to be implemented. Keying behavior to the negotiated protocol revision reuses a mechanism the protocol already has.

Prior art. Kubernetes moves features through alpha, beta and GA stages with different stability promises and deprecation windows, which SEP-2596 already cites. OpenTelemetry Collector components carry development, alpha, beta and stable stability levels. The Node.js Stability Index marks APIs as experimental or stable, among other levels.

Backward Compatibility

This SEP changes process only, and changes no message on the wire.

  • It replaces the identifier rule and the creation, iteration and promotion steps of SEP-2133 with the rules above.
  • Existing Experimental repositories are unchanged.
  • ext-skills and ext-tasks are classified as Beta. Their identifiers and wire behavior do not change, but their repositories are renamed.
  • ext-server-card is classified as Beta once SEP-2127 is accepted, and is renamed then.
  • ext-apps and ext-auth are classified as Stable. From Final, changes to their published specifications wait for a core protocol revision.

Security Implications

Experimental and Beta extensions iterate without Core Maintainer review, so an insecure design can be implemented before anyone with security expertise reads it. The controls are:

  • Extensions stay disabled by default at every stage, so an implementer opts in deliberately.
  • Beta entry requires an Extensions Track SEP, including its Security Implications section, and Stable entry requires a second full SEP review.
  • Stable changes take effect only with a core protocol revision, and are visible in draft/ beforehand.
  • Core Maintainers keep the authority to archive an extension at any stage, for example when a vulnerability is reported.

Allowing breaking changes under one identifier at Experimental and Beta stages means two implementations built against different revisions can negotiate the extension and then misparse each other's data. Implementations MUST validate extension data (SEP-2133), and the revision setting, where present, lets a peer refuse a revision it does not know.

Reference Implementation

This is a Process SEP and has no protocol implementation. On reaching Final it lands the documentation changes and repository renames listed in Transition.