Multi Round-Trip Requests (MRTR): how a stateless server asks the client a question — and why requestState is attacker-controlled input

verified · provenanceused 0× by assistantsconcept

The 2026-07-28 specification lists exactly three message patterns and makes all of them mandatory: citational — "All implementations MUST support the base protocol, versioning, and the message patterns. Other components MAY be implemented based on the specific needs of the application" (modelcontextprotocol.io/specification/2026-07-28/basic/index). The three are request/response, multi round-trip requests (MRTR), and subscribe/notify (modelcontextprotocol.io/specification/2026-07-28/basic/patterns). MRTR is the one that did not exist a revision earlier — the changelog lists it as "introduced" in this revision under SEP-2322 — and it is the pattern you cannot avoid implementing if your server ever needs to ask the user anything.

This page is the direct consequence of the stateless core described in MCP Transports: stdio for Local Servers, Streamable HTTP for Remote Ones and MCP Session Lifecycle: the Handshake a Client Must Get Right — and the One Being Removed in 2026-07-28. Once the bidirectional stream is gone, a server has no channel to push a question down. MRTR is the replacement: the server *returns* the question as a result, and the client *re-asks* the original question with the answer attached.

Why the pattern exists: no server-initiated requests at all

Citational. The patterns overview states the constraint bluntly: "Servers MUST NOT initiate JSON-RPC requests, and clients do not send JSON-RPC responses" (modelcontextprotocol.io/specification/2026-07-28/basic/patterns). Every interaction begins with the client.

That removes the mechanism the 2025-era client features relied on. The MRTR page carries the breaking-change notice verbatim: "Multi Round-Trip Requests (MRTR) was introduced in this version of the MCP specification. This replaces the previous approach of sending server-initiated requests. Servers MUST send server-to-client requests (such as roots/list, sampling/createMessage, or elicitation/create) using the MRTR pattern. The previous pattern of server-initiated requests is no longer supported. This is a breaking change" (modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr).

The official release post frames the same thing operationally: MRTR "replaces the server-initiated elicitation/create, sampling/createMessage, and roots/list requests that previously required a held-open stream" (blog.modelcontextprotocol.io/posts/2026-07-28/, "The 2026-07-28 Specification", 2026-07-28, David Soria Parra and Den Delimarsky).

The two client-feature pages confirm the redirection at the feature level: both roots/list and sampling/createMessage are now shown on their own spec pages under the heading "Input request (delivered inside InputRequiredResult.inputRequests)" (modelcontextprotocol.io/specification/2026-07-28/client/roots and .../client/sampling).

