diff --git a/.gitattributes b/.gitattributes index b4cbf1a4d..4a770f35d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,5 +2,5 @@ package-lock.json linguist-generated=true schema/*/schema.json linguist-generated=true docs/specification/*/schema.md linguist-generated=true docs/specification/*/schema.mdx linguist-generated=true -docs/community/seps/index.mdx linguist-generated=true -docs/community/seps/[0-9]*.mdx linguist-generated=true +docs/seps/index.mdx linguist-generated=true +docs/seps/[0-9]*.mdx linguist-generated=true diff --git a/docs/.mintlify/skills/search-mcp-github b/docs/.mintlify/skills/search-mcp-github new file mode 120000 index 000000000..cbf85adb4 --- /dev/null +++ b/docs/.mintlify/skills/search-mcp-github @@ -0,0 +1 @@ +../../../plugins/mcp-spec/skills/search-mcp-github \ No newline at end of file diff --git a/docs/clients.mdx b/docs/clients.mdx index 60ba0e3e4..bee63a7e7 100644 --- a/docs/clients.mdx +++ b/docs/clients.mdx @@ -1519,20 +1519,24 @@ mcp-use is an open source python library to very easily connect any LLM to any M -`mcpc` is a universal CLI client for MCP that maps MCP operations to intuitive commands for interactive shell use, scripts, and AI coding agents. +`mcpc` is a universal command-line client for MCP. It maps MCP operations to intuitive CLI commands, giving AI coding agents full protocol access through a single `Bash()` tool call. It works with any MCP server over Streamable HTTP or stdio, with or without a config file. Agents discover commands through `--help` without needing external skills, while MCP handles remote concerns like server discovery, authentication, payments, and access control. **Key features:** -- Swiss Army knife for MCP: supports stdio and streamable HTTP, server config files and zero config, OAuth 2.1, HTTP headers, and main MCP features. -- Persistent sessions for interaction with multiple servers simultaneously. -- Structured text output enables AI agents to explore and interact with MCP servers. -- JSON output and schema validation allow stable integration with other CLI tools, scripting, and MCP **code mode** in a shell. -- Proxy MCP server to provide AI code sandboxes with secure access to authenticated MCP sessions. +- **Code mode in the shell:** `--json` output composes with `jq`, `xargs`, and shell pipelines for writing MCP workflows as shell scripts, which can be more accurate and token-efficient than tool calling. `--schema` validates tool schemas against snapshots to detect breaking changes. +- **Progressive tool discovery:** `grep` searches tools, resources, and prompts across all active sessions with regex, so agents load only relevant tools into context. +- **Full MCP coverage:** tools, resources (including subscriptions and templates), prompts, instructions, async tasks with progress tracking and cancellation, list-change notifications, pagination, and logging control. +- **Persistent sessions:** maintain multiple simultaneous server connections via named `@sessions`, with automatic reconnection and health monitoring. +- **Authentication:** OAuth 2.1 with PKCE and dynamic client registration, bearer tokens, multiple named profiles per server, and secure credential storage in the OS keychain. +- **AI sandboxing:** built-in MCP proxy server (`--proxy`) exposes authenticated sessions to AI-generated code without leaking credentials. +- **Interactive shell:** `shell` command provides a REPL with command history, arrow-key navigation, and in-session help for exploratory server testing. +- **x402 payments (experimental):** autonomous USDC payments on Base blockchain, letting AI agents pay for tool calls via the HTTP 402 protocol. +- **Lightweight and cross-platform:** no LLM required, minimal dependencies, production-ready. Runs on macOS, Windows, and Linux. Install via `npm install -g @apify/mcpc`. @@ -2259,19 +2263,21 @@ v0 turns your ideas into fullstack apps, no code required. Describe what you wan -VS Code integrates MCP with GitHub Copilot through [agent mode](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode), allowing direct interaction with MCP-provided tools within your agentic coding workflow. Configure servers in Claude Desktop, workspace or user settings, with guided MCP installation and secure handling of keys in input variables to avoid leaking hard-coded keys. +VS Code integrates MCP with GitHub Copilot [agents](https://code.visualstudio.com/docs/copilot/agents/overview), which plan, write code, and verify results across your project. Install MCP servers from the built-in gallery or configure them in workspace (`.vscode/mcp.json`) or user settings, with secure handling of keys via input variables. **Key features:** -- Support for stdio and server-sent events (SSE) transport -- Per-session selection of tools per agent session for optimal performance -- Easy server debugging with restart commands and output logging -- Tool calls with editable inputs and always-allow toggle -- Integration with existing VS Code extension system to register MCP servers from extensions +- MCP server gallery in the Extensions view for one-click install and discovery +- Support for stdio, SSE, and streamable HTTP transports +- Sandbox mode for stdio servers on macOS and Linux to restrict file system and network access +- MCP Apps for interactive UI components like forms and visualizations rendered in chat +- Per-session tool selection, editable inputs, and auto-approve toggle +- Enterprise management of MCP server access via GitHub policies +- Settings Sync support to share MCP configuration across devices diff --git a/docs/community/seps/2243-http-standardization.mdx b/docs/community/seps/2243-http-standardization.mdx new file mode 100644 index 000000000..1ccc6483d --- /dev/null +++ b/docs/community/seps/2243-http-standardization.mdx @@ -0,0 +1,773 @@ +--- +title: "SEP-2243: Untitled" +sidebarTitle: "SEP-2243: Untitled" +description: "Untitled" +--- + +
+ + Draft + + + Standards Track + +
+ +| Field | Value | +| ------------- | ------------------------------------------------------------------------------- | +| **SEP** | 2243 | +| **Title** | Untitled | +| **Status** | Draft | +| **Type** | Standards Track | +| **Created** | 2026-02-04 | +| **Author(s)** | MCP Transports Working Group | +| **Sponsor** | None | +| **PR** | [#2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243) | + +--- + +## 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 | +| `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. + +**Case Sensitivity**: Header names (called "field names" in [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110#name-field-names)) are case-insensitive. Clients and servers MUST use case-insensitive comparisons for header names. + +#### Example: tools/call Request + +```http +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 + +```http +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 + +```http +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: + +```http +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" + } + } +} +``` + +### 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 contain only ASCII characters (excluding space and `:`) +- MUST be case-insensitively unique among all `x-mcp-header` values in the `inputSchema` +- MUST only be applied to parameters with primitive types (number, string, boolean) + +Clients 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. + +**Example Tool Definition**: + +```json +{ + "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**: + +```json +{ + "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**: + +```http +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**: + +```json +{ + "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**: + +```http +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**: + +```json +{ + "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**: + +```http +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](https://datatracker.ietf.org/doc/html/rfc9110#name-field-values), 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 + - `number`: Convert to decimal string representation (e.g., `42`, `3.14`) + - `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: + +```text +Mcp-Param-{Name}: =?base64?{Base64EncodedValue}?= +``` + +The prefix `=?base64?` and suffix `?=` indicate that the value is Base64-encoded. Servers and intermediaries that need to inspect these values MUST decode them accordingly. + +**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=?=` | + +#### 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`) +1. Append the `Mcp-Method` header and, if applicable, `Mcp-Name` header to the request +1. Inspect the tool's `inputSchema` for properties marked with `x-mcp-header` and extract the value for each parameter +1. Encode the values according to the rules in [Value Encoding](#value-encoding) +1. Append a `Mcp-Param-{Name}: {Value}` header to the request: + +#### 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](#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**: + +```json +{ + "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. + +**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/')` | +| 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: + +```python +# 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](https://datatracker.ietf.org/doc/html/rfc9110)) 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 using the new or later version that are missing required headers. Servers MAY support older clients by accepting requests without headers when negotiating an older protocol version. + +Intermediate servers can only rely on header values when the `Mcp-Protool-Version` header is present and set the new protocol version or later. + +### 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](#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 | +| Malformed wrapper | `=?BASE64?SGVsbG8=?=` | Server MUST accept (case-insensitive prefix) | + +#### 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 diff --git a/docs/community/skills-over-mcp/charter.mdx b/docs/community/skills-over-mcp/charter.mdx new file mode 100644 index 000000000..a056e23e2 --- /dev/null +++ b/docs/community/skills-over-mcp/charter.mdx @@ -0,0 +1,136 @@ +--- +title: Skills Over MCP Charter +description: Charter for the MCP Skills Over MCP Working Group. +--- + +## Group Type + +**Working Group** + +## Mission Statement + +The Skills Over MCP Working Group defines how "agent skills" — rich, structured +instructions for agent workflows — are discovered, distributed, and consumed through MCP. +Native skills support in host applications demonstrates strong demand, and the group +emerged from discussion on [SEP-2076 — Agent Skills as a First-Class MCP Primitive](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2076), +which raised open questions about whether existing MCP primitives suffice or what +conventions to standardize. The WG produces specification extensions, reference +implementations, and coordination artifacts because solutions touch the protocol spec, +registry schema, SDK implementations, and client behavior. + +## Scope + +### In Scope + +- **Specification Work**: SEPs defining how skills are represented, discovered, and + consumed within MCP — including the Skills Extension (Extensions Track) and related + protocol changes +- **Reference Implementations**: SDK components and reference servers demonstrating + skill discovery and consumption patterns +- **Cross-Cutting Concerns**: Coordination with Registry WG (skills discovery/distribution, + registry schema changes), Agents WG (skill activation, server metadata consumption), + Primitive Grouping WG (progressive disclosure patterns), and external projects including + the [Agent Skills](https://agentskills.io/) spec (content format and well-known URI + discovery), FastMCP, and PydanticAI +- **Documentation**: Specification sections and guidance covering skill authoring, + discovery, and consumption + +### Out of Scope + +- **Registry schema decisions**: Schema ownership belongs to the Registry WG; this WG + contributes requirements but does not own the schema +- **Client implementation mandates**: We can document patterns but not require specific + client behavior +- **Plugin/bundle packaging**: Installable bundles (skills + servers + subagents + + configuration as a single artifact) — surfaced by use cases but belongs to a broader + packaging effort + +### Related Groups + +- **Agents WG** — How agents consume server metadata, skill activation +- **Registry WG** — Skills discovery/distribution, registry schema changes +- **Primitive Grouping WG** — Progressive disclosure patterns + +## Leadership + +| Role | Name | Organization | GitHub | Term | +| ---- | --------------- | --------------------------- | ---------------------------------------- | ------- | +| Lead | Ola Hungerford | Nordstrom / MCP Maintainer | [@olaservo](https://github.com/olaservo) | Initial | +| Lead | Peter Alexander | Anthropic / Core Maintainer | [@pja-ant](https://github.com/pja-ant) | Initial | + +## Authority & Decision Rights + +| 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 | + +## Membership + +| Name | Organization | GitHub | Discord | Level | +| ------------------------ | ------------------------------- | ------------------------------------------------------ | ------- | ----------- | +| Ola Hungerford | Nordstrom / MCP Maintainer | [@olaservo](https://github.com/olaservo) | | Lead | +| Peter Alexander | Anthropic / Core Maintainer | [@pja-ant](https://github.com/pja-ant) | | Lead | +| Yu Yi | Google | [@erain](https://github.com/erain) | | Participant | +| Sunish Sheth | Databricks | [@sunishsheth2009](https://github.com/sunishsheth2009) | | Participant | +| Keith A Groves | Hyix | [@keithagroves](https://github.com/keithagroves) | | Participant | +| Peder Holdgaard Pedersen | Saxo Bank / MCP Maintainer | [@pederhp](https://github.com/pederhp) | | Participant | +| Sam Morrow | GitHub | [@SamMorrowDrums](https://github.com/SamMorrowDrums) | | Participant | +| Jacob MacDonald | Google | [@jakemac53](https://github.com/jakemac53) | | Participant | +| Jonathan Hefner | Independent / MCP Maintainer | [@jonathanhefner](https://github.com/jonathanhefner) | | Participant | +| Luca Chang | AWS / MCP Maintainer | [@LucaButBoring](https://github.com/LucaButBoring) | | Participant | +| Bob Dickinson | TeamSpark.ai / MCP Maintainer | [@BobDickinson](https://github.com/BobDickinson) | | Participant | +| Radoslav Dimitrov | Stacklok / MCP Maintainer | [@rdimitrov](https://github.com/rdimitrov) | | Participant | +| Juan Antonio Osorio | Stacklok | [@JAORMX](https://github.com/JAORMX) | | Participant | +| Kaxil Naik | Astronomer / Apache Airflow PMC | [@kaxil](https://github.com/kaxil) | | Participant | +| Cliff Hall | Futurescale | [@cliffhall](https://github.com/cliffhall) | | Participant | + +## Operations + +| Meeting | Frequency | Duration | Purpose | +| --------------- | ----------------- | ---------- | ----------------------------------------------------------------------------------- | +| Working Session | Weekly (Tuesdays) | 60 minutes | Technical discussion, pattern evaluation, proposal review; open to all participants | + +Default start time is 9:00 AM Pacific. Sessions may occasionally be scheduled earlier to better accommodate non-US time zones. + +Meetings are published at [meet.modelcontextprotocol.io](https://meet.modelcontextprotocol.io). Agendas are posted in advance per MCP meeting policy. Meeting notes are published to [Meeting Notes — Skills Over MCP WG](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/categories/meeting-notes-skills-over-mcp-wg). + +Discord: [#skills-over-mcp-wg](https://discord.com/channels/1358869848138059966/1464745826629976084) + +## Resources + +- Experimental findings and reference implementations: [modelcontextprotocol/experimental-ext-skills](https://github.com/modelcontextprotocol/experimental-ext-skills) +- Project board: [Skills Over MCP WG](https://github.com/orgs/modelcontextprotocol/projects/38/views/1) + +## Deliverables & Success Metrics + +### Active Work Items + +Full live tracking is on the [Skills Over MCP WG project board](https://github.com/orgs/modelcontextprotocol/projects/38/views/1). Headline workstreams: + +| Item | Status | Target Date | Champion | +| ---------------------------------------------- | ----------- | ----------- | -------------------------------------------------------------------------------------------- | +| Skills Extension SEP (draft, Extensions Track) | In Review | | [@pja-ant](https://github.com/pja-ant) | +| Skills Extension reference implementation | In Review | | [@olaservo](https://github.com/olaservo) | +| Agent Skills spec coordination | In Progress | | [@jonathanhefner](https://github.com/jonathanhefner), [@pja-ant](https://github.com/pja-ant) | +| Registry skills.json proposal | In Progress | | [@JAORMX](https://github.com/JAORMX) | + +### Success Criteria + +- **Short-term**: Documented consensus on requirements and evaluation of existing approaches +- **Medium-term**: Clear recommendation (convention vs. protocol extension vs. both) — the draft Skills Extension SEP represents the WG's current direction: a formal extension using existing Resources primitives +- **Long-term**: Interoperable skill distribution across MCP servers and clients + +## Changelog + +| Date | Change | +| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-04-16 | Converted from Interest Group to Working Group | +| 2026-04-14 | Initial charter (formalized from [experimental-ext-skills](https://github.com/modelcontextprotocol/experimental-ext-skills) repo README, which served as the de facto charter before the charter process was established via [SEP-2149](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2149)) | +| 2026-02-01 | IG formed; experimental repo created | diff --git a/docs/docs.json b/docs/docs.json index e02527a78..c33cf0448 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -425,7 +425,8 @@ { "group": "Draft", "pages": [ - "seps/2557-tasks-stabilization" + "seps/2243-http-standardization", + "seps/2557-adapt-tasks-for-stateless-and-sessionless-protocol" ] } ] @@ -463,6 +464,7 @@ "pages": [ "community/inspector-v2/charter", "community/server-card/charter", + "community/skills-over-mcp/charter", "community/triggers-events/charter" ] }, diff --git a/docs/seps/2149-working-group-charter-template.mdx b/docs/seps/2149-working-group-charter-template.mdx index 4f9b49afc..110bb05e4 100644 --- a/docs/seps/2149-working-group-charter-template.mdx +++ b/docs/seps/2149-working-group-charter-template.mdx @@ -228,7 +228,7 @@ with the Core Maintainers in a core maintainer meeting. - There must be a widely acknowledged concern requiring coordination - PR for creation of WG into `docs/community//overview.mdx`, gated by CODEOWNERS requiring approval by Maintainers -- PR for charter into `docs/community//charter.mdx`, gated by CODEOWNERS requiring approval from Core Maintainers +- PR for charter into `docs/community//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:** diff --git a/docs/seps/2243-http-standardization.mdx b/docs/seps/2243-http-standardization.mdx new file mode 100644 index 000000000..d1cc22a58 --- /dev/null +++ b/docs/seps/2243-http-standardization.mdx @@ -0,0 +1,787 @@ +--- +title: "SEP-2243: HTTP Header Standardization for Streamable HTTP Transport" +sidebarTitle: "SEP-2243: HTTP Header Standardization for Streama…" +description: "HTTP Header Standardization for Streamable HTTP Transport" +--- + +
+ + Draft + + + Standards Track + +
+ +| Field | Value | +| ------------- | ------------------------------------------------------------------------------- | +| **SEP** | 2243 | +| **Title** | HTTP Header Standardization for Streamable HTTP Transport | +| **Status** | Draft | +| **Type** | Standards Track | +| **Created** | 2026-02-04 | +| **Author(s)** | MCP Transports Working Group | +| **Sponsor** | None | +| **PR** | [#2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243) | + +--- + +## 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. + +**Case Sensitivity**: Header names (called "field names" in [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110#name-field-names)) are case-insensitive. Clients and servers MUST use case-insensitive comparisons for header names. + +#### Example: tools/call Request + +```http +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 + +```http +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 + +```http +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: + +```http +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: + +```http +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 contain only ASCII characters (excluding space and `:`) +- MUST be case-insensitively unique among all `x-mcp-header` values in the `inputSchema` +- MUST only be applied to parameters with primitive types (number, string, boolean) + +Clients 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. + +**Example Tool Definition**: + +```json +{ + "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**: + +```json +{ + "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**: + +```http +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**: + +```json +{ + "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**: + +```http +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**: + +```json +{ + "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**: + +```http +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](https://datatracker.ietf.org/doc/html/rfc9110#name-field-values), 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 + - `number`: Convert to decimal string representation (e.g., `42`, `3.14`) + - `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: + +```text +Mcp-Param-{Name}: =?base64?{Base64EncodedValue}?= +``` + +The prefix `=?base64?` and suffix `?=` indicate that the value is Base64-encoded. Servers and intermediaries that need to inspect these values MUST decode them accordingly. + +**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=?=` | + +#### 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`) +1. Append the `Mcp-Method` header and, if applicable, `Mcp-Name` header to the request +1. Inspect the tool's `inputSchema` for properties marked with `x-mcp-header` and extract the value for each parameter +1. Encode the values according to the rules in [Value Encoding](#value-encoding) +1. Append a `Mcp-Param-{Name}: {Value}` header to the request: + +#### 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](#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**: + +```json +{ + "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. + +**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/')` | +| 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: + +```python +# 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](https://datatracker.ietf.org/doc/html/rfc9110)) 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](#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 | +| Malformed wrapper | `=?BASE64?SGVsbG8=?=` | Server MUST accept (case-insensitive prefix) | + +#### 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 diff --git a/docs/seps/2557-adapt-tasks-for-stateless-and-sessionless-protocol.mdx b/docs/seps/2557-adapt-tasks-for-stateless-and-sessionless-protocol.mdx new file mode 100644 index 000000000..faa60360c --- /dev/null +++ b/docs/seps/2557-adapt-tasks-for-stateless-and-sessionless-protocol.mdx @@ -0,0 +1,758 @@ +--- +title: "SEP-2557: Adapat Tasks for Stateless & Sessionless Protocol" +sidebarTitle: "SEP-2557: Adapat Tasks for Stateless & Sessionles…" +description: "Adapat Tasks for Stateless & Sessionless Protocol" +--- + +
+ + Draft + + + Standards Track + +
+ +| Field | Value | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| **SEP** | 2557 | +| **Title** | Adapat Tasks for Stateless & Sessionless Protocol | +| **Status** | Draft | +| **Type** | Standards Track | +| **Created** | 2026-04-12 | +| **Author(s)** | Luca Chang ([@LucaButBoring](https://github.com/LucaButBoring)), Caitie McCaffrey ([@CaitieM20](https://github.com/CaitieM20)) | +| **Sponsor** | Caitie McCaffrey ([@CaitieM20](https://github.com/CaitieM20)) | +| **PR** | [#2557](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2557) | + +--- + +## Abstract + +This SEP incorporates changes into `Tasks` necessary following the acceptance of: + +- [SEP-2260: Require Server requests to be associated with a Client request](./2260-Require-Server-requests-to-be-associated-with-Client-requests.md) +- [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322) +- [SEP-2243: Http Standardization](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243) +- [SEP-2567: Sessionless MCP via Explicit State Handles](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567). + +It also proposes a simplification of [tasks](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) based on feedback since the initial experimental release. + +This SEP DOES NOT move Tasks out of Experimentation status. + +## Motivation + +A number of changes are necessary in the `Tasks` feature to support the upcoming changes to the spec listed in the SEPs above, and to address issues identified since the initial experimental release of `Tasks` in the `2025-11-25` specification release. + +### Changes to support upcoming SEPs + +A number of changes are necessay to support the upcoming changes to the spec listed in the SEPs above. + +[SEP-2260] disallows unsolicited server-to-client requests, which +invalidates the concept of client tasks for elicitation and sampling operations. +This proposal removes client-hosted tasks & their associated capabiliteis since +these scenarios are no longer supported. + +[SEP-2567] removes sessions from the protocol, which was the only defined +scope for `tasks/list`. This proposal removes `tasks/list` methods and +capabilities since there is no clear way to scope a list of tasks between a +server and a client. + +[SEP-2322] introduces a new flow for how the server requests more input +from the client, i.e. `elicitations`, `sampling`, and `roots`. SEP-2322 +leverages the existing `input_required` task status to signal when the server +needs more information. The client then makes a request to `tasks/result` to +retrieve the `IncompleteResult` which contains the server's request for more +information. The current specification requires the client to make this +`tasks/result` request in a blocking manner, which is unintuitive and has led to +implementation issues. We identified that we would need to make a breaking +change to `tasks/result` during SEP-2322 discussions, but decided redesigning +tasks would be a scope expansion that would derail MRTR discussion. This +proposal simplifies this flow by inlining the `IncompleteResult` into the +`tasks/get` response when the task is in an `input_required` state, eliminating +the need for a separate blocking request and improving the intuitiveness of the +flow. + +Given the difficulty of implementing the MRTR changes in Tasks as the spec currently stands, we believe that simplifying the Tasks flow by collapsing several methods into `tasks/get` is necessary and is also proposed here. + +[SEP-2243] introduces standard headers in the Streamable HTTP Transport +to facilitate more efficient routing. Routing on `TaskId` is also desirable +since there is often state associated with a specific Task that needs to be +consistently routed to the same server instance. This proposal requires that the +`Mcp-Name` header MUST be set to the value of `params.taskId` by the client when +making `tasks/get` and `tasks/cancel` requests over the Streamable HTTP +Transport. + +### Lessons Learned from Implementation & Usage Feedback + +`Tasks` were introduced in an experimental state in the `2025-11-25` specification release, serving as an alternate execution mode for certain request types (tool calls, elicitation, and sampling) to enable polling for the result of a task-augmented operation. + +#### Current Task Flow Overview + +This is done according to the following process, using tool calls as an example: + +1. Check the receiver's task capabilities for the request type. If the capability is not present, the receiver does not support tasks. +1. Invoke `tools/list` to retrieve the tool list, then check `execution.taskSupport` on the appropriate tool to determine if the tool supports tasks. +1. The client issues a `tools/call` request to the server, declaring the `task` parameter with a desired task TTL to indicate that the server should create a task to represent that work. +1. The server returns a `CreateTaskResult` with the task info for the client to look up the result later. This contains a suggested polling interval that the client should follow to avoid overwhelming the server. +1. The client polls `tasks/get` repeatedly according to the suggested polling interval. +1. If the task status is ever `input_required` during this phase, the client prematurely issues a `tasks/result` call to the server, which is expected to block until the task result is available: + 1. The server sends some request message to the client concurrently with this premature request. In stdio, this is effectively meaningless, but in Streamable HTTP, this allows the server to open an SSE side channel to send that pending request on. + 1. The client receives the request and sends a response, according to the standard conventions of the transport in use. This is completely disconnected from the (still-ongoing) `tasks/result` call. + 1. Once the server receives the result it needs, it transitions the task back to the `working` status. +1. Once the task status is `completed`, the client issues a `tasks/result` call to retrieve the final result. + 1. If the client still has an active `tasks/result` call from a prior `input_required` status, it will receive the result as the result to that open request. + +#### Current Task Flow Problems + +The current flow has a number of problems: + +1. `tasks/result` is overloaded and calling it to retrieve server requests when in the `input_required` status is unintuitive. +2. `tasks/result` is expected to block until the task is completed if called prematurely. This has led to [implementation issues](https://github.com/modelcontextprotocol/java-sdk/pull/755#issuecomment-3806079033). Requires long lived persistent connections, which many clients & servers do not want to implement. This problem still exists even with MRTR. +3. The flow is inefficient. Clients must make multiple calls to `tasks/get` to check on the status and then to `tasks/result` to retrieve the final result or required input. + +Task Creation also has a number of issues since it is client-directed instead of being server-directed.Today the client must declare that it wants a task to be created by including the `task` field in the request. This creates several issues: + +1. It requires requestors to explicitly check the capabilities of receivers. This introduces an unnecessary state contract that may be violated during mid-session deployments under the Streamable HTTP transport, and also raises concerns about the capability exchange growing in payload size indefinitely as more methods are supported. +2. It requires a tool-specific behavior carveout which gets pushed onto the client to navigate. Related to this, it forces clients to cache a `tools/list` call prior to making any task-augmented tool call. +3. It requires host application developers to explicitly choose to opt into task support from request to request, rather than relying on a single, consistent request construction path for all protocol operations. +4. It creates unnecessary burden on server developers to handle both the task and non-task flow for the same tool call. Most Tools will either return a `Task` or not, no use case has emerged where a server would want to return a `Task` for some calls and not others for the same tool, so this flexibility is unnecessary. + +## Specification + +To support the new SEPs and solve the issues outlined above we propose the follwoing changes to the `Tasks` specification: + +1. Removes Features made obsolete by upcoming SEPs. +2. Task Creation is determined by the Server. +3. Servers MUST support `task/cancel` operation. +4. Removal of Task Capabilities that are no longer necessary. +5. Simplified Task Polling Flow which consolidates all polling onto the `tasks/get` method. + +### Remove features made obsolete by upcoming SEPs + +1. We will remove the concept of client-hosted tasks (Sampling & Elicitation), as SEP-2260 disallows unsolicited server-to-client requests +2. We will remove the optional `tasks/list` operation, as SEP-2567 removes sessions which was the only defined scope for listing tasks between a server and a client. We may expand task support to additional client-to-server request types in the future, and implementors are still advised against implementing tasks as a tool-specific protocol operation. + +### Task Creation Changes + +Tasks will no longer be an optional capability, but will instead become a standard part of the protocol that servers MAY choose to implement. If a Tool returns a Task, Servers MUST declare `execution.tasksupport` on the tool definition. + +Clients MUST understand Tasks, however they are not required to implement the polling workflow and leverage tools with `execution.taskSupport`. Clients MAY choose to filter out tools with `execution.taskSupport` declared. + +1. We will remove the `tasks` capability declaration on both the client and the server. +1. Servers MAY return `CreateTaskResult` or a `CallToolResult`in response to `CallToolRequest`. +1. Servers SHOULD ignore the `task` field if it is present in a `CallToolRequest`. +1. If a server returns a `CreateTaskResult`, it MUST support the `tasks/get` and `tasks/cancel` methods to allow clients to poll for the result and cancel the task if desired. + +### Task Cancel Changes + +We propose aligning Task Cancellation with the Cooperative Cancellation model. In this model the requestor The requestor signals intent, but the worker decides when or if to honor it. This alligns `tasks/cancel with the cancellation pattern used in `notifications/cancelled`, where clients can always send a cancellation request, but servers can choose how to handle it. This also aligns Tasks with how Cancellation Tokens are handled in most async/concurrent programming languages including Go, Python, C#, TypeScript, etc... + +In the specification this means: + +Servers MUST support the `task/cancel` operation. A server MAY choose to ignore cancellation requests if its incapable or unwilling to offer cancellation of that Task. + +### Task Capabilities Changes + +This SEP Removes all capabilities related to Tasks. + +- SEP-2567 makes `tasks/list` impossible to support. +- SEP-2260 disallows unsolicited server-to-client requests, which invalidates the concept of client tasks for elicitation and sampling operations. +- This SEP makes Tasks a standard part of the protocol, and not a negotiated capability. +- This SEP Makes `tasks/cancel` required to be supported, even if a server does not wish to support tasks. + +The below table summarizes the changes to the task-related capabilities: + +| Role | Capability | Status | Description | +| ------ | --------------------------------------- | ------- | ------------------------------------------------ | +| Server | `tasks.requests.tools.call` | removed | part of core protocol not an optional capability | +| Server | `tasks.cancel` | removed | `tasks/cancel` is required | +| Server | `tasks.list` | removed | no longer supported SEP-2567 | +| Client | `tasks.requests.sampling.createMessage` | removed | no longer supported SEP-2260 | +| Client | `tasks.requests.elicitation.create` | removed | no longer supported SEP-2260 | +| Client | `tasks.cancel` | removed | no longer needed, no client tasks | +| Client | `tasks.list` | removed | no longer supported SEP-2567 | + +### Task Flow Change + +We propose simplifying the Task Flow into two methods: `tasks/get` and `tasks/cancel`. + +- The `tasks/get` methods will handle retrieving task statuses and results simultaneously, and will additionally act as the carrier for receiver-to-requestor requests for the purposes of SEP-2322: Multi Round-Trip Requests. This simplifies the flow and allows for polling or streaming updates on a single endpoint. Moreover this more closely matches Long Running Operation APIs where there is a single endpoint that is polled for the status of an operation. +- The `tasks/cancel` method will be required and a separate endpoint. + +Below is an example of the new Task Flow. + +```mermaid +sequenceDiagram + participant U as User + participant C as Client + participant S as Server + C->>S: tools/call (id: 1) + S-->>C: CreateTaskResult (id: 1, taskId: 123, status: working) + C->>S: tasks/get (taskId: 123) + S-->>C: GetTaskResult (taskId: 123, status: working) + C->>S: tasks/get (taskId: 123) + S-->>C: GetTaskResult (taskId: 123, status: input_required, inputRequests: {...}) + C->>U: Prompt User for Input + U-->>C: Provided Input + C->>S: tasks/get (taskId: 123, inputResponses: {...}) + S-->>C: GetTaskResult (taskId: 123, status: working) + C-->>S: tasks/get (taskId: 123) + S-->>C: GetTaskResult (taskId: 123, status: completed, result: {...}) +``` + +We will make the following changes to the specification to support this. + +1. We will remove the requirement that requestors react to the input_required status by prematurely invoking tasks/result to side-channel requests on an SSE stream in the Streamable HTTP transport. +2. We will inline the final task result or error into the Task shape, bringing that into tasks/get and all notifications by extension. +3. We will inline outstanding server-to-client requests into a new inputRequests field on GetTaskResult, akin to the field of the same name used in SEP-2322. +4. We will inline mid-task client-to-server results into a new inputResponses field on GetTaskRequest, akin to the field of the same name used in SEP-2322. +5. We will remove the tasks/result method. +6. We will update error handling to remove the notion that a task can be failed due to non-JSON-RPC errors such as a tool result with isError: true to maintain a strong separation between protocol-level faults and application-level faults. + +```diff +-For failures, the `error` field **MUST** contain the JSON-RPC error that occurred during execution, or the task **MAY** use `status: "failed"` with a `statusMessage` for tool results with `isError: true`. ++For failures, the `error` field **MUST** contain the JSON-RPC error that occurred during execution. The `failed` status **MUST NOT** be used to represent non-JSON-RPC errors, such as a tool result that completed with with `isError: true`. +``` + +#### task/get Specification + +We will add the following language to the specification to define the new flow and the expected behavior of `tasks/get`: + +Upon receiving a `tasks/get` request with `inputResponses`, the server MUST process the provided responses and update the task state accordingly. The server MAY choose to transition the task back to `working` status if it determines that the provided input is sufficient to continue processing. + +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 [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322). 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. + +The below section contains example responses for each of the above cases. + +Status: Working + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "working", + "statusMessage": "The operation is in progress.", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 60000, + "pollInterval": 5000 + } +} +``` + +Status: Completed + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "completed", + "statusMessage": "The operation has completed successfully.", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 60000, + "pollInterval": 5000, + "result": { + "content": [ + { + "type": "text", + "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy" + } + ], + "isError": false + } + } +} +``` + +Status: Failed + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "failed", + "statusMessage": "Tool execution failed: API rate limit exceeded", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 30000, + "error": { + "code": -32603, + "message": "API rate limit exceeded" + } + } +} +``` + +Status: Cancelled + +```json +{ + "jsonrpc": "2.0", + "id": 6, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "cancelled", + "statusMessage": "The task was cancelled by request.", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 30000, + "pollInterval": 5000 + } +} +``` + + Status: input_required + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "input_required", + "statusMessage": "The operation requires additional input.", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 60000, + "pollInterval": 5000, + "inputRequests": { + "github_login": { + "method": "elicitation/create", + "params": { + "mode": "form", + "message": "Please provide your GitHub username", + "requestedSchema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"] + } + } + } + } + } +} +``` + +### Task Schema Changes + +The `Task` schema defining the task metadata gains an optional `requestState` (see SEP-2322). We additionally introduce new derived types that inline `result`/`error`/`inputRequests`, to be used by `tasks/get` and `notifications/tasks/status`. This allows us to avoid introducing redundant/bloated fields in `CreateTaskResult`. + +```typescript +interface Task { + // Existing fields... + /** + * Optional field containing request state passed back from the server to the client (see SEP-2322). + */ + requestState?: string; +} + +interface InputRequiredTask extends Task { + status: "input_required"; + /** + * Field containing the InputRequests that specify the additional information needed from the client. Present + * only when task status is `input_required` (see SEP-2322). + */ + inputRequests: InputRequests; +} + +interface CompletedTask extends Task { + status: "completed"; + /** + * The final result of the task. Present only when status is "completed". + * The structure matches the result type of the original request. + * For example, a {@link CallToolRequest | tools/call} task would return the {@link CallToolResult} structure. + */ + result: JSONObject; +} + +interface FailedTask extends Task { + status: "failed"; + /** + * The error that caused the task to fail. Present only when status is "failed". + */ + error: JSONObject; +} + +type DetailedTask = Task | InputRequiredTask | CompletedTask | FailedTask; +``` + +#### Client Requests for `task/get` + +```typescript +interface GetTaskRequest extends JSONRPCRequest { + method "tasks/get"; + params: { + /** + * The task identifier to query. + */ + taskId: string; + /** + * Optional field to allow the client to respond to a server's request for more information + * when the task is in `input_required` state (see SEP-2322). + */ + inputResponses?: InputResponses; + /** + * Optional field containing request state passed to the server from the client (see SEP-2322). + */ + requestState?: string; + }; +} +``` + +#### Server Response for `task/get` + +```typescript +type GetTaskResult = Result & DetailedTask; + +type TaskStatusNotificationParams = NotificationParams & DetailedTask; +``` + +#### `ResultType` + +The ResultType field was introduced in [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322) to handle polymorphic results. `Tasks` has the same issue where a server may return a `CallToolResult` or a `CreateTaskResult`. To address this, we propose the addition of the `task` ResultType to indicate that a Response contains a `Task` object. + +```typescript +type ResultType = "complete" | "incomplete" | "task"; +``` + +For backwards compatibility ResultType is inferred by default to be `complete`. Therefore all calls which return a `Task` (i.e.`task/get`, `task/cancel`) calls must set `task` as the ResultType moving forward. + +Below (collapsed) is the full JSON example of a tool call with an unsolicited task-augmentation that matches the diagram above: + +### Example Task 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: + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "hello_world", + "arguments": {} + } +} +``` + +The server determines (via bespoke logic) that it wants to create a task to represent this work, and it immediately returns a `CreateTaskResult`: + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "resultType": "task", + "result": { + "task": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "working", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:50:00Z", + "ttl": 30000, + "pollInterval": 5000 + } + } +} +``` + +Once the client receives the `CreateTaskResult`, it begins polling `tasks/get`: + +```json +{ + "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: + +```json +{ + "jsonrpc": "2.0", + "id": 3, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "working", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:50:00Z", + "ttl": 30000, + "pollInterval": 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, and may additionally provide a `requestState` if it so chooses. On the next `tasks/get` request from the client, the server sends the elicitation payload via the `inputRequests` field. Note that, unlike in [SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322), the standard task status result is still returned. The updated task polling flow should be thought of as distinct from the MRTR flow, despite sharing many characteristics. + +```json +{ + "jsonrpc": "2.0", + "id": 4, + "method": "tasks/get", + "params": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840" + } +} +``` + +```json +{ + "id": 4, + "jsonrpc": "2.0", + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "input_required", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:50:00Z", + "ttl": 30000, + "pollInterval": 5000, + "inputRequests": { + "name": { + "method": "elicitation/create", + "params": { + "mode": "form", + "message": "Please enter your name.", + "requestedSchema": { + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"] + } + } + } + }, + "requestState": "foo" + } +} +``` + +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): + +```json +{ + "jsonrpc": "2.0", + "id": 5, + "method": "tasks/get", + "params": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "requestState": "foo" + } +} +``` + +```json +{ + "id": 5, + "jsonrpc": "2.0", + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "input_required", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:50:00Z", + "ttl": 30000, + "pollInterval": 5000, + "inputRequests": { + "name": { + "method": "elicitation/create", + "params": { + "mode": "form", + "message": "Please enter your name.", + "requestedSchema": { + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"] + } + } + } + }, + "requestState": "foo" + } +} +``` + +The user enters their name, and the client makes a new `tasks/get` request with the satisfied information: + +```json +{ + "jsonrpc": "2.0", + "id": 6, + "method": "tasks/get", + "params": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "inputResponses": { + "name": { + "action": "accept", + "content": { + "input": "Luca" + } + } + }, + "requestState": "foo" + } +} +``` + +With the elicitation fulfilled and no other outstanding requests to send, the server moves the task back into the `"working"` status: + +```json +{ + "jsonrpc": "2.0", + "id": 6, + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "working", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:50:00Z", + "ttl": 30000, + "pollInterval": 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: + +```json +{ + "jsonrpc": "2.0", + "id": 7, + "method": "tasks/get", + "params": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840" + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "id": 7, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "completed", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:50:00Z", + "ttl": 30000, + "pollInterval": 5000, + "result": { + "content": [ + { + "type": "text", + "text": "Hello, Luca!" + } + ], + "isError": false + } + } +} +``` + +### HTTP Streamable Transport Headers + +[SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243) introduces standard headers in the Streamable HTTP Transport to facilitate more efficient routing. Routing on `TaskId` is also desirable since there is often state associated with a specific Task that needs to be consistently routed to the same server instance. SEP-2243 requires that all requests and notifications declare an `Mcp-Method` header. + +We will extend this with semantics for the `tasks/get` and `tasks/cancel` requests, requiring that the `Mcp-Name` header MUST be set to the value of `params.taskId` by the client when making `tasks/get` and `tasks/cancel` requests over the Streamable HTTP Transport. + +## Rationale + +### Removing `tasks/result` + +Removing `tasks/result` was the logical conclusion of this proposal after inlining the result/error into `tasks/get`. As an alternative, we could have left `tasks/get` unchanged and left `tasks/result` in solely as a method for late result retrieval. This would have incentivized using `tasks/get` as a general method of retrieving the task state and result simultaneously, rendering `tasks/result` obsolete regardless. + +Furthermore, the greatest flaw of `tasks/result` today is its blocking requirement, and leaving that in place would leave the general implementation headache of dealing with that unsolved. If we chose to instead remove the blocking requirement in favor of an "incomplete" error, we could get away with leaving `tasks/result` in place in a deprecated state, but then we would have been making a breaking change to it anyways. + +### Removing `tasks/list` + +Given that tasks are intended to be bound to their creator in some way (the current specification is that they should be bound to the "authorization context" if possible), removing `tasks/list` avoids complications where that context is not well-defined. As a specific example: If a task ID is unique and resistant to guessing, but does not have a "user" associated with it, the server running that task knows subsequent requests containing that task ID are owned by the caller. However, it does not know if two separate tasks with different IDs are from the same caller unless it can correlate them via some additional property (e.g. some variety of session state). + +As a second point, we assert that all common use cases for `tasks/list` can in fact be satisfied by the client application, similarly to the argument for removing sessions posed in [SEP-2567: Sessionless MCP via Explicit State Handles](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567). An end-user application that needs to retain a list of tasks associated with a conversation can store task IDs it receives alongside its own persisted conversation state; one that wishes to maintain a list of tasks to reference across conversations can aggregate that list on its own. + +### Removing Client-Hosted Tasks + +Under [SEP-1686](./1686-tasks.md), clients could optionally offer their own task-hosting support on elicitation and sampling operations; this was not foreseen as particularly useful in its own right, but rather was intended to avoid coupling tasks to the assumptions imposed by tool calls (specifically, to remove any incentive to adding tool-specific requirements to tasks themselves). However, as of [SEP-2260](./2260-Require-Server-requests-to-be-associated-with-Client-requests.md), task-augmented elicitation and sampling are conceptually invalid. Put simply, SEP-2260 disallows unsolicited server-to-client requests; any server-to-client request must be bounded by a client-to-server request with a longer request lifetime to facilitate scalable deployment of SSE streams. Tasks explicitly decouple the _operation lifetime_ from the _request lifetime_, meaning that it is not possible for a server to poll a client under the updated specification language; _every_ polling request is unsolicited. + +As a direct consequence of that change, all server-to-client task requests are rendered invalid, hence the removal of client-hosted tasks altogether. We could have chosen to maintain that functionality for possible future use, but this would have created a maintenance burden with no corresponding benefits for the ecosystem at large. + +### Unsolicited Tasks vs. Immediate Results + +An [alternative proposal](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1905) 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](https://blog.modelcontextprotocol.io/posts/2025-12-19-mcp-transport-future/) 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](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322), but the two are not coupled with one another. + +### Waiting for Consistency + +In the updated "Task Support and Handling" section under "​Behavior Requirements", the following new requirement is introduced: + +> Receivers **MUST NOT** return a `CreateTaskResult` unless and until a `tasks/get` request would return that task; that is, in eventually-consistent systems, receivers **MUST** wait for consistency. + +This addition is intended 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. While this 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. + +## Backward Compatibility + +### `tasks/result` + +The removal of `tasks/result` is not backwards-compatible. At a protocol level, this is handled according to the protocol version. Under the `2025-11-25` protocol version, `tasks/result` **MUST** be supported if the `tasks` capability is advertised, but under subsequent protocol versions, requests **MUST** be rejected with a `-32601` (Method Not Found) error. + +### Unsolicited Tasks + +The following adjustments related to unsolicited tasks are breaking changes: + +1. We will allow `CreateTaskResult` to be returned in response to `CallToolRequest` when no `task` field is present in the client request. +1. We will allow `CallToolResult` to be returned in response to `CallToolRequest` even when the `task` field is present in the request. + +At a protocol level, this should be handled according to the protocol version. Under the `2025-11-25` protocol version, these cases **SHOULD** be handled as malformed responses, but under subsequent protocol versions, they **MUST** be treated as valid per the updated specification language. + +### On Polymorphism + +In [SEP-1686](./1686-tasks.md), we explicitly chose not to introduce support for unsolicitated task creation, as this would have required all implementations to break all method contracts by allowing `CreateTaskResult` to be returned in addition to the non-task result shape. This proposal explicitly rejects that argument, opting to consider `CreateTaskResult` as something akin to a JSON-RPC error, which already needed to be handled in the standard result path. Implementations already needed to branch response handling for error response shapes - `CreateTaskResult` is different in that rather than being a different JSON-RPC envelope shape, it is a different subset shape of a JSON-RPC result. + +Fortunately for the proposal author, `CreateTaskResult` also happens to be a unique result shape, as it is the only MCP result with a single `result.task` key. This enables implementations to predictably handle this difference internally at the deserialization layer without necessarily exposing it to SDK consumers. The following (non-binding) implementation approach is suggested to support this: + +1. All existing API surfaces should remain unchanged - that is, if a `client.callTool()` method is written to return `CallToolResult`, that method contract should not be altered to return a union of `CallToolResult` and `CreateTaskResult`. +1. Internally, if such a request returns `CreateTaskResult`, follow the standard task polling semantics of the current specification. +1. Gradually introduce new methods that surface the polling flow to SDK consumers as needed. + +## Security Implications + +This change does not introduce any new security implications. + +## Reference Implementation + +To be provided. diff --git a/docs/seps/2557-tasks-stabilization.mdx b/docs/seps/2557-tasks-stabilization.mdx index 29184cddb..faa60360c 100644 --- a/docs/seps/2557-tasks-stabilization.mdx +++ b/docs/seps/2557-tasks-stabilization.mdx @@ -1,7 +1,7 @@ --- -title: "SEP-2557: Stabilizing Tasks" -sidebarTitle: "SEP-2557: Stabilizing Tasks" -description: "Stabilizing Tasks" +title: "SEP-2557: Adapat Tasks for Stateless & Sessionless Protocol" +sidebarTitle: "SEP-2557: Adapat Tasks for Stateless & Sessionles…" +description: "Adapat Tasks for Stateless & Sessionless Protocol" ---
@@ -16,7 +16,7 @@ description: "Stabilizing Tasks" | Field | Value | | ------------- | ------------------------------------------------------------------------------------------------------------------------------ | | **SEP** | 2557 | -| **Title** | Stabilizing Tasks | +| **Title** | Adapat Tasks for Stateless & Sessionless Protocol | | **Status** | Draft | | **Type** | Standards Track | | **Created** | 2026-04-12 | @@ -28,17 +28,67 @@ description: "Stabilizing Tasks" ## Abstract -This proposal builds on [tasks](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) by introducing several simplifications to the original functionality to prepare the feature for stabilization, following implementation and usage feedback since the initial experimental release. In particular, this proposal allows tasks to be returned in response to non-task requests to remove unneeded stateful handshakes, and it collapses `tasks/result` into `tasks/get`, removing the error-prone interaction between the `input_required` task status and the `tasks/result` method. - -This SEP also incorporates changes into `Tasks` necessary following the acceptance of: +This SEP incorporates changes into `Tasks` necessary following the acceptance of: - [SEP-2260: Require Server requests to be associated with a Client request](./2260-Require-Server-requests-to-be-associated-with-Client-requests.md) - [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322) - [SEP-2243: Http Standardization](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243) +- [SEP-2567: Sessionless MCP via Explicit State Handles](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567). + +It also proposes a simplification of [tasks](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) based on feedback since the initial experimental release. + +This SEP DOES NOT move Tasks out of Experimentation status. ## Motivation -**Tasks** were introduced in an experimental state in the `2025-11-25` specification release, serving as an alternate execution mode for certain request types (tool calls, elicitation, and sampling) to enable polling for the result of a task-augmented operation. This is done according to the following process, using tool calls as an example: +A number of changes are necessary in the `Tasks` feature to support the upcoming changes to the spec listed in the SEPs above, and to address issues identified since the initial experimental release of `Tasks` in the `2025-11-25` specification release. + +### Changes to support upcoming SEPs + +A number of changes are necessay to support the upcoming changes to the spec listed in the SEPs above. + +[SEP-2260] disallows unsolicited server-to-client requests, which +invalidates the concept of client tasks for elicitation and sampling operations. +This proposal removes client-hosted tasks & their associated capabiliteis since +these scenarios are no longer supported. + +[SEP-2567] removes sessions from the protocol, which was the only defined +scope for `tasks/list`. This proposal removes `tasks/list` methods and +capabilities since there is no clear way to scope a list of tasks between a +server and a client. + +[SEP-2322] introduces a new flow for how the server requests more input +from the client, i.e. `elicitations`, `sampling`, and `roots`. SEP-2322 +leverages the existing `input_required` task status to signal when the server +needs more information. The client then makes a request to `tasks/result` to +retrieve the `IncompleteResult` which contains the server's request for more +information. The current specification requires the client to make this +`tasks/result` request in a blocking manner, which is unintuitive and has led to +implementation issues. We identified that we would need to make a breaking +change to `tasks/result` during SEP-2322 discussions, but decided redesigning +tasks would be a scope expansion that would derail MRTR discussion. This +proposal simplifies this flow by inlining the `IncompleteResult` into the +`tasks/get` response when the task is in an `input_required` state, eliminating +the need for a separate blocking request and improving the intuitiveness of the +flow. + +Given the difficulty of implementing the MRTR changes in Tasks as the spec currently stands, we believe that simplifying the Tasks flow by collapsing several methods into `tasks/get` is necessary and is also proposed here. + +[SEP-2243] introduces standard headers in the Streamable HTTP Transport +to facilitate more efficient routing. Routing on `TaskId` is also desirable +since there is often state associated with a specific Task that needs to be +consistently routed to the same server instance. This proposal requires that the +`Mcp-Name` header MUST be set to the value of `params.taskId` by the client when +making `tasks/get` and `tasks/cancel` requests over the Streamable HTTP +Transport. + +### Lessons Learned from Implementation & Usage Feedback + +`Tasks` were introduced in an experimental state in the `2025-11-25` specification release, serving as an alternate execution mode for certain request types (tool calls, elicitation, and sampling) to enable polling for the result of a task-augmented operation. + +#### Current Task Flow Overview + +This is done according to the following process, using tool calls as an example: 1. Check the receiver's task capabilities for the request type. If the capability is not present, the receiver does not support tasks. 1. Invoke `tools/list` to retrieve the tool list, then check `execution.taskSupport` on the appropriate tool to determine if the tool supports tasks. @@ -52,72 +102,258 @@ This SEP also incorporates changes into `Tasks` necessary following the acceptan 1. Once the task status is `completed`, the client issues a `tasks/result` call to retrieve the final result. 1. If the client still has an active `tasks/result` call from a prior `input_required` status, it will receive the result as the result to that open request. -The task-polling flow as currently-defined has several problems, most of which are related to the `input_required` status transition: +#### Current Task Flow Problems -1. Prematurely invoking `tasks/result` is unintuitive and it only done to accommodate the possibility of no other SSE streams being open in Streamable HTTP. -1. The fact that `tasks/result` blocks until completion is [even less intuitive](https://github.com/modelcontextprotocol/java-sdk/pull/755#issuecomment-3806079033). -1. Clients need to issue an additional request after encountering the `completed` status just to retrieve the final task result. -1. When a task reaches the `completed` status after this point, the server needs to identify all open `tasks/result` requests for that task to appropriately close them with the final task result, introducing unnecessary architectural complexity by mandating some sort of internal push-based messaging, which defies the intent of tasks' polling-based design. +The current flow has a number of problems: -Tasks also require their requestor to be cooperative, in the sense that the requestor of a task must explicitly opt into task-augmented execution on a request. While this contract ensures that both the requestor and receiver understand their peer's capabilities and safely agree on the request and response formats in advance, it also has a few conceptual flaws: +1. `tasks/result` is overloaded and calling it to retrieve server requests when in the `input_required` status is unintuitive. +2. `tasks/result` is expected to block until the task is completed if called prematurely. This has led to [implementation issues](https://github.com/modelcontextprotocol/java-sdk/pull/755#issuecomment-3806079033). Requires long lived persistent connections, which many clients & servers do not want to implement. This problem still exists even with MRTR. +3. The flow is inefficient. Clients must make multiple calls to `tasks/get` to check on the status and then to `tasks/result` to retrieve the final result or required input. + +Task Creation also has a number of issues since it is client-directed instead of being server-directed.Today the client must declare that it wants a task to be created by including the `task` field in the request. This creates several issues: 1. It requires requestors to explicitly check the capabilities of receivers. This introduces an unnecessary state contract that may be violated during mid-session deployments under the Streamable HTTP transport, and also raises concerns about the capability exchange growing in payload size indefinitely as more methods are supported. -1. It requires a tool-specific behavior carveout which gets pushed onto the client to navigate. Related to this, it forces clients to cache a `tools/list` call prior to making any task-augmented tool call. -1. It requires host application developers to explicitly choose to opt into task support from request to request, rather than relying on a single, consistent request construction path for all protocol operations. +2. It requires a tool-specific behavior carveout which gets pushed onto the client to navigate. Related to this, it forces clients to cache a `tools/list` call prior to making any task-augmented tool call. +3. It requires host application developers to explicitly choose to opt into task support from request to request, rather than relying on a single, consistent request construction path for all protocol operations. +4. It creates unnecessary burden on server developers to handle both the task and non-task flow for the same tool call. Most Tools will either return a `Task` or not, no use case has emerged where a server would want to return a `Task` for some calls and not others for the same tool, so this flexibility is unnecessary. -In practical terms, these flaws imply that an MCP server cannot make a clean break from non-task to task-augmented execution on its tools, even if clients have implemented support for tasks already; the server must wait for all host applications to additionally opt into tasks as well and sit in an awkward in-between state in the meantime, where it must choose to either break compatibility with host applications (even if those host applications have an updated client SDK) or accept the costs of task-optional execution and internally poll on tasks sometimes. The requirement that task support be declared ahead of time makes task execution predictable, but also prematurely removes the possibility of only dispatching a task when there is real work to be done, along the lines of the .NET [ValueTask](https://learn.microsoft.com/en-us/dotNet/api/system.threading.tasks.valuetask?view=net-10.0). Allowing the requestor to dictate whether or not a task will be created similarly eliminates the possibility of caching results or sending early return values, requiring the creation of a task on every request if tasks are supported by the requestor at all. +## Specification -Furthermore, in [SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322), we identified that we would need to make a breaking change to `tasks/result` for these changes anyways, but that redesigning the flow within SEP-2322 would be a scope expansion that would derail MRTR discussion. Regardless, MRTR relies heavily on tasks as a solution for "persistent" requests that require server-side state, so these two proposals are somewhat interdependent. +To support the new SEPs and solve the issues outlined above we propose the follwoing changes to the `Tasks` specification: -To both improve the adoption of tasks and to reduce their upfront messaging overhead, this proposal simplifies their execution model by allowing peers to raise unsolicited tasks to each other and consolidating the polling lifecycle entirely into the `tasks/get` method. +1. Removes Features made obsolete by upcoming SEPs. +2. Task Creation is determined by the Server. +3. Servers MUST support `task/cancel` operation. +4. Removal of Task Capabilities that are no longer necessary. +5. Simplified Task Polling Flow which consolidates all polling onto the `tasks/get` method. -## Specification +### Remove features made obsolete by upcoming SEPs + +1. We will remove the concept of client-hosted tasks (Sampling & Elicitation), as SEP-2260 disallows unsolicited server-to-client requests +2. We will remove the optional `tasks/list` operation, as SEP-2567 removes sessions which was the only defined scope for listing tasks between a server and a client. We may expand task support to additional client-to-server request types in the future, and implementors are still advised against implementing tasks as a tool-specific protocol operation. + +### Task Creation Changes + +Tasks will no longer be an optional capability, but will instead become a standard part of the protocol that servers MAY choose to implement. If a Tool returns a Task, Servers MUST declare `execution.tasksupport` on the tool definition. + +Clients MUST understand Tasks, however they are not required to implement the polling workflow and leverage tools with `execution.taskSupport`. Clients MAY choose to filter out tools with `execution.taskSupport` declared. + +1. We will remove the `tasks` capability declaration on both the client and the server. +1. Servers MAY return `CreateTaskResult` or a `CallToolResult`in response to `CallToolRequest`. +1. Servers SHOULD ignore the `task` field if it is present in a `CallToolRequest`. +1. If a server returns a `CreateTaskResult`, it MUST support the `tasks/get` and `tasks/cancel` methods to allow clients to poll for the result and cancel the task if desired. -The following changes will be made to the tasks specification: - -1. With respect to task creation - 1. We will remove the `tasks` capability declaration on both the client and the server. - 1. We will remove the `execution.taskSupport` field from the `Tool` shape. - 1. We will allow `CreateTaskResult` to be returned in response to `CallToolRequest` when no `task` field is present in the request. - 1. We will allow `CallToolResult` to be returned in response to `CallToolRequest` even when the `task` field is present in the request. -1. With respect to the task polling lifecycle: - 1. We will consolidate the entire polling lifecycle into the `tasks/get` method. This single method will handle retrieving task statuses and results simultaneously, and will additionally act as the carrier for receiver-to-requestor requests for the purposes of [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322). - 1. We will remove the requirement that requestors react to the `input_required` status by prematurely invoking `tasks/result` to side-channel requests on an SSE stream in the Streamable HTTP transport. - 1. We will inline the final task result or error into the `Task` shape, bringing that into `tasks/get` and all notifications by extension. - 1. We will inline outstanding server-to-client requests into a new `inputRequests` field on `GetTaskResult`, akin to the field of the same name used in [SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322). - 1. We will inline mid-task client-to-server results into a new `inputResponses` field on `GetTaskRequest`, akin to the field of the same name used in [SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322). - 1. We will remove the `tasks/result` method. -1. With respect to tasks in general: - 1. We will remove the concept of client-hosted tasks, as [SEP-2260](./2260-Require-Server-requests-to-be-associated-with-Client-requests.md) renders them conceptually invalid. - 1. We will remove the `tasks/list` operation. - 1. We may expand task support to additional client-to-server request types in the future, and implementors are still advised against implementing tasks as a tool-specific protocol operation. - 1. We will require `tasks/cancel` to be supported even if a server is incapabable or unwillling of offering actual task cancellation, similar to `notifications/cancelled` (it should instead return an error). - -### Task Capabilities Changes Summary +### Task Cancel Changes + +We propose aligning Task Cancellation with the Cooperative Cancellation model. In this model the requestor The requestor signals intent, but the worker decides when or if to honor it. This alligns `tasks/cancel with the cancellation pattern used in `notifications/cancelled`, where clients can always send a cancellation request, but servers can choose how to handle it. This also aligns Tasks with how Cancellation Tokens are handled in most async/concurrent programming languages including Go, Python, C#, TypeScript, etc... + +In the specification this means: + +Servers MUST support the `task/cancel` operation. A server MAY choose to ignore cancellation requests if its incapable or unwilling to offer cancellation of that Task. + +### Task Capabilities Changes + +This SEP Removes all capabilities related to Tasks. + +- SEP-2567 makes `tasks/list` impossible to support. +- SEP-2260 disallows unsolicited server-to-client requests, which invalidates the concept of client tasks for elicitation and sampling operations. +- This SEP makes Tasks a standard part of the protocol, and not a negotiated capability. +- This SEP Makes `tasks/cancel` required to be supported, even if a server does not wish to support tasks. The below table summarizes the changes to the task-related capabilities: -| Role | Capability | Status | Description | -| ------ | --------------------------------------- | ------- | ---------------------------- | -| Server | `tasks.requests.tools.call` | removed | | -| Server | `tasks.cancel` | removed | `tasks/cancel` is required | -| Server | `tasks.list` | removed | `tasks/list` is removed | -| Client | `tasks.requests.sampling.createMessage` | removed | no longer supported SEP-2260 | -| Client | `tasks.requests.elicitation.create` | removed | no longer supported SEP-2260 | -| Client | `tasks.cancel` | removed | no longer needed | -| Client | `tasks.list` | removed | no longer needed | +| Role | Capability | Status | Description | +| ------ | --------------------------------------- | ------- | ------------------------------------------------ | +| Server | `tasks.requests.tools.call` | removed | part of core protocol not an optional capability | +| Server | `tasks.cancel` | removed | `tasks/cancel` is required | +| Server | `tasks.list` | removed | no longer supported SEP-2567 | +| Client | `tasks.requests.sampling.createMessage` | removed | no longer supported SEP-2260 | +| Client | `tasks.requests.elicitation.create` | removed | no longer supported SEP-2260 | +| Client | `tasks.cancel` | removed | no longer needed, no client tasks | +| Client | `tasks.list` | removed | no longer supported SEP-2567 | + +### Task Flow Change + +We propose simplifying the Task Flow into two methods: `tasks/get` and `tasks/cancel`. + +- The `tasks/get` methods will handle retrieving task statuses and results simultaneously, and will additionally act as the carrier for receiver-to-requestor requests for the purposes of SEP-2322: Multi Round-Trip Requests. This simplifies the flow and allows for polling or streaming updates on a single endpoint. Moreover this more closely matches Long Running Operation APIs where there is a single endpoint that is polled for the status of an operation. +- The `tasks/cancel` method will be required and a separate endpoint. + +Below is an example of the new Task Flow. + +```mermaid +sequenceDiagram + participant U as User + participant C as Client + participant S as Server + C->>S: tools/call (id: 1) + S-->>C: CreateTaskResult (id: 1, taskId: 123, status: working) + C->>S: tasks/get (taskId: 123) + S-->>C: GetTaskResult (taskId: 123, status: working) + C->>S: tasks/get (taskId: 123) + S-->>C: GetTaskResult (taskId: 123, status: input_required, inputRequests: {...}) + C->>U: Prompt User for Input + U-->>C: Provided Input + C->>S: tasks/get (taskId: 123, inputResponses: {...}) + S-->>C: GetTaskResult (taskId: 123, status: working) + C-->>S: tasks/get (taskId: 123) + S-->>C: GetTaskResult (taskId: 123, status: completed, result: {...}) +``` + +We will make the following changes to the specification to support this. + +1. We will remove the requirement that requestors react to the input_required status by prematurely invoking tasks/result to side-channel requests on an SSE stream in the Streamable HTTP transport. +2. We will inline the final task result or error into the Task shape, bringing that into tasks/get and all notifications by extension. +3. We will inline outstanding server-to-client requests into a new inputRequests field on GetTaskResult, akin to the field of the same name used in SEP-2322. +4. We will inline mid-task client-to-server results into a new inputResponses field on GetTaskRequest, akin to the field of the same name used in SEP-2322. +5. We will remove the tasks/result method. +6. We will update error handling to remove the notion that a task can be failed due to non-JSON-RPC errors such as a tool result with isError: true to maintain a strong separation between protocol-level faults and application-level faults. + +```diff +-For failures, the `error` field **MUST** contain the JSON-RPC error that occurred during execution, or the task **MAY** use `status: "failed"` with a `statusMessage` for tool results with `isError: true`. ++For failures, the `error` field **MUST** contain the JSON-RPC error that occurred during execution. The `failed` status **MUST NOT** be used to represent non-JSON-RPC errors, such as a tool result that completed with with `isError: true`. +``` + +#### task/get Specification + +We will add the following language to the specification to define the new flow and the expected behavior of `tasks/get`: + +Upon receiving a `tasks/get` request with `inputResponses`, the server MUST process the provided responses and update the task state accordingly. The server MAY choose to transition the task back to `working` status if it determines that the provided input is sufficient to continue processing. -### Task Methods Changes Summary +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 [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322). 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. + +The below section contains example responses for each of the above cases. + +Status: Working + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "working", + "statusMessage": "The operation is in progress.", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 60000, + "pollInterval": 5000 + } +} +``` -The below table summarizes the changes to the task-related methods: +Status: Completed -| Method | Status | Description | -| --------------------------------- | --------------- | --------------------------------------------------------------------- | -| `tasks/get` | still supported | Consolidates the entire polling lifecycle into a single method. | -| `tasks/result` | removed | No longer needed; results are inlined into `tasks/get`. | -| `tasks/input_response` (SEP-2322) | removed | No longer needed; results are inlined into `tasks/get`. | -| `tasks/cancel` | still supported | Required to be supported even if actual cancellation is not possible. | -| `tasks/list` | removed | Use cases can be satisfied with client handling. | +```json +{ + "jsonrpc": "2.0", + "id": 1, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "completed", + "statusMessage": "The operation has completed successfully.", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 60000, + "pollInterval": 5000, + "result": { + "content": [ + { + "type": "text", + "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy" + } + ], + "isError": false + } + } +} +``` + +Status: Failed + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "failed", + "statusMessage": "Tool execution failed: API rate limit exceeded", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 30000, + "error": { + "code": -32603, + "message": "API rate limit exceeded" + } + } +} +``` + +Status: Cancelled + +```json +{ + "jsonrpc": "2.0", + "id": 6, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "cancelled", + "statusMessage": "The task was cancelled by request.", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 30000, + "pollInterval": 5000 + } +} +``` + + Status: input_required + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "input_required", + "statusMessage": "The operation requires additional input.", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 60000, + "pollInterval": 5000, + "inputRequests": { + "github_login": { + "method": "elicitation/create", + "params": { + "mode": "form", + "message": "Please provide your GitHub username", + "requestedSchema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"] + } + } + } + } + } +} +``` ### Task Schema Changes @@ -162,7 +398,7 @@ interface FailedTask extends Task { type DetailedTask = Task | InputRequiredTask | CompletedTask | FailedTask; ``` -### Client Requests for `task/get` +#### Client Requests for `task/get` ```typescript interface GetTaskRequest extends JSONRPCRequest { @@ -185,7 +421,7 @@ interface GetTaskRequest extends JSONRPCRequest { } ``` -### Server Response for `task/get` +#### Server Response for `task/get` ```typescript type GetTaskResult = Result & DetailedTask; @@ -193,7 +429,7 @@ type GetTaskResult = Result & DetailedTask; type TaskStatusNotificationParams = NotificationParams & DetailedTask; ``` -### `ResultType` +#### `ResultType` The ResultType field was introduced in [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322) to handle polymorphic results. `Tasks` has the same issue where a server may return a `CallToolResult` or a `CreateTaskResult`. To address this, we propose the addition of the `task` ResultType to indicate that a Response contains a `Task` object. @@ -203,28 +439,9 @@ type ResultType = "complete" | "incomplete" | "task"; For backwards compatibility ResultType is inferred by default to be `complete`. Therefore all calls which return a `Task` (i.e.`task/get`, `task/cancel`) calls must set `task` as the ResultType moving forward. -### Example Task Flow - -```mermaid -sequenceDiagram - participant U as User - participant C as Client - participant S as Server - C->>S: tools/call (id: 1) - S-->>C: CreateTaskResult (id: 1, taskId: 123, status: working) - C->>S: tasks/get (taskId: 123) - S-->>C: GetTaskResult (taskId: 123, status: input_required, inputRequests: {...}) - C->>U: Prompt User for Input - U-->>C: Provided Input - C->>S: tasks/get (taskId: 123, inputResponses: {...}) - S-->>C: GetTaskResult (taskId: 123, status: working) - C-->>S: tasks/get (taskId: 123) - S-->>C: GetTaskResult (taskId: 123, status: completed, result: {...}) -``` - Below (collapsed) is the full JSON example of a tool call with an unsolicited task-augmentation that matches the diagram above: -
+### Example Task Flow Consider a simple tool call, `hello_world`, requiring an elicitation for the user to provide their name. The tool itself takes no arguments. @@ -463,182 +680,6 @@ Eventually, the server completes the request, so it stores the final `CallToolRe } ``` -
- -### `tasks/get` Behavior by Task State - -A `Task` can be in one of the following states: `working`, `completed`, `failed`, `cancelled`, or `input_required`. This section defines the expected behavior of a call to `tasks/get` when in each state. - -#### Working - -The response MUST include the `working` status. - -
- -```json -{ - "jsonrpc": "2.0", - "id": 1, - "resultType": "task", - "result": { - "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", - "status": "working", - "statusMessage": "The operation is in progress.", - "createdAt": "2025-11-25T10:30:00Z", - "lastUpdatedAt": "2025-11-25T10:40:00Z", - "ttl": 60000, - "pollInterval": 5000 - } -} -``` - -
- -#### Completed - -When a task is in the `completed` state, a call to `tasks/get` MUST return the `Task` with status `completed` and include the final result of the task. - -
- -```json -{ - "jsonrpc": "2.0", - "id": 1, - "resultType": "task", - "result": { - "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", - "status": "completed", - "statusMessage": "The operation has completed successfully.", - "createdAt": "2025-11-25T10:30:00Z", - "lastUpdatedAt": "2025-11-25T10:40:00Z", - "ttl": 60000, - "pollInterval": 5000, - "result": { - "content": [ - { - "type": "text", - "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy" - } - ], - "isError": false - } - } -} -``` - -
- -#### Failed - -When a task is in the `failed` state, a call to `tasks/get` should return an error in the `result` field indicating the reason for the failure. - -To maintain a strong separation between the handling of protocol faults and application-level faults, we will revise prior language suggesting that the `failed` state may be used for tool call errors. Specifically, in error-handling paths for tasks, we will make the following change to the "Result Retrieval" section of the existing specification: - -```diff -### Result Retrieval - -When a task reaches a terminal status (`completed`, `failed`, or `cancelled`), servers **MUST** inline the final result or error into the `Task` object returned by `tasks/get`. - -For successful completion, the `result` field **MUST** contain what the underlying request would have returned (e.g., `CallToolResult` for `tools/call`). - --For failures, the `error` field **MUST** contain the JSON-RPC error that occurred during execution, or the task **MAY** use `status: "failed"` with a `statusMessage` for tool results with `isError: true`. -+For failures, the `error` field **MUST** contain the JSON-RPC error that occurred during execution. The `failed` status **MUST NOT** be used to represent non-JSON-RPC errors, such as a tool result that completed with with `isError: true`. - -Servers **MUST** include the `result` or `error` field in `notifications/tasks/status` notifications when notifying about terminal status transitions. -``` - -
- -```json -{ - "jsonrpc": "2.0", - "id": 1, - "resultType": "task", - "result": { - "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", - "status": "failed", - "statusMessage": "Tool execution failed: API rate limit exceeded", - "createdAt": "2025-11-25T10:30:00Z", - "lastUpdatedAt": "2025-11-25T10:40:00Z", - "ttl": 30000, - "error": { - "code": -32603, - "message": "API rate limit exceeded" - } - } -} -``` - -
- -#### Cancelled - -When a task is in the `cancelled` state, a call to `tasks/get` MUST return the `Task` with status `cancelled`. - -
- -```json -{ - "jsonrpc": "2.0", - "id": 6, - "resultType": "task", - "result": { - "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", - "status": "cancelled", - "statusMessage": "The task was cancelled by request.", - "createdAt": "2025-11-25T10:30:00Z", - "lastUpdatedAt": "2025-11-25T10:40:00Z", - "ttl": 30000, - "pollInterval": 5000 - } -} -``` - -
- -#### `input_required` - -If the task status is `input_required`, this indicates that the `Task` requires additional input from the client before it can proceed. The server MUST return the `Task` with status set to `input_required` and an `IncompleteResult` from [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322). - -
- -```json -{ - "jsonrpc": "2.0", - "id": 1, - "resultType": "task", - "result": { - "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", - "status": "input_required", - "statusMessage": "The operation requires additional input.", - "createdAt": "2025-11-25T10:30:00Z", - "lastUpdatedAt": "2025-11-25T10:40:00Z", - "ttl": 60000, - "pollInterval": 5000, - "inputRequests": { - "github_login": { - "method": "elicitation/create", - "params": { - "mode": "form", - "message": "Please provide your GitHub username", - "requestedSchema": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": ["name"] - } - } - } - } - } -} -``` - -
- ### HTTP Streamable Transport Headers [SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243) introduces standard headers in the Streamable HTTP Transport to facilitate more efficient routing. Routing on `TaskId` is also desirable since there is often state associated with a specific Task that needs to be consistently routed to the same server instance. SEP-2243 requires that all requests and notifications declare an `Mcp-Method` header. @@ -681,6 +722,8 @@ In the updated "Task Support and Handling" section under "​Behavior Requiremen This addition is intended 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. While this 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. + ## Backward Compatibility ### `tasks/result` diff --git a/docs/seps/index.mdx b/docs/seps/index.mdx index 362af9272..6cfa630b3 100644 --- a/docs/seps/index.mdx +++ b/docs/seps/index.mdx @@ -12,7 +12,7 @@ Specification Enhancement Proposals (SEPs) are the primary mechanism for proposi ## Summary -- **Draft**: 1 +- **Draft**: 2 - **Accepted**: 2 - **Final**: 27 @@ -20,8 +20,9 @@ Specification Enhancement Proposals (SEPs) are the primary mechanism for proposi | SEP | Title | Status | Type | Created | | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ------------------------------------------------- | ---------------- | ---------- | -| [SEP-2557](/seps/2557-tasks-stabilization) | Stabilizing Tasks | Draft | Standards Track | 2026-04-12 | +| [SEP-2557](/seps/2557-adapt-tasks-for-stateless-and-sessionless-protocol) | Adapat Tasks for Stateless & Sessionless Protocol | Draft | Standards Track | 2026-04-12 | | [SEP-2260](/seps/2260-Require-Server-requests-to-be-associated-with-Client-requests) | Require Server requests to be associated with a Client request. | Accepted | Standards Track | 2026-02-16 | +| [SEP-2243](/seps/2243-http-standardization) | HTTP Header Standardization for Streamable HTTP Transport | Draft | Standards Track | 2026-02-04 | | [SEP-2207](/seps/2207-oidc-refresh-token-guidance) | OIDC-Flavored Refresh Token Guidance | Accepted | Standards Track | 2026-02-04 | | [SEP-2149](/seps/2149-working-group-charter-template) | MCP Group Governance and Charter Template | Final | Process | 2025-01-15 | | [SEP-2148](/seps/2148-contributor-ladder) | MCP Contributor Ladder | Final | Process | 2026-01-15 | diff --git a/docs/specification/draft/basic/transports.mdx b/docs/specification/draft/basic/transports.mdx index f2f32c84a..3f73b8663 100644 --- a/docs/specification/draft/basic/transports.mdx +++ b/docs/specification/draft/basic/transports.mdx @@ -91,18 +91,20 @@ MCP endpoint. 1. The client **MUST** use HTTP POST to send JSON-RPC messages to the MCP endpoint. 2. The client **MUST** include an `Accept` header, listing both `application/json` and `text/event-stream` as supported content types. -3. The body of the HTTP POST request **MUST** be a single JSON-RPC _request_, _notification_, or _response_ to a server-sent request. -4. If the body is a JSON-RPC _notification_ or _response_ to a server-sent request: +3. The client **MUST** include the [standard MCP request headers](#standard-mcp-request-headers) + on each POST request. +4. The body of the HTTP POST request **MUST** be a single JSON-RPC _request_, _notification_, or _response_ to a server-sent request. +5. If the body is a JSON-RPC _notification_ or _response_ to a server-sent request: - If the server accepts the input, the server **MUST** return HTTP status code 202 Accepted with no body. - If the server cannot accept the input, it **MUST** return an HTTP error status code (e.g., 400 Bad Request). The HTTP response body **MAY** comprise a JSON-RPC _error response_ that has no `id`. -5. If the body is a JSON-RPC _request_, the server **MUST** either +6. 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. The client **MUST** support both these cases. -6. If the server initiates an SSE stream: +7. If the server initiates an SSE stream: - The server **SHOULD** immediately send an SSE event consisting of an event ID and an empty `data` field in order to prime the client to reconnect (using that event ID as `Last-Event-ID`). @@ -285,6 +287,290 @@ version `2025-03-26`. If the server receives a request with an invalid or unsupported `MCP-Protocol-Version`, it **MUST** respond with `400 Bad Request`. +### Standard MCP Request Headers + +The Streamable HTTP transport requires clients to include the following headers on POST +requests, mirrored from the JSON-RPC 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. + +#### Examples + +**`tools/call` request:** + +```http +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" + } + } +} +``` + +**`resources/read` request:** + +```http +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" + } +} +``` + +**`initialize` request (no `Mcp-Name` needed):** + +```http +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" + } + } +} +``` + +**Notification:** + +```http +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" +} +``` + +#### Case Sensitivity + +Header names (called "field names" in [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110#name-field-names)) +are case-insensitive. Clients and servers **MUST** use case-insensitive comparisons for +header names. Header _values_ (such as method names) are case-sensitive. + +#### Server Validation + +Servers that process the request body **MUST** reject requests where the values specified +in the headers do not match the corresponding values in the request body. This prevents +potential security vulnerabilities when different components in the network rely on +different sources of truth (e.g., a load balancer routing on the header value while the +MCP server executes based on the body value). + +When rejecting a request due to header validation failure, servers **MUST** return HTTP +status `400 Bad Request` and **SHOULD** include a JSON-RPC error response using 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`). + +**Example error response:** + +```json +{ + "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 include: + +- A required standard header (`Mcp-Method`, `Mcp-Name`) is missing +- A header value does not match the corresponding request body value +- A header value contains invalid characters + + + +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. + + + +### 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`. See [Tool Definitions](/specification/draft/server/tools#x-mcp-header) for +details on how to annotate tool parameters. + +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. + +#### 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** contain only ASCII characters (excluding space and `:`) +- **MUST** be case-insensitively unique among all `x-mcp-header` values in the + `inputSchema` +- **MUST** only be applied to parameters with primitive types (number, string, boolean) + +Clients **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. + +**Example tool definition:** + +```json +{ + "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"] + } +} +``` + +**Resulting HTTP request:** + +```http +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" + } + } +} +``` + +#### Value Encoding + +Clients **MUST** encode parameter values before including them in HTTP headers to ensure +safe transmission and prevent injection attacks. + +**Type conversion**: Convert the parameter value to its string representation: + +- `string`: Use the value as-is +- `number`: Convert to decimal string representation (e.g., `42`, `3.14`) +- `boolean`: Convert to lowercase `"true"` or `"false"` + +Per [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110#name-field-values), HTTP +header field values must consist of visible ASCII characters (0x21-0x7E), space (0x20), +and horizontal tab (0x09). When a value cannot be safely represented as a plain ASCII +header value (e.g., it contains non-ASCII characters, control characters, or has +leading/trailing whitespace), clients **MUST** use Base64 encoding of the UTF-8 +representation with the following format: + +```text +Mcp-Param-{Name}: =?base64?{Base64EncodedValue}?= +``` + +The prefix `=?base64?` and suffix `?=` indicate that the value is Base64-encoded. +Servers and intermediaries that need to inspect these values **MUST** decode them +accordingly. + +**Encoding 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=?=` | + +#### 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 [Value Encoding](#value-encoding) rules +5. Append a `Mcp-Param-{Name}: {Value}` header to the request + +#### Server Behavior for Custom Headers + +Intermediate servers that do not recognize an `Mcp-Param-{Name}` header **MUST** forward it and otherwise ignore it, as required by the [HTTP Semantics RFC](https://www.rfc-editor.org/rfc/rfc9110.html#name-field-names). + +Servers **MUST** reject requests with a recognized `Mcp-Param-{Name}` headers that contain invalid +characters (see [Value Encoding](#value-encoding)). + +Any server that processes the message body **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 and JSON-RPC error +code `-32001` (`HeaderMismatch`) if any validation fails. + +| 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 | + ### SSE Stream Configuration When initiating SSE streams, servers **SHOULD** include the `X-Accel-Buffering: no` diff --git a/docs/specification/draft/changelog.mdx b/docs/specification/draft/changelog.mdx index 36885d258..f300aa12a 100644 --- a/docs/specification/draft/changelog.mdx +++ b/docs/specification/draft/changelog.mdx @@ -26,6 +26,7 @@ the previous revision, [2025-11-25](/specification/2025-11-25). 1. Add `extensions` field to `ClientCapabilities` and `ServerCapabilities` to support optional [extensions](/docs/extensions/overview) beyond the core protocol. 2. Document OpenTelemetry trace context propagation conventions for `_meta` keys (`traceparent`, `tracestate`, `baggage`) ([SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414)). 3. Servers **SHOULD** return tools from `tools/list` in a deterministic order to enable client-side caching and improve LLM prompt cache hit rates. +4. Require standard MCP request headers (`Mcp-Method`, `Mcp-Name`) on Streamable HTTP POST requests, and add support for custom headers from tool parameters via `x-mcp-header` ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). ## Other schema changes diff --git a/docs/specification/draft/server/tools.mdx b/docs/specification/draft/server/tools.mdx index 2b1cae4a1..8fe682c56 100644 --- a/docs/specification/draft/server/tools.mdx +++ b/docs/specification/draft/server/tools.mdx @@ -209,6 +209,8 @@ A tool definition includes: - For tools with no parameters, use one of these valid approaches: - `{ "type": "object", "additionalProperties": false }` - **Recommended**: explicitly accepts only empty objects - `{ "type": "object" }` - accepts any object (including with properties) + - Properties **MAY** include an [`x-mcp-header`](#x-mcp-header) annotation to expose + parameter values as HTTP headers - `outputSchema`: Optional JSON Schema defining expected output structure - Follows the [JSON Schema usage guidelines](/specification/draft/basic#json-schema-usage) - Defaults to 2020-12 if no `$schema` field is present @@ -246,6 +248,66 @@ disambiguation. +#### x-mcp-header + +The `x-mcp-header` extension property allows servers to designate specific tool +parameters to be mirrored into HTTP headers when using the +[Streamable HTTP transport](/specification/draft/basic/transports#custom-headers-from-tool-parameters). +This enables network intermediaries (load balancers, proxies, WAFs) to route and process +requests based on parameter values without parsing the request body. + +The `x-mcp-header` property is placed directly within the JSON Schema of the property to +be mirrored. Its value specifies the name portion of the resulting `Mcp-Param-{name}` +HTTP header. + +**Constraints on `x-mcp-header` values:** + +- **MUST NOT** be empty +- **MUST** contain only ASCII characters, excluding space and `:` +- **MUST** be case-insensitively unique among all `x-mcp-header` values in the + `inputSchema` +- **MUST** only be applied to parameters with primitive types (number, string, boolean) + +Clients **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 ensures that a single +malformed tool definition does not prevent other valid tools from being used. + +**Example tool definition with `x-mcp-header`:** + +```json +{ + "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"] + } +} +``` + +In this example, when the tool is called with `"region": "us-west1"`, the client adds +the header `Mcp-Param-Region: us-west1` to the HTTP request. + + + +Server developers **SHOULD NOT** mark sensitive parameters (passwords, API keys, tokens, +PII) with `x-mcp-header`, as header values are visible to network intermediaries. + + + ### Tool Result Tool results may contain [**structured**](#structured-content) or **unstructured** content. diff --git a/plugins/mcp-spec/README.md b/plugins/mcp-spec/README.md index fa146629e..fc36426f3 100644 --- a/plugins/mcp-spec/README.md +++ b/plugins/mcp-spec/README.md @@ -34,3 +34,17 @@ Search across MCP GitHub discussions, issues, and pull requests to find relevant ``` **Note:** The skill searches both open AND closed issues/PRs, which is important for understanding past decisions and historical context. + +### `/draft-sep ` + +Research and draft a Specification Enhancement Proposal that conforms to the [SEP governance process](https://modelcontextprotocol.io/community/sep-guidelines). Gates on whether the idea is SEP-worthy, interviews the author, checks existing spec coverage and prior art, then fills the template's required and optional sections and writes `seps/0000-{slug}.md`. Optionally opens a draft PR, backfills the SEP number, and runs `npm run generate:seps` and `npm run format:docs` so CI stays green. + +**Prerequisite:** Run from a local clone of this repository or your fork of it (the skill reads `seps/TEMPLATE.md` and writes into `seps/`). + +**Example:** + +``` +/draft-sep add websocket transport +``` + +**Note:** The skill will ask clarifying questions (SEP type, breaking-change status, prototype, prior discussion, sponsor, security) before writing anything. The SEP guidelines advise discussing an idea in Discord or a Working Group before drafting — the skill will flag if that hasn't happened. diff --git a/plugins/mcp-spec/skills/draft-sep/SKILL.md b/plugins/mcp-spec/skills/draft-sep/SKILL.md new file mode 100644 index 000000000..bde412799 --- /dev/null +++ b/plugins/mcp-spec/skills/draft-sep/SKILL.md @@ -0,0 +1,164 @@ +--- +name: draft-sep +description: Research and draft a Specification Enhancement Proposal following the MCP SEP governance process +user_invocable: true +arguments: + - name: idea + description: One-line summary of the proposed change + required: true +--- + +# Drafting a Specification Enhancement Proposal + +This skill guides an author through producing a SEP that conforms to `docs/community/sep-guidelines.mdx` and `seps/TEMPLATE.md`. Work through the phases **in order** — do not start writing the draft until the gate, interview, and research are complete. + +**Prerequisite:** This skill must be run from a local clone of `modelcontextprotocol/modelcontextprotocol` or a fork of it. Before doing anything else: + +1. Verify `seps/TEMPLATE.md` exists in the working directory. If it does not, stop and tell the user to run `gh repo fork modelcontextprotocol/modelcontextprotocol --clone` (or, if they are a maintainer with push access, clone the upstream repo directly) and re-run from its root. +2. Determine the **canonical remote** — the remote that points at `modelcontextprotocol/modelcontextprotocol` itself, not a fork. Inspect `git remote -v`: if `origin` points at the canonical repo, the canonical remote is `origin`. If `origin` points at a fork, look for an `upstream` remote; if none exists, add it with `git remote add upstream https://github.com/modelcontextprotocol/modelcontextprotocol.git`. The canonical remote is then `upstream`. +3. Run `git fetch {canonical}` and ensure local `main` is current with `{canonical}/main` so the research phases see up-to-date SEPs, schema, and `MAINTAINERS.md`. + +Phase 6 references `{canonical}` for the branch start-point and `origin` for the push target; these are the same remote for maintainers and different remotes for fork-based contributors. + +**Discuss before drafting.** The SEP guidelines advise raising an idea in Discord or a Working Group or Interest Group meeting before opening a SEP. If the user has not discussed this idea anywhere yet, say so explicitly and ask whether they want to proceed anyway. A cold SEP is valid but more likely to stall — and if no sponsor is found within 6 months, Core Maintainers may close the PR and mark the SEP `dormant`. + +The SEP author is responsible for building consensus within the community and documenting dissenting opinions — capture both as you go. + +## Phase 1 — Gate + +Before any interview or research, decide from the one-line `{idea}` whether this is SEP-worthy. + +**Redirect** (do not proceed) if the idea is: + +- A bug fix or typo correction +- A documentation clarification +- Adding examples to an existing feature +- A minor schema fix that does not change behavior + +For these, point the user at a regular pull request or the bug-report issue form instead and stop. + +**Proceed** if the idea is: + +- A new protocol feature or a change to an existing one +- A breaking change +- A governance or process change +- Anything controversial enough to need a design document and historical record + +When unsure, the guidelines say to ask in Discord before starting significant work — point the user there rather than burning time on a draft that may not be SEP-worthy. + +## Phase 2 — Interview + +Ask the user these six questions before touching any files. The answers feed directly into the draft. + +1. **SEP type?** Standards Track (core protocol feature), Extensions Track (extension rather than core — see SEP-2133), Informational (guidelines/design notes), or Process (governance/workflow change). Most SEPs are Standards Track. Note: `seps/TEMPLATE.md` and the SEP guidelines list only three types; Extensions Track was added by SEP-2133 and has not yet been backfilled into those docs. + - **If Extensions Track:** also ask which Working Group and Extension Maintainers will be responsible for the extension — SEP-2133 makes this a hard requirement, and an Extensions Track SEP MUST have at least one reference implementation in an official SDK prior to review. +2. **Is this a breaking change?** Determines how much weight the Backward Compatibility section carries. +3. **Prototype status?** There are two distinct gates: a working prototype is required before a SEP can be **accepted**, and a complete reference implementation is required before it can reach **Final**. The prototype proves feasibility — it doesn't need to be production-ready, but it must be runnable, not pseudocode. Does one exist, is one in progress, or is it still TBD? +4. **Where was this discussed?** Discord thread, Working Group or Interest Group meeting, GitHub Discussion — the link becomes the consensus evidence in the Rationale section. If the answer is "nowhere," flag it (see above). +5. **Author and sponsor?** Capture the author's name, email, and GitHub username for the `Author(s)` preamble field. Then ask about a sponsor: a SEP needs a Core Maintainer or Maintainer sponsor to **enter** `draft` status — the sponsor is what grants it. Until a sponsor signs on, the SEP sits in an "awaiting sponsor" state (Core Maintainers may close it as `dormant` after 6 months). If the user has a sponsor lined up, capture their GitHub username (without the `@` — `gh pr create --reviewer` expects a bare handle). If not, the preamble should read `Sponsor: None` and the finding-a-sponsor guidance from `docs/community/sep-guidelines.mdx` applies: tag 1-2 relevant maintainers from `MAINTAINERS.md` on the PR, share in the relevant Discord channel, and if there's no response in two weeks ask in `#general`. +6. **Security implications?** Does this proposal touch the attack surface — new transports, auth flows, data exposure, trust boundaries? The Security Implications section is required in `seps/TEMPLATE.md`; even "none identified" needs to be stated explicitly with reasoning. + +## Phase 3 — Research + +Run each step and **capture the findings** — they feed directly into the draft sections. + +### 1. Current spec coverage + +Use the `SearchModelContextProtocol` tool on the `mcp-docs` MCP server (if available) to find what the spec already says about this area. If that server is not configured, fall back to `grep -rn "{keyword}" docs/specification/draft/`. This becomes the "why is the current spec inadequate" half of the Motivation section. + +### 2. Prior art on GitHub + +Invoke `/search-mcp-github {idea}`. Look for: + +- Merged PRs that touched the same surface +- Closed issues that asked for this (or something close) +- Prior discussions where maintainers set direction or rejected a similar approach + +If a similar proposal was already rejected, that context is load-bearing — the new SEP needs to explain what changed. + +### 3. Overlapping SEPs + +```bash +grep -l -i "{keyword}" seps/*.md +``` + +Pick one or two keywords from the idea. If an existing SEP covers this area, the right move is usually to extend or supersede it rather than file a parallel proposal. Read any matches before continuing. + +### 4. Design-principle and roadmap fit + +Read `docs/community/design-principles.mdx` and `docs/development/roadmap.mdx`. Identify which principles the proposal serves and which it is in tension with. Check whether the proposal aligns with current Core Maintainer priorities reflected in the roadmap — proposals outside current priorities are more likely to face delays in review. Both findings go in the Rationale section. + +### 5. Schema touch-points + +```bash +grep -n "{affected-type}" schema/draft/schema.ts +``` + +For Standards Track and Extensions Track SEPs, find the concrete types the spec change would add or modify. Reference these by name in the Specification section. + +### 6. Exemplar SEPs + +```bash +grep -l "Status.*Final" seps/*.md | head -3 +``` + +Read two or three Final-status SEPs to see what a well-filled section looks like in practice. Match their level of detail. + +## Phase 4 — Draft + +Read `seps/TEMPLATE.md` and fill each section in order. Write to `seps/0000-{slug}.md` where `{slug}` is a lowercase, hyphenated version of the idea trimmed to ~50 characters (match the pattern of existing `seps/*.md` filenames). The `0000` placeholder is the documented convention from the SEP guidelines. + +Everything above the `---` rule in the template is required — write "none identified" with reasoning rather than omitting a section. Headings under "Additional Optional Sections" are optional. + +**Preamble notes:** + +- `Status:` — leave blank or omit. Authors should request status changes through their sponsor rather than setting the field themselves. +- `Type:` — from Q1. `seps/TEMPLATE.md` lists only `Standards Track | Informational | Process`, but `Extensions Track` is valid here per SEP-2133. +- `Created:` — today's date in `YYYY-MM-DD` format. +- `Author(s):` — `Name (@github-username)` from Q5. +- `Sponsor:` — `@github-username` from Q5, or the literal `None`. +- `PR:` — set to `https://github.com/modelcontextprotocol/modelcontextprotocol/pull/{NUMBER}` (the `{NUMBER}` placeholder gets filled in Phase 6). + +## Phase 5 — Checkpoint + +Tell the user: + +- The path to the draft file +- A one-line summary of what went into each section + +Then **ask**: open a draft PR now, or stop here so they can edit the file first? + +**Do not proceed to Phase 6 without a yes.** + +## Phase 6 — Open PR (only if the user says yes) + +SEP-1850 documents an amend-based flow: open the PR with the `0000-` placeholder, then immediately rename and amend so the final history is a single commit with the real number. + +```bash +git fetch {canonical} +git checkout -b sep/{slug} {canonical}/main +git add seps/0000-{slug}.md +git commit -m "SEP: {title}" +git push -u origin sep/{slug} +gh pr create --repo modelcontextprotocol/modelcontextprotocol --base main \ + --title "SEP: {title}" --body "{one-paragraph summary}" --draft --reviewer {sponsor-username} +``` + +`{canonical}` is the remote established in the Prerequisite (either `origin` or `upstream`); `origin` is always the push target. If the `sep/{slug}` branch already exists (e.g., re-entering this phase after a checkpoint pause), reuse it instead of creating a new one. Omit `--reviewer` if Q5 answered `None`. If `gh` prompts for a default repository, run `gh repo set-default modelcontextprotocol/modelcontextprotocol` and retry. + +Capture the PR number `{N}` from `gh pr create` output, then backfill: + +```bash +git mv seps/0000-{slug}.md seps/{N}-{slug}.md +# edit the file: replace SEP-{NUMBER} with SEP-{N} in the title line, +# fill the PR link in the preamble +npm run generate:seps +npm run format:docs +git add seps/{N}-{slug}.md docs/seps/ docs/docs.json +git commit --amend --no-edit +git push --force-with-lease +``` + +The amend keeps the rename in one commit per SEP-1850. `npm run generate:seps` renders `docs/seps/{N}-{slug}.mdx` and updates `docs/docs.json` (required for the `render-seps.yml` CI check), and `npm run format:docs` keeps the `markdown-format.yml` check green. + +**If Q5 answered `None`, the next step after the PR is open is finding a sponsor** — tag 1-2 relevant maintainers from `MAINTAINERS.md` on the PR and share it in the relevant Discord channel. The 6-month clock starts now. diff --git a/plugins/mcp-spec/skills/search-mcp-github/SKILL.md b/plugins/mcp-spec/skills/search-mcp-github/SKILL.md index d974c121c..c2eb3c222 100644 --- a/plugins/mcp-spec/skills/search-mcp-github/SKILL.md +++ b/plugins/mcp-spec/skills/search-mcp-github/SKILL.md @@ -1,6 +1,7 @@ --- name: search-mcp-github description: Search MCP PRs, issues, and discussions across the modelcontextprotocol GitHub org +license: Apache-2.0 user_invocable: true arguments: - name: topic diff --git a/seps/2149-working-group-charter-template.md b/seps/2149-working-group-charter-template.md index d82928af5..390cc7475 100644 --- a/seps/2149-working-group-charter-template.md +++ b/seps/2149-working-group-charter-template.md @@ -209,7 +209,7 @@ with the Core Maintainers in a core maintainer meeting. - There must be a widely acknowledged concern requiring coordination - PR for creation of WG into `docs/community//overview.mdx`, gated by CODEOWNERS requiring approval by Maintainers -- PR for charter into `docs/community//charter.mdx`, gated by CODEOWNERS requiring approval from Core Maintainers +- PR for charter into `docs/community//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:** diff --git a/seps/2243-http-standardization.md b/seps/2243-http-standardization.md new file mode 100644 index 000000000..f15626832 --- /dev/null +++ b/seps/2243-http-standardization.md @@ -0,0 +1,771 @@ +# SEP-2243: HTTP Header Standardization for Streamable HTTP Transport + + + + +- **Status**: Draft +- **Type**: Standards Track +- **Created**: 2026-02-04 +- **Author(s)**: MCP Transports Working Group +- **Sponsor**: None +- **PR**: https://github.com/modelcontextprotocol/specification/pull/2243 + +## 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. + +**Case Sensitivity**: Header names (called "field names" in [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110#name-field-names)) are case-insensitive. Clients and servers MUST use case-insensitive comparisons for header names. + +#### Example: tools/call Request + +```http +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 + +```http +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 + +```http +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: + +```http +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: + +```http +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 contain only ASCII characters (excluding space and `:`) +- MUST be case-insensitively unique among all `x-mcp-header` values in the `inputSchema` +- MUST only be applied to parameters with primitive types (number, string, boolean) + +Clients 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. + +**Example Tool Definition**: + +```json +{ + "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**: + +```json +{ + "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**: + +```http +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**: + +```json +{ + "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**: + +```http +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**: + +```json +{ + "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**: + +```http +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](https://datatracker.ietf.org/doc/html/rfc9110#name-field-values), 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 + - `number`: Convert to decimal string representation (e.g., `42`, `3.14`) + - `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: + +```text +Mcp-Param-{Name}: =?base64?{Base64EncodedValue}?= +``` + +The prefix `=?base64?` and suffix `?=` indicate that the value is Base64-encoded. Servers and intermediaries that need to inspect these values MUST decode them accordingly. + +**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=?=` | + +#### 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`) +1. Append the `Mcp-Method` header and, if applicable, `Mcp-Name` header to the request +1. Inspect the tool's `inputSchema` for properties marked with `x-mcp-header` and extract the value for each parameter +1. Encode the values according to the rules in [Value Encoding](#value-encoding) +1. Append a `Mcp-Param-{Name}: {Value}` header to the request: + +#### 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](#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**: + +```json +{ + "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. + +**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/')` | +| 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: + +```python +# 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](https://datatracker.ietf.org/doc/html/rfc9110)) 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](#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 | +| Malformed wrapper | `=?BASE64?SGVsbG8=?=` | Server MUST accept (case-insensitive prefix) | + +#### 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 diff --git a/seps/2557-tasks-stabilization.md b/seps/2557-adapt-tasks-for-stateless-and-sessionless-protocol.md similarity index 67% rename from seps/2557-tasks-stabilization.md rename to seps/2557-adapt-tasks-for-stateless-and-sessionless-protocol.md index 420954507..6f5c1273e 100644 --- a/seps/2557-tasks-stabilization.md +++ b/seps/2557-adapt-tasks-for-stateless-and-sessionless-protocol.md @@ -1,4 +1,4 @@ -# SEP-2557: Stabilizing Tasks +# SEP-2557: Adapat Tasks for Stateless & Sessionless Protocol - **Status**: Draft - **Type**: Standards Track @@ -9,17 +9,39 @@ ## Abstract -This proposal builds on [tasks](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) by introducing several simplifications to the original functionality to prepare the feature for stabilization, following implementation and usage feedback since the initial experimental release. In particular, this proposal allows tasks to be returned in response to non-task requests to remove unneeded stateful handshakes, and it collapses `tasks/result` into `tasks/get`, removing the error-prone interaction between the `input_required` task status and the `tasks/result` method. - -This SEP also incorporates changes into `Tasks` necessary following the acceptance of: +This SEP incorporates changes into `Tasks` necessary following the acceptance of: - [SEP-2260: Require Server requests to be associated with a Client request](./2260-Require-Server-requests-to-be-associated-with-Client-requests.md) - [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322) - [SEP-2243: Http Standardization](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243) +- [SEP-2567: Sessionless MCP via Explicit State Handles](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567). + +It also proposes a simplification of [tasks](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) based on feedback since the initial experimental release. + +This SEP DOES NOT move Tasks out of Experimentation status. + ## Motivation +A number of changes are necessary in the `Tasks` feature to support the upcoming changes to the spec listed in the SEPs above, and to address issues identified since the initial experimental release of `Tasks` in the `2025-11-25` specification release. + +### Changes to support upcoming SEPs +A number of changes are necessay to support the upcoming changes to the spec listed in the SEPs above. + +[SEP-2260] disallows unsolicited server-to-client requests, which invalidates the concept of client tasks for elicitation and sampling operations. This proposal removes client-hosted tasks & their associated capabiliteis since these scenarios are no longer supported. + +[SEP-2567] removes sessions from the protocol, which was the only defined scope for `tasks/list`. This proposal removes `tasks/list` methods and capabilities since there is no clear way to scope a list of tasks between a server and a client. + +[SEP-2322] introduces a new flow for how the server requests more input from the client, i.e. `elicitations`, `sampling`, and `roots`. SEP-2322 leverages the existing `input_required` task status to signal when the server needs more information. The client then makes a request to `tasks/result` to retrieve the `IncompleteResult` which contains the server's request for more information. The current specification requires the client to make this `tasks/result` request in a blocking manner, which is unintuitive and has led to implementation issues. We identified that we would need to make a breaking change to `tasks/result` during SEP-2322 discussions, but decided redesigning tasks would be a scope expansion that would derail MRTR discussion. This proposal simplifies this flow by inlining the `IncompleteResult` into the `tasks/get` response when the task is in an `input_required` state, eliminating the need for a separate blocking request and improving the intuitiveness of the flow. + +Given the difficulty of implementing the MRTR changes in Tasks as the spec currently stands, we believe that simplifying the Tasks flow by collapsing several methods into `tasks/get` is necessary and is also proposed here. + +[SEP-2243] introduces standard headers in the Streamable HTTP Transport to facilitate more efficient routing. Routing on `TaskId` is also desirable since there is often state associated with a specific Task that needs to be consistently routed to the same server instance. This proposal requires that the `Mcp-Name` header MUST be set to the value of `params.taskId` by the client when making `tasks/get` and `tasks/cancel` requests over the Streamable HTTP Transport. + +### Lessons Learned from Implementation & Usage Feedback +`Tasks` were introduced in an experimental state in the `2025-11-25` specification release, serving as an alternate execution mode for certain request types (tool calls, elicitation, and sampling) to enable polling for the result of a task-augmented operation. -**Tasks** were introduced in an experimental state in the `2025-11-25` specification release, serving as an alternate execution mode for certain request types (tool calls, elicitation, and sampling) to enable polling for the result of a task-augmented operation. This is done according to the following process, using tool calls as an example: +#### Current Task Flow Overview +This is done according to the following process, using tool calls as an example: 1. Check the receiver's task capabilities for the request type. If the capability is not present, the receiver does not support tasks. 1. Invoke `tools/list` to retrieve the tool list, then check `execution.taskSupport` on the appropriate tool to determine if the tool supports tasks. @@ -33,72 +55,244 @@ This SEP also incorporates changes into `Tasks` necessary following the acceptan 1. Once the task status is `completed`, the client issues a `tasks/result` call to retrieve the final result. 1. If the client still has an active `tasks/result` call from a prior `input_required` status, it will receive the result as the result to that open request. -The task-polling flow as currently-defined has several problems, most of which are related to the `input_required` status transition: +#### Current Task Flow Problems +The current flow has a number of problems: +1. `tasks/result` is overloaded and calling it to retrieve server requests when in the `input_required` status is unintuitive. +2. `tasks/result` is expected to block until the task is completed if called prematurely. This has led to [implementation issues](https://github.com/modelcontextprotocol/java-sdk/pull/755#issuecomment-3806079033). Requires long lived persistent connections, which many clients & servers do not want to implement. This problem still exists even with MRTR. +3. The flow is inefficient. Clients must make multiple calls to `tasks/get` to check on the status and then to `tasks/result` to retrieve the final result or required input. -1. Prematurely invoking `tasks/result` is unintuitive and it only done to accommodate the possibility of no other SSE streams being open in Streamable HTTP. -1. The fact that `tasks/result` blocks until completion is [even less intuitive](https://github.com/modelcontextprotocol/java-sdk/pull/755#issuecomment-3806079033). -1. Clients need to issue an additional request after encountering the `completed` status just to retrieve the final task result. -1. When a task reaches the `completed` status after this point, the server needs to identify all open `tasks/result` requests for that task to appropriately close them with the final task result, introducing unnecessary architectural complexity by mandating some sort of internal push-based messaging, which defies the intent of tasks' polling-based design. +Task Creation also has a number of issues since it is client-directed instead of being server-directed.Today the client must declare that it wants a task to be created by including the `task` field in the request. This creates several issues: +1. It requires requestors to explicitly check the capabilities of receivers. This introduces an unnecessary state contract that may be violated during mid-session deployments under the Streamable HTTP transport, and also raises concerns about the capability exchange growing in payload size indefinitely as more methods are supported. +2. It requires a tool-specific behavior carveout which gets pushed onto the client to navigate. Related to this, it forces clients to cache a `tools/list` call prior to making any task-augmented tool call. +3. It requires host application developers to explicitly choose to opt into task support from request to request, rather than relying on a single, consistent request construction path for all protocol operations. +4. It creates unnecessary burden on server developers to handle both the task and non-task flow for the same tool call. Most Tools will either return a `Task` or not, no use case has emerged where a server would want to return a `Task` for some calls and not others for the same tool, so this flexibility is unnecessary. -Tasks also require their requestor to be cooperative, in the sense that the requestor of a task must explicitly opt into task-augmented execution on a request. While this contract ensures that both the requestor and receiver understand their peer's capabilities and safely agree on the request and response formats in advance, it also has a few conceptual flaws: +## Specification +To support the new SEPs and solve the issues outlined above we propose the follwoing changes to the `Tasks` specification: -1. It requires requestors to explicitly check the capabilities of receivers. This introduces an unnecessary state contract that may be violated during mid-session deployments under the Streamable HTTP transport, and also raises concerns about the capability exchange growing in payload size indefinitely as more methods are supported. -1. It requires a tool-specific behavior carveout which gets pushed onto the client to navigate. Related to this, it forces clients to cache a `tools/list` call prior to making any task-augmented tool call. -1. It requires host application developers to explicitly choose to opt into task support from request to request, rather than relying on a single, consistent request construction path for all protocol operations. +1. Removes Features made obsolete by upcoming SEPs. +2. Task Creation is determined by the Server. +2. Servers MUST support `task/cancel` operation. +3. Removal of Task Capabilities that are no longer necessary. +4. Simplified Task Polling Flow which consolidates all polling onto the `tasks/get` method. -In practical terms, these flaws imply that an MCP server cannot make a clean break from non-task to task-augmented execution on its tools, even if clients have implemented support for tasks already; the server must wait for all host applications to additionally opt into tasks as well and sit in an awkward in-between state in the meantime, where it must choose to either break compatibility with host applications (even if those host applications have an updated client SDK) or accept the costs of task-optional execution and internally poll on tasks sometimes. The requirement that task support be declared ahead of time makes task execution predictable, but also prematurely removes the possibility of only dispatching a task when there is real work to be done, along the lines of the .NET [ValueTask](https://learn.microsoft.com/en-us/dotNet/api/system.threading.tasks.valuetask?view=net-10.0). Allowing the requestor to dictate whether or not a task will be created similarly eliminates the possibility of caching results or sending early return values, requiring the creation of a task on every request if tasks are supported by the requestor at all. +### Remove features made obsolete by upcoming SEPs +1. We will remove the concept of client-hosted tasks (Sampling & Elicitation), as SEP-2260 disallows unsolicited server-to-client requests +2. We will remove the optional `tasks/list` operation, as SEP-2567 removes sessions which was the only defined scope for listing tasks between a server and a client. We may expand task support to additional client-to-server request types in the future, and implementors are still advised against implementing tasks as a tool-specific protocol operation. -Furthermore, in [SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322), we identified that we would need to make a breaking change to `tasks/result` for these changes anyways, but that redesigning the flow within SEP-2322 would be a scope expansion that would derail MRTR discussion. Regardless, MRTR relies heavily on tasks as a solution for "persistent" requests that require server-side state, so these two proposals are somewhat interdependent. +### Task Creation Changes +Tasks will no longer be an optional capability, but will instead become a standard part of the protocol that servers MAY choose to implement. If a Tool returns a Task, Servers MUST declare `execution.tasksupport` on the tool definition. -To both improve the adoption of tasks and to reduce their upfront messaging overhead, this proposal simplifies their execution model by allowing peers to raise unsolicited tasks to each other and consolidating the polling lifecycle entirely into the `tasks/get` method. +Clients MUST understand Tasks, however they are not required to implement the polling workflow and leverage tools with `execution.taskSupport`. Clients MAY choose to filter out tools with `execution.taskSupport` declared. -## Specification + 1. We will remove the `tasks` capability declaration on both the client and the server. + 1. Servers MAY return `CreateTaskResult` or a `CallToolResult`in response to `CallToolRequest`. + 1. Servers SHOULD ignore the `task` field if it is present in a `CallToolRequest`. + 1. If a server returns a `CreateTaskResult`, it MUST support the `tasks/get` and `tasks/cancel` methods to allow clients to poll for the result and cancel the task if desired. + +### Task Cancel Changes +We propose aligning Task Cancellation with the Cooperative Cancellation model. In this model the requestor The requestor signals intent, but the worker decides when or if to honor it. This alligns `tasks/cancel with the cancellation pattern used in `notifications/cancelled`, where clients can always send a cancellation request, but servers can choose how to handle it. This also aligns Tasks with how Cancellation Tokens are handled in most async/concurrent programming languages including Go, Python, C#, TypeScript, etc... -The following changes will be made to the tasks specification: - -1. With respect to task creation - 1. We will remove the `tasks` capability declaration on both the client and the server. - 1. We will remove the `execution.taskSupport` field from the `Tool` shape. - 1. We will allow `CreateTaskResult` to be returned in response to `CallToolRequest` when no `task` field is present in the request. - 1. We will allow `CallToolResult` to be returned in response to `CallToolRequest` even when the `task` field is present in the request. -1. With respect to the task polling lifecycle: - 1. We will consolidate the entire polling lifecycle into the `tasks/get` method. This single method will handle retrieving task statuses and results simultaneously, and will additionally act as the carrier for receiver-to-requestor requests for the purposes of [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322). - 1. We will remove the requirement that requestors react to the `input_required` status by prematurely invoking `tasks/result` to side-channel requests on an SSE stream in the Streamable HTTP transport. - 1. We will inline the final task result or error into the `Task` shape, bringing that into `tasks/get` and all notifications by extension. - 1. We will inline outstanding server-to-client requests into a new `inputRequests` field on `GetTaskResult`, akin to the field of the same name used in [SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322). - 1. We will inline mid-task client-to-server results into a new `inputResponses` field on `GetTaskRequest`, akin to the field of the same name used in [SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322). - 1. We will remove the `tasks/result` method. -1. With respect to tasks in general: - 1. We will remove the concept of client-hosted tasks, as [SEP-2260](./2260-Require-Server-requests-to-be-associated-with-Client-requests.md) renders them conceptually invalid. - 1. We will remove the `tasks/list` operation. - 1. We may expand task support to additional client-to-server request types in the future, and implementors are still advised against implementing tasks as a tool-specific protocol operation. - 1. We will require `tasks/cancel` to be supported even if a server is incapabable or unwillling of offering actual task cancellation, similar to `notifications/cancelled` (it should instead return an error). - -### Task Capabilities Changes Summary +In the specification this means: + +Servers MUST support the `task/cancel` operation. A server MAY choose to ignore cancellation requests if its incapable or unwilling to offer cancellation of that Task. + +### Task Capabilities Changes +This SEP Removes all capabilities related to Tasks. +- SEP-2567 makes `tasks/list` impossible to support. +- SEP-2260 disallows unsolicited server-to-client requests, which invalidates the concept of client tasks for elicitation and sampling operations. +- This SEP makes Tasks a standard part of the protocol, and not a negotiated capability. +- This SEP Makes `tasks/cancel` required to be supported, even if a server does not wish to support tasks. The below table summarizes the changes to the task-related capabilities: | Role | Capability | Status | Description | | ------ | --------------------------------------- | ------- | ---------------------------- | -| Server | `tasks.requests.tools.call` | removed | | +| Server | `tasks.requests.tools.call` | removed | part of core protocol not an optional capability | | Server | `tasks.cancel` | removed | `tasks/cancel` is required | -| Server | `tasks.list` | removed | `tasks/list` is removed | +| Server | `tasks.list` | removed | no longer supported SEP-2567 | | Client | `tasks.requests.sampling.createMessage` | removed | no longer supported SEP-2260 | -| Client | `tasks.requests.elicitation.create` | removed | no longer supported SEP-2260 | -| Client | `tasks.cancel` | removed | no longer needed | -| Client | `tasks.list` | removed | no longer needed | +| Client | `tasks.requests.elicitation.create` | removed | no longer supported SEP-2260 | +| Client | `tasks.cancel` | removed | no longer needed, no client tasks | +| Client | `tasks.list` | removed | no longer supported SEP-2567 | + +### Task Flow Change +We propose simplifying the Task Flow into two methods: `tasks/get` and `tasks/cancel`. +- The `tasks/get` methods will handle retrieving task statuses and results simultaneously, and will additionally act as the carrier for receiver-to-requestor requests for the purposes of SEP-2322: Multi Round-Trip Requests. This simplifies the flow and allows for polling or streaming updates on a single endpoint. Moreover this more closely matches Long Running Operation APIs where there is a single endpoint that is polled for the status of an operation. +- The `tasks/cancel` method will be required and a separate endpoint. + +Below is an example of the new Task Flow. +```mermaid +sequenceDiagram + participant U as User + participant C as Client + participant S as Server + C->>S: tools/call (id: 1) + S-->>C: CreateTaskResult (id: 1, taskId: 123, status: working) + C->>S: tasks/get (taskId: 123) + S-->>C: GetTaskResult (taskId: 123, status: working) + C->>S: tasks/get (taskId: 123) + S-->>C: GetTaskResult (taskId: 123, status: input_required, inputRequests: {...}) + C->>U: Prompt User for Input + U-->>C: Provided Input + C->>S: tasks/get (taskId: 123, inputResponses: {...}) + S-->>C: GetTaskResult (taskId: 123, status: working) + C-->>S: tasks/get (taskId: 123) + S-->>C: GetTaskResult (taskId: 123, status: completed, result: {...}) +``` + +We will make the following changes to the specification to support this. +1. We will remove the requirement that requestors react to the input_required status by prematurely invoking tasks/result to side-channel requests on an SSE stream in the Streamable HTTP transport. +2. We will inline the final task result or error into the Task shape, bringing that into tasks/get and all notifications by extension. +3. We will inline outstanding server-to-client requests into a new inputRequests field on GetTaskResult, akin to the field of the same name used in SEP-2322. +4. We will inline mid-task client-to-server results into a new inputResponses field on GetTaskRequest, akin to the field of the same name used in SEP-2322. +5. We will remove the tasks/result method. +6. We will update error handling to remove the notion that a task can be failed due to non-JSON-RPC errors such as a tool result with isError: true to maintain a strong separation between protocol-level faults and application-level faults. +```diff +-For failures, the `error` field **MUST** contain the JSON-RPC error that occurred during execution, or the task **MAY** use `status: "failed"` with a `statusMessage` for tool results with `isError: true`. ++For failures, the `error` field **MUST** contain the JSON-RPC error that occurred during execution. The `failed` status **MUST NOT** be used to represent non-JSON-RPC errors, such as a tool result that completed with with `isError: true`. +``` + +#### task/get Specification +We will add the following language to the specification to define the new flow and the expected behavior of `tasks/get`: + +Upon receiving a `tasks/get` request with `inputResponses`, the server MUST process the provided responses and update the task state accordingly. The server MAY choose to transition the task back to `working` status if it determines that the provided input is sufficient to continue processing. + +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 [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322). 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. +5. if the status is `cancelled` the server MUST return a `Task` object with status `cancelled`. +4. if the status is `failed` the server MUST return a `Task` object with status `failed` and the error that occurred during execution. + + -### Task Methods Changes Summary +The below section contains example responses for each of the above cases. -The below table summarizes the changes to the task-related methods: +Status: Working + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "working", + "statusMessage": "The operation is in progress.", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 60000, + "pollInterval": 5000 + } +} +``` -| Method | Status | Description | -| --------------------------------- | --------------- | --------------------------------------------------------------------- | -| `tasks/get` | still supported | Consolidates the entire polling lifecycle into a single method. | -| `tasks/result` | removed | No longer needed; results are inlined into `tasks/get`. | -| `tasks/input_response` (SEP-2322) | removed | No longer needed; results are inlined into `tasks/get`. | -| `tasks/cancel` | still supported | Required to be supported even if actual cancellation is not possible. | -| `tasks/list` | removed | Use cases can be satisfied with client handling. | +Status: Completed + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "completed", + "statusMessage": "The operation has completed successfully.", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 60000, + "pollInterval": 5000, + "result": { + "content": [ + { + "type": "text", + "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy" + } + ], + "isError": false + } + } +} +``` + +Status: Failed + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "failed", + "statusMessage": "Tool execution failed: API rate limit exceeded", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 30000, + "error": { + "code": -32603, + "message": "API rate limit exceeded" + } + } +} +``` + +Status: Cancelled + +```json +{ + "jsonrpc": "2.0", + "id": 6, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "cancelled", + "statusMessage": "The task was cancelled by request.", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 30000, + "pollInterval": 5000 + } +} +``` + + Status: input_required + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "resultType": "task", + "result": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", + "status": "input_required", + "statusMessage": "The operation requires additional input.", + "createdAt": "2025-11-25T10:30:00Z", + "lastUpdatedAt": "2025-11-25T10:40:00Z", + "ttl": 60000, + "pollInterval": 5000, + "inputRequests": { + "github_login": { + "method": "elicitation/create", + "params": { + "mode": "form", + "message": "Please provide your GitHub username", + "requestedSchema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"] + } + } + } + } + } +} +``` ### Task Schema Changes @@ -143,7 +337,7 @@ interface FailedTask extends Task { type DetailedTask = Task | InputRequiredTask | CompletedTask | FailedTask; ``` -### Client Requests for `task/get` +#### Client Requests for `task/get` ```typescript interface GetTaskRequest extends JSONRPCRequest { @@ -166,7 +360,7 @@ interface GetTaskRequest extends JSONRPCRequest { } ``` -### Server Response for `task/get` +#### Server Response for `task/get` ```typescript type GetTaskResult = Result & DetailedTask; @@ -174,7 +368,7 @@ type GetTaskResult = Result & DetailedTask; type TaskStatusNotificationParams = NotificationParams & DetailedTask; ``` -### `ResultType` +#### `ResultType` The ResultType field was introduced in [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322) to handle polymorphic results. `Tasks` has the same issue where a server may return a `CallToolResult` or a `CreateTaskResult`. To address this, we propose the addition of the `task` ResultType to indicate that a Response contains a `Task` object. @@ -184,28 +378,10 @@ type ResultType = "complete" | "incomplete" | "task"; For backwards compatibility ResultType is inferred by default to be `complete`. Therefore all calls which return a `Task` (i.e.`task/get`, `task/cancel`) calls must set `task` as the ResultType moving forward. -### Example Task Flow - -```mermaid -sequenceDiagram - participant U as User - participant C as Client - participant S as Server - C->>S: tools/call (id: 1) - S-->>C: CreateTaskResult (id: 1, taskId: 123, status: working) - C->>S: tasks/get (taskId: 123) - S-->>C: GetTaskResult (taskId: 123, status: input_required, inputRequests: {...}) - C->>U: Prompt User for Input - U-->>C: Provided Input - C->>S: tasks/get (taskId: 123, inputResponses: {...}) - S-->>C: GetTaskResult (taskId: 123, status: working) - C-->>S: tasks/get (taskId: 123) - S-->>C: GetTaskResult (taskId: 123, status: completed, result: {...}) -``` Below (collapsed) is the full JSON example of a tool call with an unsolicited task-augmentation that matches the diagram above: -
+### Example Task Flow Consider a simple tool call, `hello_world`, requiring an elicitation for the user to provide their name. The tool itself takes no arguments. @@ -444,182 +620,6 @@ Eventually, the server completes the request, so it stores the final `CallToolRe } ``` -
- -### `tasks/get` Behavior by Task State - -A `Task` can be in one of the following states: `working`, `completed`, `failed`, `cancelled`, or `input_required`. This section defines the expected behavior of a call to `tasks/get` when in each state. - -#### Working - -The response MUST include the `working` status. - -
- -```json -{ - "jsonrpc": "2.0", - "id": 1, - "resultType": "task", - "result": { - "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", - "status": "working", - "statusMessage": "The operation is in progress.", - "createdAt": "2025-11-25T10:30:00Z", - "lastUpdatedAt": "2025-11-25T10:40:00Z", - "ttl": 60000, - "pollInterval": 5000 - } -} -``` - -
- -#### Completed - -When a task is in the `completed` state, a call to `tasks/get` MUST return the `Task` with status `completed` and include the final result of the task. - -
- -```json -{ - "jsonrpc": "2.0", - "id": 1, - "resultType": "task", - "result": { - "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", - "status": "completed", - "statusMessage": "The operation has completed successfully.", - "createdAt": "2025-11-25T10:30:00Z", - "lastUpdatedAt": "2025-11-25T10:40:00Z", - "ttl": 60000, - "pollInterval": 5000, - "result": { - "content": [ - { - "type": "text", - "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy" - } - ], - "isError": false - } - } -} -``` - -
- -#### Failed - -When a task is in the `failed` state, a call to `tasks/get` should return an error in the `result` field indicating the reason for the failure. - -To maintain a strong separation between the handling of protocol faults and application-level faults, we will revise prior language suggesting that the `failed` state may be used for tool call errors. Specifically, in error-handling paths for tasks, we will make the following change to the "Result Retrieval" section of the existing specification: - -```diff -### Result Retrieval - -When a task reaches a terminal status (`completed`, `failed`, or `cancelled`), servers **MUST** inline the final result or error into the `Task` object returned by `tasks/get`. - -For successful completion, the `result` field **MUST** contain what the underlying request would have returned (e.g., `CallToolResult` for `tools/call`). - --For failures, the `error` field **MUST** contain the JSON-RPC error that occurred during execution, or the task **MAY** use `status: "failed"` with a `statusMessage` for tool results with `isError: true`. -+For failures, the `error` field **MUST** contain the JSON-RPC error that occurred during execution. The `failed` status **MUST NOT** be used to represent non-JSON-RPC errors, such as a tool result that completed with with `isError: true`. - -Servers **MUST** include the `result` or `error` field in `notifications/tasks/status` notifications when notifying about terminal status transitions. -``` - -
- -```json -{ - "jsonrpc": "2.0", - "id": 1, - "resultType": "task", - "result": { - "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", - "status": "failed", - "statusMessage": "Tool execution failed: API rate limit exceeded", - "createdAt": "2025-11-25T10:30:00Z", - "lastUpdatedAt": "2025-11-25T10:40:00Z", - "ttl": 30000, - "error": { - "code": -32603, - "message": "API rate limit exceeded" - } - } -} -``` - -
- -#### Cancelled - -When a task is in the `cancelled` state, a call to `tasks/get` MUST return the `Task` with status `cancelled`. - -
- -```json -{ - "jsonrpc": "2.0", - "id": 6, - "resultType": "task", - "result": { - "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", - "status": "cancelled", - "statusMessage": "The task was cancelled by request.", - "createdAt": "2025-11-25T10:30:00Z", - "lastUpdatedAt": "2025-11-25T10:40:00Z", - "ttl": 30000, - "pollInterval": 5000 - } -} -``` - -
- -#### `input_required` - -If the task status is `input_required`, this indicates that the `Task` requires additional input from the client before it can proceed. The server MUST return the `Task` with status set to `input_required` and an `IncompleteResult` from [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322). - -
- -```json -{ - "jsonrpc": "2.0", - "id": 1, - "resultType": "task", - "result": { - "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", - "status": "input_required", - "statusMessage": "The operation requires additional input.", - "createdAt": "2025-11-25T10:30:00Z", - "lastUpdatedAt": "2025-11-25T10:40:00Z", - "ttl": 60000, - "pollInterval": 5000, - "inputRequests": { - "github_login": { - "method": "elicitation/create", - "params": { - "mode": "form", - "message": "Please provide your GitHub username", - "requestedSchema": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": ["name"] - } - } - } - } - } -} -``` - -
- ### HTTP Streamable Transport Headers [SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243) introduces standard headers in the Streamable HTTP Transport to facilitate more efficient routing. Routing on `TaskId` is also desirable since there is often state associated with a specific Task that needs to be consistently routed to the same server instance. SEP-2243 requires that all requests and notifications declare an `Mcp-Method` header. @@ -662,6 +662,8 @@ In the updated "Task Support and Handling" section under "​Behavior Requiremen This addition is intended 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. While this 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. + ## Backward Compatibility ### `tasks/result` diff --git a/seps/TEMPLATE.md b/seps/TEMPLATE.md index f37a02e23..7783426ab 100644 --- a/seps/TEMPLATE.md +++ b/seps/TEMPLATE.md @@ -7,7 +7,7 @@ - **Created**: YYYY-MM-DD - **Author(s)**: Name (@github-username) - **Sponsor**: @github-username (or "None" if seeking sponsor) -- **PR**: https://github.com/modelcontextprotocol/specification/pull/{NUMBER} +- **PR**: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/{NUMBER} ## Abstract