A tool is about to delete three files. It only needs one final answer from the user: approve or cancel?
Another tool has started a deployment that may take twelve minutes. The client may disconnect before it finishes and still needs to recover the progress later.
At the same time, a gateway wants to know which tool a request is calling so it can choose a backend and apply the right rate limit. The client would rather not fetch tools/list on every turn. The tracing system would like one trace to follow the operation from the host through the MCP client, gateway, MCP server, and downstream API.
Putting all of those responsibilities back into one long-lived session would merely recreate the operating cost that the stateless redesign removed.
MCP 2026-07-28 takes a different route. Responsibilities that used to be easy to attach to a session now have separate mechanisms and separate lifetimes.[1]
Keep five questions in mind before the protocol names:
- MRTR: the server needs one more answer.
- Tasks: the work itself still has to keep running, even across reconnects.
- Headers: the gateway needs to understand the operation without parsing the whole JSON body first.
- Caching: the client needs to know whether a result is still fresh and who may reuse it.
- Trace Context: several services need to recognise that they are handling the same operation.
Once those five jobs are clear, requestState, taskId, ttlMs, and _meta.traceparent are implementation details with a place to live.
The useful question is which mechanism owns which problem, and what breaks when one mechanism is stretched into another one’s job.
Version scope: this article targets MCP
2026-07-28and the current specification and extension documentation available on 18 August 2026. Compatibility with earlier protocol eras, authorisation hardening, and feature deprecations are reserved for Part 03. Tasks is an extension and can evolve independently, so implementation work should always re-check the current extension documentation.
Separate the responsibilities a session used to collect
Part 01 followed state after the protocol session disappeared. Part 02 goes one layer lower: once a persistent session no longer joins every interaction together, how are the interactions themselves continued?
A useful map is:
| Problem | Main 2026-07-28 mechanism | State / lifetime |
|---|---|---|
| A tool needs confirmation or another parameter mid-call | MRTR | Short-lived continuation carried through requestState when needed |
| Work may run for minutes or hours and survive reconnects | Tasks Extension | Durable task state plus taskId |
| A gateway needs to see the RPC method or tool / URI | HTTP request metadata | Repeated on each request |
| A client should avoid re-fetching stable catalogues or resources | ttlMs + cacheScope | Client-managed freshness and cache scope |
| One tool call should correlate across client, server, and downstream services | _meta.traceparent / tracestate / baggage | Distributed trace context |
The first two are the easiest to confuse: MRTR and Tasks.