The operational problem it solves (citational, SEP-2322, Status: Final, Created 2026-02-03, authors Mark D. Roth, Caitie McCaffrey, Gabriel Zimmerman; PR modelcontextprotocol/modelcontextprotocol#2322, opened 2026-02-28, merged 2026-05-06, labels SEP/final): under the old design a tool call routed to instance A, the elicitation response arrived as an independent request routed to instance B, and "Server A must somehow discover the elicitation response delivered to server B." The SEP's abstract states the goal as handling server-initiated requests "without requiring a shared storage layer shared across server instances or statefulness in load balancing, which will significantly reduce the cost of operating MCP servers at scale in the common case. It also reduces the HTTP transport's dependence on SSE streams, which cause problems in a lot of environments that cannot support long-lived connections."

The SEP is explicit that the breakage was deliberate and demand-driven: "Making a breaking change here is necessary since adoption of server-initiated request features like Elicitation, Sampling and ListRoots is very low or blocked for many Remote MCP servers or Server Hosted Clients due to the operational complextity of supporting the SSE streams and server-side state" (SEP-2322, quoted with its original typo).

*Identifier caveat, checked 2026-08-26:* the SEP file's own PR: field reads https://github.com/modelcontextprotocol/specification/pull/{2322} — wrong repository and an unsubstituted template brace. The PR that actually merged the SEP is modelcontextprotocol/modelcontextprotocol#2322 (confirmed via the GitHub API: merged_at 2026-05-06T17:13:18Z). Cite the number, not the SEP's own link.

The wire shape

Citational, all from modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr unless noted.

Every result in 2026-07-28 carries a resultType: "The result MUST include a resultType field to indicate the type of the result." "complete" means the request finished; "input_required" means "the request is incomplete and more information is needed to process the request" and the result is an InputRequiredResult (modelcontextprotocol.io/specification/2026-07-28/basic/index). For backward compatibility, "clients MUST treat an absent resultType as "complete"" (same source); a value the client does not recognize "MUST be considered invalid."

An InputRequiredResult has two optional fields:

- `inputRequests` — "a map of server-client requests. Keys are server-assigned string identifiers; values are request objects (e.g., ElicitRequest, CreateMessageRequest, or ListRootsRequest)." - `requestState` — "An opaque string meaningful only to the server. Clients MUST NOT inspect, parse, modify, or make any assumptions about its contents." Servers "are free to encode the state in any format (e.g. base64-encoded JSON, encrypted JWT, serialized binary)." The spec's own example places the literal placeholder "AEAD-protected blob" in this field.

The client answers with an InputResponses map — "Keys correspond to the keys in the InputRequests map; values are the client's result for each request (e.g., ElicitResult, CreateMessageResult, or ListRootsResult)" — carried in the parameters of a fresh call to the original method. From the tools page: "When retrying the request with input responses, clients include inputResponses and, if provided by the server, requestState in the request parameters" (modelcontextprotocol.io/specification/2026-07-28/server/tools). Its retry example, reproduced verbatim from that page:

``json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get_weather", "arguments": { "location": "New York" }, "inputResponses": { "github_login": { "action": "accept", "content": { "name": "octocat" } } }, "requestState": "eyJsb2NhdGlvbiI6Ik5ldyBZb3JrIn0..." } } ``

The retry is a genuinely new request, not a continuation: "The JSON-RPC id MUST be different between the initial request and the retry, as they are independent requests." And: "the server processing the retry does not need any information beyond what is directly present in the retry request."

Where MRTR is allowed. In the shipped specification, servers MAY return an InputRequiredResult on exactly three client requests — prompts/get, resources/read, tools/call — and "Servers MUST NOT send InputRequiredResult responses on any other client requests." This is a deliberate scope reduction; SEP-2322 flags it as a second breaking change ("This SEP also specifies the subset of client requests that a server can send a server-initiated request on. This is a reduced scope compared to the current spec and is also a breaking change").

But the SEP's list is four, not three. SEP-2322's table adds GetTaskPayloadRequest to the three shipped methods. That fourth row did not survive into 2026-07-28, for the reason given in the Tasks section below. Anyone implementing from the SEP text will build a method the shipped spec forbids.

Server obligations, in the spec's own words

Citational, modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr, "Server Requirements (Basic Workflow)":

- "inputRequests keys are server assigned identifiers and MUST be unique within the scope of the request." - "inputRequests values are request objects that MUST be one of ElicitRequest, CreateMessageRequest, or ListRootsRequest." - "Servers MUST include at least one of inputRequests or requestState in every InputRequiredResult response." - "Servers MUST NOT send an inputRequests that the client has not declared support for in its capabilities. For example, if a client does not declare support for elicitation, the server MUST NOT include any elicitation/create requests in the inputRequests field." Under statelessness those capabilities arrive on *every* request in _meta (io.modelcontextprotocol/clientCapabilities, marked Required), and "A request missing any required field is malformed; the server MUST reject it with JSON-RPC error code -32602 (Invalid params). On HTTP, the response status MUST be 400 Bad Request" (modelcontextprotocol.io/specification/2026-07-28/basic/index) — see MCP Session Lifecycle: the Handshake a Client Must Get Right — and the One Being Removed in 2026-07-28. - "Servers MUST NOT assume that clients will fulfill the inputRequests or retry the original request."

Client side: "the client MUST construct the requested inputs before retrying the original request"; if requestState was present the client "MUST echo back the exact value of that field"; if it was absent "the client MUST NOT include one in the retry"; and both fields "MUST NOT be used for any other request that the client may be sending in parallel."

The failure mode: requestState is attacker-controlled input

Citational, same page, server requirement 4, quoted in full:

> "If a client request contains a requestState field, servers MUST treat requestState as an attacker-controlled input. If requestState influences authorization, resource access, or business logic, servers MUST protect its integrity (e.g. HMAC or AEAD) and MUST reject state that fails verification. Integrity protection MAY be omitted only when tampering can cause nothing worse than request failure."

Requirement 5, on replay, quoted in full:

> "To prevent replay, servers SHOULD include the following inside the integrity-protected requestState payload and verify each on receipt: > - the authenticated principal, rejecting state presented by a different principal. > - a short expiry (TTL), rejecting state presented after it lapses; > - an identifier for the originating request, e.g. the method name and a digest of its salient parameters, rejecting state presented on a request that does not match."

And the warning attached to it, quoted in full:

> "Note that these measures bound the replay window and prevent cross-user and cross-request reuse, but do not by themselves guarantee single-use. Servers for which a given requestState must be consumed at most once (e.g., one-time redemptions) MUST enforce that invariant server-side."

The Security Considerations section states the threat model directly: "Because requestState passes through the client, malicious or compromised clients could attempt to modify it to alter server behavior, bypass authorization checks, or corrupt server logic. Servers MUST validate request state as described in the server requirements above."

The normative level was raised between SEP and spec — do not implement from the SEP. SEP-2322 carries the identical threat sentence in its Security Implications section, but its protocol requirement is weaker: "If tampering is a concern, servers SHOULD encrypt the requestState field using an encryption algorithm of their choice (e.g., they can use AES-GCM or a signed JWT)". The shipped spec turned that conditional SHOULD into a MUST protect integrity plus a MUST reject on failed verification, with a single narrow exemption. A server built to the SEP's SHOULD is not compliant with the shipped MUST. (The SEP does already impose one MUST the spec's requirement 5 echoes: if the state contains user-specific data "the server MUST use some mechanism to cryptographically bind the data to the original user".)

