The short version: The 2026-07-28 MCP spec removed the session and the always-open bidirectional stream — but servers still need to reach back to your client mid-tool-call to run a model completion (sampling), ask your user for a value (elicitation), or list your roots. The mechanism that replaces the stream is Multi Round-Trip Requests (MRTR), and the mental model is a resume loop: a tools/call can return an InputRequiredResult instead of finishing, you fulfill the server's requests locally, then you re-issue the same call with your answers and an opaque requestState token echoed back byte-for-byte. This is the server-callback companion to the stateless migration — the client migration and server checklist cover the headers and handshake; this one covers the callbacks that used to need a socket.
If you want the background on why the session died, we walked through the stateless core and what it breaks as they landed. Below is how the callbacks work now.
1. Why the old way is gone#
Before 2026-07-28, if a server handling your tool call needed the model to summarize a diff, or needed the user to confirm a destructive action, it just sent a sampling/createMessage or elicitation/create request back down the open bidirectional stream. That stream was the problem: it required a connection that stayed alive for the whole call, which in practice meant a sticky session pinned to one server instance. Kill the session — which the new spec does — and there is nowhere for that server-initiated request to go.
MRTR is the fix. Instead of talking over a held-open line, the server pauses the call and hands the conversation back to the client as data. Nothing stays open between round trips, so the entire exchange survives on plain stateless HTTP behind a round-robin load balancer.
2. The shape: a call that returns "I need input" instead of finishing#
Under MRTR, a tools/call, prompts/get, or resources/read has two possible outcomes: it either returns its normal final result, or it returns an InputRequiredResult. That result carries two things:
inputRequests— the server-initiated requests you must fulfill (sampling/createMessage,elicitation/create,roots/list), each with its own id.requestState— an opaque token that encodes everything the server needs to resume the paused call. Treat it as the server's continuation; you hold it, you don't read it.
You fulfill each request locally, then re-issue the original call — same tool, same arguments — adding your inputResponses and echoing requestState back unmodified. Repeat until the call returns a real result.
# Client-side MRTR loop (illustrative — mirrors the 2026-07-28 pattern).
# handle_server_request() runs sampling against your model, prompts the user
# for elicitation, or returns your roots — exactly the work the stream used to carry.
def call_with_mrtr(client, tool, arguments):
resp = client.tools_call(tool, arguments)
while resp.type == "input_required": # InputRequiredResult
input_responses = []
for req in resp.inputRequests: # server-initiated requests
answer = handle_server_request(req) # sampling / elicitation / roots
input_responses.append({"id": req.id, "result": answer})
# Re-issue the SAME original call. Echo requestState byte-for-byte.
resp = client.tools_call(
tool, arguments,
inputResponses=input_responses,
requestState=resp.requestState, # opaque — do not touch
)
return resp # normal CallToolResult
The critical detail is that last line inside the loop: you are not answering on a side channel, you are calling the same tool again. The server recognizes the resumed call from requestState, applies your inputResponses, and continues where it paused.
3. The one rule that changes your client architecture#
In the old model, a client had to run an always-listening receive loop because a server could push a request at any moment the connection was open. Under 2026-07-28, servers may only send server-to-client requests while actively processing one of your client requests. There are no unsolicited pushes.
That means you can delete the background listener. Server callbacks now only ever appear inside the response to a call you just made, as an InputRequiredResult. Your handling code moves from "react to whatever arrives on the stream" to the tight, synchronous loop above — which is far easier to reason about, retry, and test.
4. What it means for a team of one#
Server-initiated behavior stopped being a scaling tax. Sampling and elicitation were the two features that quietly forced you back onto sticky sessions and stateful infrastructure even after the rest of MCP went stateless — because they needed the stream. MRTR closes that gap: a server that elicits a confirmation or asks the model to draft a summary now runs on the same cheap, horizontally-scalable HTTP tier as a plain tool call. For a bootstrapped team, that is the difference between "we can run MCP on a serverless function" and "we still need a session store."
Do this if you touch MCP callbacks: if your server uses sampling, elicitation, or roots, port each one from a stream push to an InputRequiredResult return; on the client, replace the receive loop with the MRTR resume loop and make sure you echo requestState unmodified. It is the last piece of the stateless migration that actually changes your control flow — the rest is mostly headers.