Figure 2-1 | MRTR ends the current request and lets the client retry with the missing input. Tasks turn the work itself into a durable object that can be polled and resumed. Both can surface input_required, but their lifecycles are different.
MRTR: ask for an answer, then end this request
Consider a tool such as:
delete_files(["a.txt", "b.txt", "c.txt"])
The server has checked the files, permissions, and impact. It only needs a final user confirmation.
In the older bidirectional model, the server could send an elicitation request on the existing SSE stream and wait for the client to answer. That couples both sides of the interaction to the same continuing stream and session lifecycle.[3]
Multi Round-Trip Requests change the shape of the interaction. The server finishes the current request first.
It can return an incomplete result with resultType: "input_required", containing inputRequests and, when required, requestState.[1][3]
For example:
{
"resultType": "input_required",
"inputRequests": {
"confirm_delete": {
"method": "elicitation/create",
"params": {
"message": "Delete these three files?"
}
}
},
"requestState": "opaque-server-state"
}
The client fulfils the input through the user or another registered handler. It then issues the original tools/call again with the collected inputResponses and the exact echoed requestState:
{
"inputResponses": {
"confirm_delete": {
"action": "accept"
}
},
"requestState": "opaque-server-state"
}
The old HTTP request has already ended; the retry is a new call.
The retry has a new JSON-RPC ID and may be routed to another server instance. The receiving instance should be able to continue from the information in the new request rather than consulting hidden session memory from the first instance.[2][3]
In the current protocol model, this InputRequiredResult pattern is used for operations such as tools/call, prompts/get, and resources/read where execution may need client input before it can complete.[2][3]
Keep requestState narrow or it becomes a session by another name
requestState immediately invites an attractive shortcut:
If the client can carry state back to me, why not put the whole session in that field?
The server is free to choose the encoding. SEP-2322 discusses formats ranging from plain JSON and Base64 to signed or encrypted structures.[3]
The intended scope is narrower: continuation state for the same logical multi-round request.
The client has no interpretation authority over requestState. It must treat the value as opaque and echo the exact value on the retry. It must not parse, edit, or infer semantics from it.[3]
The server has the opposite responsibility. A value that comes back through the client is untrusted input and must be validated. If the state contains data specific to the original user, SEP-2322 requires the server to cryptographically bind that data to the original user and verify that the returned state belongs to the currently authenticated user.[3]
Suppose the state represents:
files = [a.txt, b.txt, c.txt]
approved_scope = project-123
A different signed-in user must not gain the ability to continue that operation merely by obtaining and replaying the state value.
MRTR is an ephemeral continuation mechanism
MRTR fits cases such as:
- deletion confirmation
- one missing argument
- an elicitation that must be answered before the call can complete
- a short workflow that needs two or three rounds of input
- load shedding where the minimum continuation state can travel with the next request
If a server has already been working for twenty minutes, holds substantial durable progress, and should continue while the client is offline, requestState becomes an awkward place to keep that workload alive.
That is where the lifecycle changes from a round trip to a Task.
Tasks: let the work outlive the HTTP request
Consider a deploy_service tool that has to:
- build an image
- run tests
- upload an artefact
- wait for rollout
- wait for an approval gate
- verify the health check
That may run beyond an ordinary HTTP timeout. The client might close, change networks, or restart while the backend continues working.
Keeping the original request alive for the entire operation turns timeout behaviour into part of the application contract.
MCP Tasks moves this workload into the io.modelcontextprotocol/tasks extension. The client declares Tasks support in the capabilities attached to the request, while the server advertises the extension through server/discover. Once the client has opted in, the server decides whether a supported request should return its normal result or a CreateTaskResult.[5][6]
The current Tasks extension supports task-augmented tools/call. That boundary matters because older proposal material and the experimental 2025 Tasks wire shape do not describe the same contract.[5][6]
A created task has a durable taskId plus operational metadata such as status, ttlMs, and pollIntervalMs. The server must create the task durably before returning the task result.[5][6]
The client can then leave the original tool request behind:
tools/call
↓
CreateTaskResult(taskId)
↓
tasks/get
↓
working / input_required / completed / failed / cancelled
If the task needs input halfway through execution, tasks/get exposes the outstanding inputRequests, and the client answers them with tasks/update.[5][6]
That distinction is easy to miss:
MRTR retries the original method.
A Task continues the durable task lifecycle.
Both can expose input_required; they do not use the same continuation path.
Clients may also send tasks/cancel, but cancellation is cooperative. The server acknowledges the intent; it is not required to stop the backend operation at the exact moment the request arrives, and the task can still race to another terminal outcome.[5][6]
Status is available through tasks/get polling. Servers may also send notifications/tasks, with clients opting into those updates through subscriptions/listen.[5][6]
MRTR or Tasks?
The decision is easier when the two lifecycles are compared directly.
| Question | MRTR | Tasks |
|---|---|---|
| Primary job | Short multi-round interaction within one logical request | Durable asynchronous work |
| Server result | resultType: "input_required" | resultType: "task" |
| Client continuation | Retry the original request | tasks/get / tasks/update |
| Continuation handle | Optional requestState | taskId |
| State model | Can travel entirely with the request | Durable server-side task state |
| Recover after client reconnect | Poor fit for a long-lived job registry | One of the main reasons to use Tasks |
| Mid-flight input | inputRequests then retry with inputResponses | tasks/get then tasks/update |
| Current scope | Supported MRTR request types in the core protocol | Current extension task-augments tools/call |
The practical boundary is whether the work itself must keep existing.
If the server is waiting briefly for one missing answer and a fresh request can resume the logic, MRTR is a good fit.
If the work has become a durable entity with progress, recovery, and execution that survives the client connection, give it a taskId instead of stretching requestState into a job system.
HTTP headers: let the gateway see the operation without parsing JSON first
There is a separate production problem once the interaction lifecycle is sorted out: how does an intermediary know what an MCP request is doing?
JSON-RPC method and tool name live in the request body. A load balancer, WAF, rate limiter, or gateway that wants to:
- apply different policies to
tools/callandresources/read - route
execute_sqlto a specialised backend - rate-limit one tool independently
- route a region-specific operation to the relevant cluster
would otherwise have to inspect the JSON body first.
The 2026-07-28 Streamable HTTP transport mirrors selected request metadata into HTTP headers so intermediaries can reason about the request at the HTTP boundary.[2]
A tools/call can look like:
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: execute_sql
Mcp-Method mirrors the RPC method. Mcp-Name is required for the named or URI-based operations that currently include tools/call, resources/read, and prompts/get.[2]
A tool definition can also mark selected primitive arguments with x-mcp-header, causing the client to mirror them into Mcp-Param-* headers. A region parameter, for example, might become:
Mcp-Param-Region: us-west1
This gives routing and policy infrastructure a useful signal without forcing every intermediary to parse the full JSON-RPC message.[2]