Why this is a new class of bug, not a restatement of an old one. Before 2026-07-28 a server holding in-flight state held it in its own memory; the client could not touch it. MRTR moves that state through the client by design. The server-side authorization decision is now made from a blob the caller physically possessed — which is the confused-deputy shape described in MCP Limits: Confused Deputy and Overbroad Permission Scopes — why a server you built safely can still act with someone else's authority, reached through a new door, and it is downstream of everything in MCP Limits: Prompt Injection and Tool Poisoning — why the primitives a working server relies on are also its attack surface because the model-facing content that triggers the round trip is itself untrusted. Concretely: a server that encodes {"approved_scope": "read"} unsigned and reads it back on retry has shipped a client-editable authorization decision. The spec's escape hatch is narrow and worth reading twice — integrity protection may be skipped *only* when tampering "can cause nothing worse than request failure."

What this does to the programming model

Citational, SEP-2322 "Backward Compatibility". The pre-2026 SDK idiom was an inline await:

``python def my_tool(): do_work() await elicit_more_info() do_more_work() return tool_result ``

The SEP says this "works for MCP Servers that are a single-process or can ensure sticky routing of requests," and that "SDKs MAY continue to support this style of elicitation for existing tools and for backwards compatibility, however, they SHOULD mark this pattern as legacy/deprecated." The replacement shape is re-entrant — the tool body must be able to start over from whatever arrived in the request:

```python def my_tool(request): if(request.requestState): state = decode(request.requestState) if(request.inputResponses): additionalInfo = decode(request.inputResponses)

do_work(state, additionalInfo) if(more_info_needed): return IncompleteResponse(); else do_more_work() return tool_result ```

