MCP Model Input: Sampling Migration and Stateless MRTR
MCP 2026-07-28 deprecates Sampling for new designs and removes the server-to-client request channel. If an existing workflow still needs the client's model, the server returns an input_required result and the client retries the original request with the model output. The reasoning loop becomes explicit, bounded, and stateless at the protocol layer.
Lesson 11 of 45Build~75 minutesPython
Before this: Lesson 7 of Phase 13 (MCP server), lesson 10 of Phase 13 (resources and prompts)
On this page
Learning Objectives
- Explain why Sampling is deprecated in MCP 2026-07-28 and choose the direct model integration default for new servers.
- Implement a compatibility workflow that carries
sampling/createMessagethrough Multi Round-Trip Requests (MRTR). - Put the protocol revision and client capabilities in every request
_metaobject. - Return
resultType: "input_required"and retry the original method with a fresh JSON-RPC id. - Integrity-protect
requestStateand bind it to the principal, method, arguments, and expiry. - Bound model-assisted loops with capability checks, approval, response validation, and a round limit.
The Decision Before the Protocol
A tool such as summarize_repo needs two kinds of work:
- Deterministic work: list files, read allowed files, validate paths, and assemble content.
- Model work: choose representative files and synthesize the summary.
You now have two valid architectures.
New server: integrate with a model provider directly
This is the current default. The server owns model selection, credentials, budgets, retries, and observability. It returns one ordinary tools/call result to the MCP client.
Choose this when the server is already a hosted service or when predictable model behavior matters more than using the host's model.
Existing Sampling workflow: migrate it to MRTR
Sampling still exists during its deprecation window. A server targeting 2026-07-28 cannot send a live sampling/createMessage request back to the client. It instead embeds that request in an InputRequiredResult.
Choose this compatibility path only when using the client's model and credentials is a real product requirement. Record a removal plan because new implementations should not adopt deprecated Sampling.
The Stateless Contract
The July 2026 protocol has no initialize exchange, no notifications/initialized, and no Mcp-Session-Id. Every request carries the information that used to live in the handshake:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "summarize_repo",
"arguments": {"audience": "developer"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {"sampling": {}},
"io.modelcontextprotocol/clientInfo": {
"name": "lesson-client",
"version": "1.0.0"
}
}
}
}
The server validates the revision on every request. A missing or non-string version is invalid params, -32602. An unsupported string returns -32022 with exact data {"supported":["2026-07-28"],"requested":"<client version>"}. A missing Sampling capability returns -32021 with data.requiredCapabilities set to {"sampling":{}}.
An envelope without a JSON-RPC id is a notification. The receiver may process it, but it emits neither a success response nor an error response. A Streamable HTTP adapter returns 202 Accepted with no body for an accepted notification.
The server also implements server/discover with the exact supportedVersions key, capabilities, ttlMs, and cacheScope so a client can learn and cache the server contract before calling a tool. Because discovery advertises tools, the server also implements mandatory tools/list. Its deterministic summarize_repo descriptor includes a valid object inputSchema, resultType: "complete", server identity metadata, and public cache hints.
Every successful modern result has a discriminator:
resultType: "complete"means the operation finished.resultType: "input_required"means the client must fulfill embedded requests and retry.- Extensions may define additional result types. The Tasks extension adds
"task"in Lesson 13.
One MRTR Round
The server cannot call the client while handling the request. It returns this result instead:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "input_required",
"inputRequests": {
"pick_files": {
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Choose three representative files and return a JSON array."
}
}
],
"systemPrompt": "Return only the requested value.",
"modelPreferences": {
"costPriority": 0.8,
"intelligencePriority": 0.2
},
"maxTokens": 400
}
}
},
"requestState": "opaque-integrity-protected-value"
}
}
The client verifies that it supports Sampling, applies its approval and model policies, and obtains a model response. Then it sends a new request with a different JSON-RPC id:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "summarize_repo",
"arguments": {"audience": "developer"},
"inputResponses": {
"pick_files": {
"role": "assistant",
"content": {
"type": "text",
"text": "[\"README.md\", \"server.py\", \"docs/intro.md\"]"
},
"model": "host-model",
"stopReason": "endTurn"
}
},
"requestState": "opaque-integrity-protected-value",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {"sampling": {}}
}
}
}
The retry is not a continuation of a protocol session. It is a new request that repeats the original method and arguments, adds only the current round's inputResponses, and echoes requestState byte for byte.
MRTR is allowed only on tools/call, prompts/get, and resources/read. A server must not return input_required from unrelated methods.
Multi-Round State
This lesson needs two model calls:
pick_filesreturns a JSON array.summaryreturns the final prose.
Each retry carries only the responses for that round. The server therefore puts the phase and validated intermediate data into the next requestState.
Treat that value as attacker-controlled. Signing a raw phase name is not enough. Bind the state to:
- the authenticated principal, not self-reported
clientInfo; - the originating method;
- a digest of the original arguments;
- a short expiry;
- the current phase and validated intermediate values.
Use HMAC when confidentiality is not required. Use authenticated encryption when the client must not read the state. Reject a bad signature, expired value, changed principal, or changed arguments with -32602.
The client must not parse or modify requestState. Its only job is to echo the exact string on the retry.
Model Preferences Are Hints
costPriority, speedPriority, and intelligencePriority are independent preferences. They are not a probability distribution and do not need to sum to one. The client may ignore them because the client owns model policy.
Keep includeContext at "none" if you maintain a legacy Sampling flow. Other context modes increase leakage risk and are themselves deprecated. Pass the minimum explicit context in the request.
Safety Invariants
The client is the trust boundary for embedded Sampling requests.
- Show the user what the server is asking the model to do when policy requires approval.
- Cap MRTR rounds. A malicious server can otherwise create a model-spend loop.
- Validate every sampling response before using it as a filename, URL, or tool input.
- Limit bytes and tokens per round.
- Refuse an input request that was not declared in current client capabilities.
- Keep model output out of authorization decisions.
- Log the originating method and input-request key without logging sensitive prompt content.
clientInfo and serverInfo are display and diagnostics metadata. Never use either as an authenticated identity.
t3-sampling-flip
Build It
code/main.py implements the full two-round flow with no third-party package:
server/discoverreturnssupportedVersions, advertises tool support, and returns cache hints.tools/listreturns a deterministic, cacheablesummarize_repodescriptor with an object input schema.tools/callvalidates per-request metadata.- The first result embeds
sampling/createMessagefor file selection. - The first retry validates the model result and embeds a second request.
- HMAC-protected
requestStatecarries the phase between independent requests. - The final result uses
resultType: "complete".
The fake host model makes the example deterministic. Replace only fake_host_model when connecting a real host. The server-side state machine should stay deterministic and testable.
Use It
From the repository root:
cd phases/13-tools-and-protocols/11-mcp-sampling/code
python3 main.py
python3 -m unittest discover tests -v
Expected checkpoints:
- Discovery returns a complete result with
ttlMsandcacheScope. - Tool discovery returns the same sorted descriptor with
resultType, server identity, and cache hints. - Missing capabilities and unsupported versions use exact
-32021and-32022error data. - An id-less notification produces no JSON-RPC response.
- Request ids are
[1, 2, 3], proving each MRTR round is independent. - The first two results are
input_required. - The final result is
completeand contains the selected files plus summary. - Changing the original arguments on a retry fails the request-state check.
Ship It
outputs/skill-sampling-loop-designer.md is now a migration planner. It first decides whether Sampling should be removed in favor of direct model integration. If compatibility is required, it produces the MRTR rounds, state binding, capability gate, budget, validation, and removal plan.
Exercises
- Change the file-selection response to invalid JSON. Confirm the server returns
-32602instead of trusting model output. - Change
audiencebetween the first call and retry. Explain why the sealed state blocks cross-request reuse. - Add a third round that asks the host to critique the summary. Carry the earlier summary inside signed state and cap the entire flow at three rounds.
- Remove Sampling by replacing the fake host callback with a server-owned model adapter. List which approval, billing, and observability responsibilities move to the server.
- Add an expiry test using a state value that is one second past its deadline.
Key Terms
| Term | Meaning in 2026-07-28 |
|---|---|
| Sampling | Deprecated feature that asks the client's model for a completion |
| MRTR | Stateless retry pattern for client input required during a request |
InputRequiredResult |
Result with resultType: "input_required" |
inputRequests |
Server-assigned map of embedded elicitation, sampling, or roots requests |
inputResponses |
Current round's client results keyed like inputRequests |
requestState |
Opaque server state echoed exactly by the client and verified by the server |
resultType |
Required discriminator for modern MCP results |
| Direct model integration | Recommended replacement for new servers that need model inference |
| Capability gate | Rule that prevents sending an embedded request the client did not advertise |
| Loop budget | Maximum rounds, tokens, bytes, time, and spend allowed for the operation |
Legacy Compatibility
A client pinned to 2025-11-25 may still use the older server-initiated sampling/createMessage flow over a live connection. Keep that behavior in a version-specific adapter only. Do not make the sessionful path the architecture for a 2026-07-28 server.
Official SDKs can translate modern input_required handlers for older peers. That shim is a compatibility boundary, not permission to add new session-dependent logic.
Further Reading
Check yourself
Quick quiz
A new MCP server needs model inference. What is the default architecture under 2026-07-28?
- Integrate directly with a model provider
- Send sampling/createMessage before every tool call
- Store the client's model key in a protocol session
- Open a permanent SSE stream for Sampling
Show the answer
A. Integrate directly with a model provider Sampling is deprecated for new designs. Direct provider integration is the recommended default when a server needs model inference.
How does a 2026-07-28 server request compatibility Sampling during tools/call?
- It writes the prompt into server/discover
- It places the prompt in an Mcp-Session-Id header
- It returns resultType input_required with sampling/createMessage in inputRequests
- It sends a reverse JSON-RPC request on the same connection
Show the answer
C. It returns resultType input_required with sampling/createMessage in inputRequests MRTR carries server-to-client requests inside an input_required result, avoiding a reverse request channel. The advertised summarize_repo tool must first be available through deterministic tools/list with a valid inputSchema.
Which property proves an MRTR retry is an independent JSON-RPC request?
- It omits per-request metadata
- It uses the same id as the original request
- It uses a fresh id while repeating the original method and arguments
- It removes the original tool arguments
Show the answer
C. It uses a fresh id while repeating the original method and arguments The client repeats the original operation with a new JSON-RPC id, current inputResponses, and the echoed requestState.
What must protect requestState when it affects resource access?
- HMAC or authenticated encryption bound to principal, request, and expiry
- Base64 encoding alone
- A clientInfo name
- A long-lived transport session
Show the answer
A. HMAC or authenticated encryption bound to principal, request, and expiry requestState passes through an untrusted client. Integrity protection and binding prevent tampering and cross-request replay.
The client did not advertise Sampling in its per-request capabilities. What exact protocol response should the server use?
- Send the embedded sampling request anyway
- Return -32021 with data.requiredCapabilities, or choose a non-Sampling path
- Add the capability to the client's metadata
- Assume capability from a previous request
Show the answer
B. Return -32021 with data.requiredCapabilities, or choose a non-Sampling path A server must not emit an input request the client did not declare support for on the current request. The exact missing-capability code is -32021.
Which migration keeps a two-round compatibility loop stateless at the protocol layer?
- Carry only current-round inputResponses and signed phase state on each fresh retry
- Store phase state under Mcp-Session-Id
- Reuse the original request id until completion
- Accumulate all model responses in a hidden session
Show the answer
A. Carry only current-round inputResponses and signed phase state on each fresh retry Each retry is self-contained. Signed requestState carries validated intermediate state, and inputResponses contains only the current round.
Questions people ask
What is MCP sampling and is it still used?
Sampling was a way for an MCP server to ask the client's AI model to generate text. The current spec deprecates it for new designs. New servers are encouraged to call a model directly, while older workflows use a request and retry pattern instead.
How does an MCP server ask for more input without keeping a session?
It returns a result marked as needing input, along with a signed state token. The client gathers the input, then retries the original request with that token and the new data. Any server copy can check the token and continue.
Lessons from AI Engineering from Scratch by Rohit Ghumare and contributors, MIT license. Notes for owners by Lozo. License