Figure 2-2 | Headers, caching, and Trace Context solve different infrastructure problems. Intermediaries can inspect request metadata, clients can manage freshness, and trace context can carry correlation through the call path.
Mirrored headers are routing signals; the body still has to agree
Duplicating the same semantic value in a header and a body creates a security boundary immediately.
Imagine a gateway sees:
Mcp-Name: safe_tool
while the MCP server reads this from the JSON body:
{"name": "dangerous_tool"}
The gateway and server would then make policy and execution decisions from different inputs.
The current Streamable HTTP specification therefore requires a server that processes the message body to validate the mirrored headers against it. A missing required header, malformed value, or mismatch must be rejected with 400 Bad Request and the MCP HeaderMismatch JSON-RPC error -32020.[2]
That validation is what makes header-based routing usable as a trust boundary.
A useful model is:
Header → gives intermediaries a visible routing / policy signal
Body → carries the MCP message the server executes
Validation → proves the two views agree
Headers can support routing, metering, and policy. Their presence does not make authorisation correct by itself. Issuer validation, token audience, and client registration belong to Part 03.
Caching: ttlMs is a freshness hint, not a background timer
Tool catalogues are a common example of data that does not need to be fetched on every turn.
If a tools/list response will be stable for the next ten minutes, repeatedly fetching the same list adds network work and can disturb upstream prompt caching.
The current MCP caching specification attaches two hints to selected results with resultType: "complete":[4]
ttlMs
cacheScope
The current cacheable set includes:
server/discovertools/listprompts/listresources/listresources/templates/listresources/read
ttlMs tells a client how long it may consider the response fresh. The semantics are similar to HTTP Cache-Control: max-age, but the field is a freshness hint rather than an instruction to poll in the background. Once the TTL has expired, the client evaluates freshness the next time it needs the data and re-fetches if appropriate.[4]
cacheScope answers a different question: who may reuse the cached result?
public: the response contains no caller-specific data and may be stored by shared caches.private: the response may be reused only within the same authorisation context and must not leak across different access contexts.[4]
Two exclusions are particularly useful when reasoning about MRTR.
An input_required interim result is not cacheable.
A retry that carries inputResponses or requestState must not be served from the ordinary cache path either, because those additional inputs are not represented safely by a normal method-and-parameters cache key.[4]
public cache scope still depends on the cache implementation
MCP defines protocol-level cache semantics.
Whether a CDN, reverse proxy, client SDK, or custom cache actually stores the response still depends on that layer’s HTTP method support, cache policy, cache key, and security configuration.
Treating ttlMs as an automatic CDN switch would confuse the protocol contract with one infrastructure implementation.
Trace Context: correlation can travel, but instrumentation still has to exist
A stateless request can cross several services:
Host
↓
MCP Client
↓
Gateway
↓
MCP Server
↓
External API / Database / another service
If every layer creates an unrelated trace, an operator still cannot follow one tool call to the place where it became slow or failed.
SEP-414 documents three keys in MCP _meta for OpenTelemetry and W3C trace-context propagation:[7]
traceparent
tracestate
baggage
When trace context is propagated through _meta, those values follow W3C Trace Context and W3C Baggage formats.[7]
Those three keys are also a deliberate exception to the usual DNS-prefix convention for _meta. Keeping the existing W3C/OpenTelemetry names preserves interoperability and avoids variants such as io.modelcontextprotocol.traceparent that would break trace and log correlation.[7]
The wording matters. This is a propagation convention. It does not guarantee that every MCP implementation generates a trace.
The application still needs instrumentation, span boundaries, propagation into downstream calls, and an exporter or backend that stores the trace.
There is also a data boundary. The SEP notes that trace context may contain correlation identifiers and should follow the data-handling guidance appropriate to the deployment.[7]
Put the five mechanisms back into one tool call
An illustrative provision_environment tool makes the split easier to see.
The client calls:
provision_environment(region="sg", size="medium")
At the HTTP boundary:
Mcp-MethodandMcp-Namelet the gateway see that this is atools/calland which tool is involved.- If the tool schema mirrors
regionusingx-mcp-header, the gateway can also receive the correspondingMcp-Param-*metadata. - The MCP server still validates the mirrored values against the body before execution.[2]
- If
_meta.traceparentis present, the server can propagate that context into the downstream cloud API or worker so the call path remains correlated.[7]
The server then decides that provisioning will not finish within the request lifetime. Because the client declared Tasks support, the server returns a taskId.[5][6]
The client uses tasks/get for progress. If production deployment later requires human approval, the task enters input_required, and the client supplies the answer through tasks/update.[5][6]
Meanwhile, tools/list and server/discover can use their own ttlMs and cacheScope rules. Polling the task does not require re-fetching the whole tool catalogue every time.[4]
Seen as one tool call, each responsibility has its own identifier, scope, and lifetime.
The mapping is:
Short multi-round continuation → requestState
Durable background work → taskId
Gateway routing signal → HTTP headers
Result freshness → ttlMs / cacheScope
Distributed correlation → trace context
The vocabulary is longer. The engineering questions become clearer: how long should this state live, and who is allowed to trust it?
These choices look reasonable until they meet production
Use MRTR to keep a thirty-minute job alive
If the work needs durable progress, reconnect recovery, and continued backend execution, requestState starts turning into an improvised job store.
That workload fits the Tasks lifecycle more naturally.[3][5]
Retry the original tools/call when a Task is input_required
Current Tasks exposes outstanding requests through tasks/get and accepts their responses through tasks/update. That is a different path from MRTR’s retry of the original request.[5][6]
Trust the header at the gateway and never validate the body at the server
That produces a routing or policy split-brain. The current specification explicitly requires servers processing the body to validate mirrored values and reject mismatches.[2]
Treat ttlMs as a polling interval
TTL answers whether a cached result may still be considered fresh. It does not require a background request when the clock expires.[4]
Add traceparent and declare observability finished
Trace Context handles propagation. Without instrumentation, span design, export, and a data policy, the system merely moves correlation identifiers around.[7]
What I would check before implementation
For a remote MCP server moving towards 2026-07-28, I would check these boundaries first:
- Interaction lifetime: is the operation waiting for one answer, or must the work itself be durable? Start with MRTR for the former and Tasks for the latter.
requestStatetrust: is every returnedrequestStatetreated as untrusted input? If it contains user-specific state, is it bound to the authenticated identity?[3]- Tasks opt-in: did both sides actually declare Tasks support? A server must not surprise a client that did not opt into the extension with a
CreateTaskResult.[5][6] - Header/body agreement: if policy depends on
Mcp-Method,Mcp-Name, orMcp-Param-*, where is header/body consistency validated?[2] - Cache boundary: does the cache key cover the method, result-affecting parameters, and authorisation context? Private results must not cross contexts.[4]
- Freshness: is
ttlMsused as a freshness check rather than a fixed polling timer?[4] - Trace pipeline: does Trace Context feed a real OpenTelemetry pipeline, or are the three keys merely being copied through
_meta?[7]
If those questions are unclear, a system can appear to support the new revision while preserving the old session problems under several new field names.
Put each lifetime back in its own place
Removing the long-lived protocol session did not remove interaction or force long-running work back into synchronous HTTP.
MRTR carries short continuation between requests. Tasks give durable work its own lifecycle. HTTP metadata makes an operation visible to intermediaries. Cache hints state how long selected results may be reused and within which scope. Trace Context carries correlation across the service path.
Together, those mechanisms are what make stateless MCP useful for real agent workflows that still need approvals, waiting, background jobs, gateway policy, and observability.
Part 03 moves from mechanism to migration:
How do you upgrade a
2025-11-25deployment that depends oninitialize,Mcp-Session-Id, server-initiated requests, legacy transports, or experimental Tasks without cutting production over in one step?
The next article will cover protocol-era detection, dual support, authorisation hardening, DCR and CIMD, and the correct deprecation paths for Roots, Sampling, Logging, and legacy HTTP+SSE.
References
- Model Context Protocol, The 2026-07-28 Specification, 28 July 2026. https://blog.modelcontextprotocol.io/posts/2026-07-28/
- Model Context Protocol, Streamable HTTP: 2026-07-28 Specification. https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http
- Model Context Protocol, SEP-2322: Multi Round-Trip Requests. https://modelcontextprotocol.io/seps/2322-MRTR
- Model Context Protocol, Caching: 2026-07-28 Specification. https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching
- Model Context Protocol, MCP Tasks: Current Extension Documentation. https://modelcontextprotocol.io/extensions/tasks/overview
- Model Context Protocol, Tasks Extension Specification. https://tasks.extensions.modelcontextprotocol.io/specification/draft/tasks
- Model Context Protocol, SEP-414: Document OpenTelemetry Trace Context Propagation Conventions. https://modelcontextprotocol.io/seps/414-request-meta