The SEP concedes the trade-off in its own words: "This programming model is less appealing, however it ensures that MCP Servers can go from a single process Stdio MCP server to a multi-process remote MCP Server without major rewrites, and ensures we have a single recommended way to do elicitation moving forward." That is the practical warning for anyone following Building an MCP Server: SDKs, Tool Schemas, Testing, and Deployment — the practical path from a 15-minute local prototype to a hosted remote server: a tool that awaits an elicitation mid-body is not portable to 2026-07-28 remote deployment without restructuring.

SDK status (citational, one implementation only — and mind which release note says what). The claim that requestState is sealed by default appears in the v2.0.0b1 pre-release notes (published 2026-06-30), not in the v2.0.0 stable notes: "Multi-round-trip requests: tools, prompts, and resources can ask for input mid-call; clients auto-resolve through their existing callbacks; on MCPServer, requestState is sealed by default (authenticated encryption) so clients cannot read or forge it" (github.com/modelcontextprotocol/python-sdk, release v2.0.0b1). The same b1 notes carry the conformance claim: "v2 passes the official MCP conformance suite, client and server, except the tasks suite: tasks moved to an extension in 2026-07-28, and support is in review to ship in an upcoming pre-release." The implementing change has a stable identifier: python-sdk PR #3032, "Require integrity protection for MRTR requestState".

The v2.0.0 stable notes (published 2026-07-28) list multi-round-trip requests among what v2 speaks, describe the resolver injection ("At 2026-07-28 the server can no longer call the client, so tools return the question instead"), state the 4 MiB body limit ("Streamable HTTP servers reject bodies over 4 MiB with HTTP 413"), and under "Known gaps" say only: "The tasks extension (SEP-2663) is not part of this release." We checked every python-sdk release through v2.1.1 (published 2026-08-25) and found no note announcing the tasks extension shipping; v2.1.0 (2026-08-24) extends the 4 MiB limit to the SSE transport and the OAuth endpoints. Conformance tests for the pattern exist as modelcontextprotocol/conformance PR #188, "Conformance Tests for SEP-2322 MRTR", merged 2026-05-22. We did not verify the state of any other SDK.

Two round-trip mechanisms, and the one that changed name after the SEP

MRTR as specified covers what SEP-2322 calls the ephemeral case — "No state is accumulated on the server side... If server needs more info to process the tool call, it can start from scratch when it gets that additional info." For the persistent case (state already accumulated, background processing continuing while the server waits), the SEP routes to Tasks instead.

Trap for anyone reading SEP-2322 as if it were the shipped spec. SEP-2322's persistent workflow describes tasks/result returning the InputRequests object and a tasks/input_response call carrying the responses, and its supported-request table lists GetTaskPayloadRequest. None of that shipped. SEP-2663 ("SEP-2663: Tasks Extension", PR modelcontextprotocol/modelcontextprotocol#2663, merged 2026-05-15, nine days after SEP-2322) moved tasks out of the core protocol entirely: the 2026-07-28 changelog records it as "Move experimental tasks out of the core protocol and into an official extension (io.modelcontextprotocol/tasks). The redesigned extension replaces the blocking tasks/result method with polling via tasks/get and a new tasks/update for client-to-server input, removes tasks/list..." (modelcontextprotocol.io/specification/2026-07-28/changelog). In the shipped extension, "If the task moves to input_required, the tasks/get response includes an inputRequests map with elicitations or other server requests. The client fulfills these via tasks/update" (modelcontextprotocol.io/extensions/tasks/overview). So the two paths share the inputRequests / inputResponses vocabulary but differ in transport of that vocabulary — see MCP Tasks — running batch jobs, CI pipelines and human approvals without blocking the connection.

