The problem MRTR solves
Before MCP 2026-07-28, a server that needed something from the client mid-call could send a server-initiated request while the original call remained active.
elicitation/create asked the user a question. sampling/createMessage borrowed the client's model. roots/list asked which filesystem roots the client exposed or recommended for the operation. These were server-initiated requests; when used mid-call, the original request remained active while the client responded.
That worked and it produced some of MCP's most interesting interactions like a tool that confirms before deleting, a server that asks for the one parameter the model didn't supply or an agent that borrows the client's LLM rather than provisioning its own.
But stateless, horizontally scaled deployments could not support it easily without reintroducing sticky routing, shared coordination state, long-lived infrastructure or an instance that stayed alive while the client or user responded.
So a pattern especially affected by statelessness was also important for interactive production deployments. That's the tension MRTR resolves.
What MRTR actually does
In short, instead of the server reaching back to the client, the server returns early and asks the client to come back.
Concretely, a call to tools/call (or prompts/get, or resources/read) can now return without completing. Rather than a finished result, the server returns an InputRequiredResult:
{
"resultType": "input_required",
"inputRequests": {
"confirm": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Delete 3 files?",
"requestedSchema": {
"type": "object",
"properties": { "confirmed": { "type": "boolean" } },
"required": ["confirmed"]
}
}
}
},
"requestState": "AEAD-protected opaque state"
}Two optional fields carry the weight, and every InputRequiredResult must contain at least one of them. inputRequests is a map of server-assigned keys to full elicitation/create, sampling/createMessage, or roots/list request objects. requestState is an opaque string that the client must echo back exactly when it retries. One constraint is easy to miss: a server must not send an input request of a kind the client has not declared in its capabilities.
The client fulfills the requests by prompting the user, calling its model, or listing its roots, and then re-issues the original method with the original parameters, inputResponses keyed to match inputRequests, and the echoed requestState when one was supplied. The retry is an independent JSON-RPC request and must use a new request ID.
Because the information needed to resume can live in the payload, the retry can land on a different instance or availability zone. That portability assumes every eligible instance has compatible code and state formats, the necessary cryptographic key material, and access to any required downstream data.
A third field is easy to overlook: every result in this protocol version carries a required resultType. Ordinary results use "complete"; interim MRTR results use "input_required". For backwards compatibility, clients must treat a result from an earlier-protocol server that omits the field as "complete".
The map shape also permits multiple independent inputs in one interim result. A server can request a user confirmation and a model completion together and receive both answers in matching inputResponses. Under the previous design, these would have required separate server-initiated JSON-RPC request/response pairs, although they were not necessarily required to run sequentially.
And one non-obvious use: the spec permits an InputRequiredResult carrying requestState and no inputRequests at all. That's a load-shedding primitive: the server suspends the work, hands the resumption state to the client, and lets the client bring it back on a retry of its own choosing.
Applying the specification's requirements to requestState
This is where MRTR gets operationally interesting. The final specification defines the core security properties: clients must treat requestState as opaque, servers must treat it as attacker-controlled, and integrity protection is mandatory when state influences authorization, resource access, or business logic. Implementers still need to choose an encoding, key-management strategy, replay policy, size limit, and state layout.
The useful framing is that requestState is a continuation token. You are serializing enough information to resume the middle of a logical operation, handing that token to an untrusted party, and asking for it back.
Choice one: value or reference
You can put the actual state in the blob:
{ "step": 1, "files": ["a", "b", "c"], "op": "delete" }
Or you can put a pointer to state you stored server-side:
{ "ref": "pending:7f3a9c21" }
A reference trades payload portability for a server-side dependency. In a horizontally scaled deployment, every instance that may receive the retry must be able to resolve the reference, or routing must direct the retry to an appropriate shard. That is not inherently wrong (MCP removed the protocol-level session, not legitimate application storage), but it gives up some of MRTR's stateless-processing benefit.
For small, ephemeral workflows, value-carrying state is often the simplest default. Use a reference or an explicit persistent-workflow mechanism when the state is too large, too sensitive to expose outside the trust boundary, must continue changing while the client is away, or already belongs in durable application storage.
Choice two: what actually needs to survive
Be ruthless. Anything safely recomputable from the original call's arguments usually does not belong in requestState, because the client re-sends those arguments. Preserve only the delta: the state-format version, resume point, intermediate values that cannot be safely recomputed, and prior inputs that later rounds still need.
Every byte in requestState is retained by the client and travels over the wire again. Although conforming clients must not inspect it, implementations may still retain opaque state in tool-call records, traces, logs, or generic conversation state. Do not assume that "opaque to the client" means "invisible everywhere."
Choice three: size
requestState travels through the client, so large values increase request size, storage, logging, and latency costs. A well-designed client should not expose the opaque value to a model, but generic application plumbing may still copy it into conversation or tool-call records. If that risk matters, test the clients you support instead of assuming their behavior.
Keep it compact and enforce explicit encoded and decoded size limits. Consider compression only for sufficiently large, non-secret structured state, with bounded decompression and an assessment of compression side channels. If the state regularly becomes large, a server-side handle or persistent workflow may be the better trade-off; the protocol does not define a universal "few kilobytes" threshold.
The security properties to design explicitly
The failure mode is very important because continuation state can influence what the server does after the client returns.
The server must treat requestState as attacker-controlled input. The client is required to echo it exactly, but a malicious or compromised client can modify, replay, substitute, or fabricate a value. Unless the server authenticates and validates consequential state, the protocol cannot enforce that the returned value is the one it issued.
A plain base64-encoded JSON object provides neither integrity nor confidentiality. If such a value controls the resume step, target resources, or authorization decision, the client can edit private execution state unless the server independently validates every consequential field. Base64 is only an encoding.
Consider: The state says {"step": 1, "files": ["a","b","c"], "op": "delete"}. The client returns {"step": 2, "files": ["/etc/passwd"], "op": "delete"}, skipping the confirmation step entirely and substituting its own target. Your server resumes at step 2, believing consent was already given.
So:
Protect its integrity. When state influences authorization, resource access, or business logic, the specification requires integrity protection, such as an HMAC-authenticated envelope or AEAD, and requires rejection when verification fails. Parse only the minimal envelope needed to select a key and verify it; do not trust or instantiate the contained application state before authentication succeeds. Integrity protection may be omitted only when tampering can cause nothing worse than request failure.
Encrypt sensitive contents. Integrity protection stops undetected tampering; it does not necessarily stop reading. Use authenticated encryption when the state contains information the client, logs, or other intermediaries should not see. Base64 is not obfuscation.
Bind it to identity and to the originating request. Include the authenticated principal and verify it on resume. Also bind the state to the MCP method, tool or resource name, and a digest of security-relevant original parameters. Otherwise, leaked state may be reusable by another principal or against materially different arguments.
Expire it. Embed and enforce a short issued-at and expiry window inside the integrity-protected payload. Without expiry, a valid requestState may remain a resumption capability long after the assumptions, data, or consent behind it have changed.
Decide your replay stance. Principal, request, and expiry binding constrain replay but do not guarantee single use. For an idempotent read, repeated submission may be harmless. For a one-time redemption or destructive operation, include a nonce or redemption identifier and enforce at-most-once use server-side. That requires some durable state, but only for operations that need that invariant.
Never deserialize into a live object graph. Language-native object deserialization on attacker-controlled bytes can turn a confirmation flow into remote code execution. Parse into inert data, authenticate the envelope, validate the claims against a strict schema and resource limits, and only then construct application state.
requestState is an untrusted continuation token. If possessing it is sufficient to resume a privileged operation, it also functions as a bearer capability and deserves token-grade integrity, confidentiality where needed, principal and request binding, expiry, and an explicit replay policy. It need not be a JWT, and standard authenticated-envelope formats are preferable to inventing ad hoc cryptography.
Operational behavior worth understanding
Round trips are visible and countable. Under Streamable HTTP, each interim result completes one HTTP request and each retry is a new POST, so a two-round-trip logical operation normally appears as two requests in access logs. Add a safe correlation identifier or distributed trace so operators can group them without logging requestState. Round-trip count is a useful metric; a tool that regularly needs several rounds may have a UX or API-design problem.
User think time moves outside the server's in-flight request. This often improves server-side request-duration distributions and reduces held-open resources, but it does not guarantee a better p99 for the end-to-end logical operation. Total user-visible latency may be unchanged or slightly higher because of retries and repeated metadata. Report server-request latency and logical-operation latency separately.
Bandwidth goes up slightly. The original arguments are re-sent on every retry. For most tools this is noise; for one taking a large payload, it is not, and that is an argument for accepting a handle rather than an inline blob.
Loop prevention is your job. Nothing in the protocol stops a server from returning input_required forever. Track round-trip depth in your state and fail closed past a sane bound.
Client and SDK ergonomics differ. Even when they implement the same wire protocol, some may offer helpers that drive retries, while others expose the interim result to application code. Verify the exact SDK versions and clients you support. If a destructive operation requires elicitation, a client that cannot complete the MRTR flow should receive a clean refusal rather than a hang or an unsafe fallback.
Migrating from server-initiated requests
If you have elicitation working today, the mechanical shape of the change is:
- Find every server-to-client request. In MCP 7-28, elicitation/create, sampling/createMessage, and roots/list are delivered through MRTR rather than as separate server-initiated JSON-RPC requests. Roots and Sampling are also deprecated in this revision: existing implementations may migrate them during the deprecation window, but new implementations should follow the documented replacement patterns instead of adopting them.
- Turn your handler inside out. The old code blocked mid-function waiting for a response. The new code returns, and is re-entered from the top on the retry. Any handler that reads like a linear script with an await in the middle has to become explicitly resumable; that is the real work, and it is more invasive than the field names suggest.
- Identify the resume point. Whatever the await was waiting on becomes a step marker in requestState.
- Move correlation into protected state. The elicitationId field and notifications/elicitation/complete notification from the previous revision are gone. If you need correlation across retries, encode your own identifier inside integrity-protected requestState.
- Set resultType on every result. It is required now, on complete results too.
- Protect, bind, version, and expire consequential state before you ship. Not after.
- Test failover and rolling upgrades. Kill the instance that issued the interim result and confirm that another compatible instance can resume it. Version the state envelope and retain appropriate decryption or verification keys for the allowed retry window. If the state uses a reference, verify that every eligible instance can resolve it; that is a deliberate application-state dependency, not a protocol session.
In the long run
MRTR is presented as a way to preserve server-to-client interaction in a stateless protocol, and elicitation is its clearest use case. More generally, it is a continuation-style request/response pattern: one request terminates, the server externalizes resumption information, and a new request continues the logical operation.
It belongs to a broader family of web patterns that pass correlation or continuation material through an untrusted intermediary: OAuth uses a state value to bind an authorization response to an initiating flow, resumable protocols use opaque handles, and durable execution systems checkpoint progress. The security semantics are not identical, but the shared lesson is that explicit state needs integrity, scope, expiry, and careful replay handling.
The practical consequence is that interactivity and horizontal scale no longer have to be in direct tension. Before this release, server-to-client questions over remote MCP commonly required sticky routing, shared coordination state, or a long-lived instance. MRTR makes confirm-before-you-act workflows practical for deployments that scale horizontally or to zero, while still allowing explicit durable state when the application genuinely needs it.
References
- MCP 2026-07-28, Multi Round-Trip Requests: https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr
- MCP 2026-07-28 schema (InputRequiredResult, InputRequests, and ResultType): https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2026-07-28/schema.ts
- SEP-2322, Multi Round-Trip Requests: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2322-MRTR.md
- MCP 2026-07-28 changelog: https://modelcontextprotocol.io/specification/2026-07-28/changelog
- MCP 2026-07-28 deprecated features registry: https://modelcontextprotocol.io/specification/2026-07-28/deprecated
- MCP 2026-07-28 release announcement: https://blog.modelcontextprotocol.io/posts/2026-07-28/
Share
Author