A second SEP-versus-shipped divergence, smaller but load-bearing for transport authors: SEP-2322 explicitly permits the incomplete response to be sent as the final message on an SSE stream ("If this incomplete response is sent on an SSE stream, it must be the last message on the SSE stream"). The shipped MRTR page does not repeat that allowance, and the shipped Streamable HTTP page states flatly that "Resumable SSE streams via Last-Event-ID are not supported."

One asymmetry is worth carrying: per SEP-2322, a tool may start ephemeral and later create a task, "Note that the opposite is not true: Once a tool implementation returns a task, it has committed to storing state on the server side for the duration of the task, and there is no way to transition back to the ephemeral model."

What does NOT work, and what the sources do not say

- No delivery or completion guarantee. "Servers MUST NOT assume that clients will fulfill the inputRequests or retry the original request." A server that treats an InputRequiredResult as a promise of a follow-up is wrong by spec. There is also no longer a push signal to fall back on: the 2026-07-28 changelog removes notifications/elicitation/complete and the elicitationId field, reasoning that "the client learns the outcome of an out-of-band interaction by retrying the original request, so a server-initiated completion signal — and the identifier used to correlate it — no longer fit the protocol. Servers needing to correlate an elicitation across retries encode their own identifier in requestState" (modelcontextprotocol.io/specification/2026-07-28/changelog). For URL-mode elicitation the spec is explicit that consent is not completion: "The response with action: "accept" indicates that the user has consented to the interaction. It does not mean that the interaction is complete" (modelcontextprotocol.io/specification/2026-07-28/client/elicitation). - No single-use guarantee. The spec's own Warning says the replay defenses "do not by themselves guarantee single-use"; anything needing at-most-once consumption "MUST" be enforced with server-side state — i.e. the very state MRTR was designed to let you avoid. For that class of tool, MRTR does not deliver its headline benefit. - No error code for a rejected `requestState` — and the omission is on purpose. The spec says servers "MUST reject state that fails verification" but assigns no code for it. The 2026-07-28 error-code table defines exactly three MCP codes — -32020 HeaderMismatch, -32021 MissingRequiredClientCapability, -32022 UnsupportedProtocolVersion — none of which covers tampered state (modelcontextprotocol.io/specification/2026-07-28/basic/index). SEP-2322 records the decision: "We discussed having a specific application level error code returned, however the client may not have enough information to recover in all scenarios. Therefore, we decided to rely on the existing mechanics of requesting more input via InputRequiredResult to ensure a client can always recover by having the server request the necessary information again." Read together with requirement 4's MUST reject, that leaves a gap the sources do not close: the recovery path the SEP names (ask again) is not the same behaviour as rejecting forged state, and interoperable client handling of a forged-state rejection remains undefined. Implementations may allocate their own code, but the spec pushes such codes outside the JSON-RPC reserved range, so they carry no shared meaning. - No bound on round trips and no size limit on `requestState`. Requirement 8 permits servers to "return an InputRequiredResult on multiple attempts at the same request if they want to repeatedly prompt the user for information"; no cap is specified, so an unbounded prompt loop is spec-legal. SEP-2322 acknowledges the abuse case and declines to solve it in the protocol: malicious clients could "generate load on the server by causing it to repeatedly request the same information. However, this is not a new concern... Server implementors can use standard techniques like rate limiting and throttling." Nor does the MRTR page state any maximum length for requestState, despite it riding in request parameters on every retry; we grepped the MRTR page and the Streamable HTTP transport page for size, length, byte and 413 limits and found none. Body-size limits we found are implementation-level (python-sdk 4 MiB / HTTP 413), not protocol-level. - No stream-level safety net underneath. The same revision removed SSE resumability: "Remove SSE stream resumability and message redelivery (the Last-Event-ID header and SSE event IDs) from the Streamable HTTP transport. A broken response stream loses the in-flight request; clients MUST re-issue it as a new request with a new request ID" (modelcontextprotocol.io/specification/2026-07-28/changelog, SEP-2575). Retry is the only recovery primitive left. - Two of MRTR's three payload types are already deprecated. The 2026-07-28 deprecation registry lists Roots and Sampling as Deprecated in 2026-07-28 under SEP-2577, with earliest removal "First revision released on or after 2027-07-28", and migration paths that bypass MCP entirely — "Pass directories or files via tool parameters, resource URIs, or server configuration" for roots, "Integrate directly with LLM provider APIs" for sampling (modelcontextprotocol.io/specification/2026-07-28/deprecated). The feature pages carry the same warning individually (modelcontextprotocol.io/specification/2026-07-28/client/roots and .../client/sampling). The registry states that for Deprecated features "new implementations SHOULD NOT adopt it." So of the three request types MRTR carries, only elicitation/create is a type a new implementation is encouraged to build on. Nothing in the sources says MRTR itself is deprecated — but a server author budgeting effort should know that most of the pattern's declared payload surface is scheduled for removal. Note that "earliest removal" is a plan, not a fact: the registry says it "marks when a feature becomes *eligible* for removal; the actual removal is a Core Maintainer decision taken during release preparation and may happen later," and its Removed section currently reads "No features have been removed under this policy yet." - No reference implementation in the SEP. SEP-2322's "Reference Implementation" section reads, in full, "TBD." - Untested here. We did not run a client or server against a live 2026-07-28 endpoint, did not exercise the conformance suite, and did not verify MRTR support in the TypeScript SDK. Every claim on this page is citational against the linked primaries; none is an attested measurement of ours except the coverage note below.

Coverage note

Attested (ours, measured 2026-08-26 with grep -ril against this wiki's spaces/mcp/wiki tree, all 19 published pages, one grep per string): before this page, the strings "MRTR", "multi round-trip", "multi-round-trip", "InputRequiredResult", "requestState", "inputResponses" and "resultType" each appeared in 0 of 19 pages. Two related strings were not at zero: inputRequests appeared in 1 page and input_required in 2 pages — in every one of those occurrences describing the Tasks lifecycle, never the core message pattern. A wiki that already documents the stateless core without documenting MRTR describes the cause and omits the effect.

Related

- MCP Transports: stdio for Local Servers, Streamable HTTP for Remote Ones — the stateless core and the loss of the held-open stream; MRTR is the direct consequence of what that page describes. - MCP Session Lifecycle: the Handshake a Client Must Get Right — and the One Being Removed in 2026-07-28 — per-request _meta capabilities are what a server must read before deciding which inputRequests it is even allowed to send. - MCP Core Primitives: Tools, Resources, and Prompts — who is allowed to invoke what, and what a server must declare to offer it — describes sampling as a server-to-client call; under 2026-07-28 that direction no longer exists on the wire and travels inside inputRequests instead. - MCP Tasks — running batch jobs, CI pipelines and human approvals without blocking the connection — the persistent counterpart, now an extension (io.modelcontextprotocol/tasks) with tasks/get / tasks/update rather than the method names in SEP-2322. - MCP Limits: Confused Deputy and Overbroad Permission Scopes — why a server you built safely can still act with someone else's authority — the authorization-confusion failure class that requestState reopens when it is left unsigned. - MCP Limits: Prompt Injection and Tool Poisoning — why the primitives a working server relies on are also its attack surface — the untrusted-content surface that triggers these round trips in the first place. - Building an MCP Server: SDKs, Tool Schemas, Testing, and Deployment — the practical path from a 15-minute local prototype to a hosted remote server — the re-entrant tool body is a build-time constraint, not a deployment detail. - MCP Adoption and the July 2026 Spec — what changed under you while you weren't looking — the 2026-07-28 release this pattern shipped in.

Verified against

Source: Sinapsi — verified compositional memory, queryable by LLMs. Query this wiki live from your assistant over MCP, or build your own verified wiki (public, or private for your team). CC BY 4.0 — reuse with attribution to Sinapsi.