Community-maintained FDE reference. Not an official Vercel or Anthropic project. About this project
Sampling-request handling
Deprecated in 2026-07-28 (SEP-2577). Sampling is deprecated as of spec revision 2026-07-28 under the feature lifecycle policy: it stays in the specification for at least twelve months (earliest removal is the first revision released on or after 2027-07-28), new implementations SHOULD NOT adopt it, and existing implementations SHOULD migrate to calling LLM provider APIs directly from the server. On Vercel that migration is concrete: call the model inside the tool handler with the AI SDK, and route providers, keys, and budgets through AI Gateway instead of borrowing the host’s model over the wire. This page stays for the deprecation window, because hosts will keep receiving sampling requests from deployed servers and must keep gating them correctly. See the deprecated features registry.
TL;DR: Sampling inverts the usual direction of MCP: a server asks the host’s model to generate text. As of 2026-07-28 that ask no longer travels as a server-initiated request. The server answers the triggering call (a tools/call, prompts/get, or resources/read) with an InputRequiredResult (resultType: "input_required") whose inputRequests map carries a sampling/createMessage request, and the client retries the original request with the completion in inputResponses. This is the Multi Round-Trip Requests (MRTR) pattern. Everything the host owned before, it still owns: it reviews the request, can edit or decline it, picks which model runs, decides what context to include, and returns only the completion. Sampling is a client capability, declared per request in _meta under io.modelcontextprotocol/clientCapabilities (the initialize handshake is gone in 2026-07-28); tool-enabled sampling additionally requires the sampling.tools sub-capability.
Plain-language explanation
Most of MCP flows host to server: the model picks a tool, the host calls it, the server answers. Sampling is the one common flow that runs the other way. A server that needs a language model in the loop (to summarize a document it fetched, classify a record, or decide a next step) does not bundle its own model or API key. Instead it asks the host: “run this prompt through your model and give me the result.” The host stays in control of model access, which is the whole point. A server gaining unmediated model access would be a server that can spend the user’s tokens, see the user’s model, and act without the user knowing.
What changed in 2026-07-28 is how the ask is delivered, and whether to keep asking at all. Delivery: instead of the server opening a request back at the client mid-call, it returns “I need input” as the result of the call, and the client comes back with a fresh retry carrying the answer. Existence: the deprecation above says the ask itself is on its way out; new servers should own their model calls directly (on Vercel, the AI SDK behind AI Gateway) rather than requesting inference through the client.
The flow (MRTR)
- The client sends the original request (say
tools/call). - The server, needing a completion, returns
resultType: "input_required"with asampling/createMessageentry ininputRequests, plus an opaquerequestStateblob encoding whatever context it needs to resume. - The host reviews the sampling request, runs (or edits, or refuses) the inference.
- The client retries the original request with a new JSON-RPC id, the completion keyed into
inputResponses, andrequestStateechoed back byte for byte. - The server reconstitutes its state from
requestStateand finishes the call (or returns anotherinput_requiredround).
Diagram source (Mermaid)
sequenceDiagram
participant Server
participant Client
participant Host
participant Model
Client->>Server: tools/call (id 1)
Note over Server: needs a completion
Server-->>Client: InputRequiredResult (sampling/createMessage in inputRequests, requestState)
Note over Client,Server: first invocation ends here
Client->>Host: surface the sampling request for review
Host->>Host: human in the loop (review, edit, approve or decline)
alt approved
Host->>Model: run inference (host picks the model, gates any tools)
Model-->>Host: completion (may elect a tool call)
Host-->>Client: reviewed result (model, role, content, stopReason)
Client->>Server: tools/call retry (id 2, completion in inputResponses, requestState echoed)
Server-->>Client: result (resultType complete)
else declined
Note over Client: do not retry, the server is not waiting
endTwo client obligations are load-bearing. The retry MUST use a different JSON-RPC id (it is an independent request, not a resend), and the client MUST echo requestState exactly without inspecting, parsing, or modifying it: to the client it is opaque, and to the server it is attacker-controlled input it integrity-protects (HMAC or AEAD) and will reject if tampered. Declining is simpler than it used to be: the client just does not retry. Under MRTR the server is not blocked waiting on a response, so there is no rejected-sampling error to fabricate (the legacy -1 “user rejected” convention belonged to the old wire shape below). If the client retries without the requested input, the server SHOULD respond with a fresh InputRequiredResult asking again rather than erroring.
Wire status: the pinned stack (mcp-handler 2.1.1 on @modelcontextprotocol/server 2.0.0) serves the 2026-07-28 contract natively over Streamable HTTP and falls back to stateless 2025-11-25 Streamable HTTP for legacy clients; the SDK Client defaults to that legacy handshake unless you opt in to modern version negotiation, so the examples’ in-memory test suites exercise only the legacy path.
The legacy wire shape (2025-11-25 and earlier)
Before 2026-07-28, sampling/createMessage was a server-initiated JSON-RPC request: it traveled client-ward over the open SSE response stream of the invocation that triggered it, the server blocked until the client answered, and a user decline came back as error -1 (“User rejected sampling request”). The 2026-07-28 revision removes server-initiated requests outright: servers MUST deliver sampling (and elicitation, and roots) through MRTR. You will still meet the legacy shape wherever the negotiated protocol version is 2025-11-25 or earlier, which today includes any SDK Client left at its default legacy negotiation and every in-memory test suite, so a production host needs to gate both shapes with the same discipline: the review points on this page do not depend on which envelope the request arrived in.
The legacy shape is also what created the serverless squeeze this page used to lead with: the entire round trip (request out, human review, model inference, completion back, and the rest of the tool handler) had to fit inside one Vercel Function invocation’s maxDuration budget, with the user’s think time burning the server’s clock. Keep that constraint in mind for as long as you serve legacy-protocol traffic; Serverless sessions covers the invocation-lifetime mechanics.
The serverless shape
MRTR is the wire shape serverless always wanted. The invocation that discovers it needs a completion ends immediately with input_required; the human review and the model run happen between invocations, on nobody’s maxDuration clock; and the retry is a fresh invocation that can land on any instance, because everything the server needs to resume rides in requestState rather than in function memory or a sticky session. The remaining time budget is just the retry’s own execution. A workflow that needs long or repeated sampling rounds is still a candidate for the async-jobs pattern, but the pressure that used to come from a user deliberating over an approval dialog is gone. The 2026-07-28 revision page covers how the same move plays out across the rest of the protocol.
What the server may ask for, and what the host controls
A sampling/createMessage entry in inputRequests carries the server’s preferences, not commands:
messages- the conversation the server wants completed (role plus text, image, or audio content). Treat this as untrusted content: it is server-supplied and may carry a prompt-injection payload aimed at the host’s model.modelPreferences- advisory hints only: model-name substrings pluscostPriority/speedPriority/intelligencePriorityvalues from 0 to 1. The host chooses the actual model; hints are substrings the client MAY map to equivalent models from a different provider, and the server cannot pin one.systemPrompt- a requested system prompt. The host may use, modify, or ignore it.includeContext- formally deprecated. The values"thisServer"and"allServers"are Deprecated under the feature lifecycle policy (SEP-2596) and will be removed no later than sampling itself. Servers SHOULD just omit the field (it defaults to"none") and SHOULD NOT send the deprecated values unless the client declared thesampling.contextsub-capability."none"is the right default:"allServers"asks to pull in context from other servers in the host, exactly the cross-server visibility the trust boundary forbids. The host decides what, if anything, to actually include.maxTokens, temperature, stop sequences -maxTokensthe client MUST respect as a ceiling; the rest are bounds the host may clamp, modify, or ignore.toolsandtoolChoice- a sampling request may carry server-supplied tool definitions plus atoolChoicedirective (auto,required, ornone), enabling tool calling within the sampling turn. This is gated twice. First by capability: clients MUST declaresampling.toolsto receive tool-enabled requests, and servers MUST NOT send them otherwise. Second by the host: the tool definitions are untrusted, server-supplied content (their names, descriptions, and schemas are the same injection surface asmessages), the model that would call them is the host’s model, and the host still owns execution and consent. When the model elects a tool call, the host decides whether that call runs at all, gates it through the same fail-closed consent discipline as any other tool call, and may decline or edit it. A forcingtoolChoice: { "mode": "required" }is likewise a request, not an obligation. Under MRTR the multi-turn tool loop becomes multipleinput_requiredrounds: the client returns the model’stool_useblocks ininputResponses, the server executes its tools and comes back with another sampling request carrying the results appended.
The result the host returns reports the model actually used, the role, the content, and a stopReason. The server learns what ran, but nothing about the host’s other context.
Human-in-the-loop
Sampling is a capability the user should approve, twice over: the spec says there SHOULD always be a human in the loop able to deny sampling requests, that the user should be able to review and edit the prompt before it runs, and that the completion should be reviewed before it returns to the server. Under MRTR the gate has a natural place to live: the pause between receiving input_required and issuing the retry belongs entirely to the host, so the fail-closed consent discipline is now literally a decision about whether to retry at all. An unattended client that auto-fulfills every sampling request and retries immediately hands servers a blank cheque on the user’s model. Default to surfacing the request; auto-fulfillment should be an explicit, scoped, revocable policy.
When a sampling request carries tools/toolChoice, the gate widens to a third point: any tool call the model makes mid-sampling is also subject to host review before its tool_use blocks go back in a retry. Surface the server-supplied tools alongside the prompt at approval time, and gate each elected call the same way you gate an ordinary tools/call. The in-sampling tool loop is a host control plane, not a server one.
Trust-boundary and cost concerns
- Server-supplied messages are untrusted. They are the injection vector; treat them as data, not instructions, with the same output-trust care you apply to tool results. See the output trust checklist.
- Keep the server out of the rest of the conversation. The server sees only its own request and the returned completion: never the host’s full transcript, system prompt, model identity beyond the reported
model, or other servers’ state. includeContextbeyond"none"is a boundary request, not a right. It is formally deprecated, capability-gated, and should stay off unless the user knowingly opted in.- Server-supplied
toolsare untrusted definitions, and the host still owns execution. Treat the definitions as part of the same injection-prone payload asmessages, apply consent per elected call, and ignore a forcingtoolChoiceyou have no reason to honor. requestStateis opaque both ways. The client MUST NOT inspect, parse, or modify it and MUST echo it only on the retry of the same request, never elsewhere. The server, for its part, must treat the echoed blob as attacker-controlled: integrity-protect it, bind it to the authenticated principal and the originating request, and give it a short TTL (the MRTR spec makes these server MUSTs).- Bound cost and recursion. A sampling result can drive further tool calls, which can drive further sampling: a loop spending real tokens, now visible as repeated
input_requiredrounds on the same logical call. The spec itself tells both parties to implement iteration limits for tool loops. Budget sampling per user, clampmaxTokens, cap retry rounds per originating request, and rate-limit requests per server. On Vercel the invocation’smaxDurationbounds only a single retry’s execution: it is a backstop, not a budget, and it kills the function, not the bill.
Common pitfalls
- Auto-fulfilling and retrying without review - gives every connected server unattended access to the user’s model and budget.
- Letting the server pick the model -
modelPreferencesare hints; treating them as a selection lets a server force an expensive or weak model. - Honoring the deprecated
includeContextvalues without thesampling.contextcapability and user awareness - leaks cross-server context the trust boundary exists to protect. - Trusting the server-supplied
messages- they can carry instructions aimed at the host’s model. - Treating server-supplied
tools/toolChoiceas authority - they are untrusted definitions and a request for tool use, not a grant; letting them bypass consent hands the server uncontrolled execution. - Inspecting or editing
requestState, or reusing the original JSON-RPC id on the retry - both break the MRTR contract; a tampered blob gets the retry rejected by the server’s integrity check. - Fabricating a decline response - under MRTR a decline is the absence of a retry; the server is not waiting, and error
-1belongs to the legacy wire shape. - No cost or loop ceiling - repeated
input_requiredsampling rounds can run away, and in-sampling tool calling makes the loop tighter. - Building new features on sampling - it is deprecated; new server-side inference belongs on direct provider APIs (on Vercel: the AI SDK with AI Gateway).
Example implementation
examples/sampling-server(in the repository) - a TypeScript server whose tool requests a completion from the client mid-call. Its vitest suite wires the server to an in-memory client overInMemoryTransport.createLinkedPair(), declares thesamplingcapability on the client, and answers with a canned completion, so the full request-to-completion round trip runs deterministically offline. The test plays the host that reviews and answers the sampling request, and asserts the server’s outbound messages. The example is kept through the deprecation window with a deprecation banner in its README naming the SEP-2577 migration (direct provider APIs; on Vercel, the AI SDK / AI Gateway). Wire status: the pinned stack (mcp-handler2.1.1 on@modelcontextprotocol/server2.0.0) serves the 2026-07-28 contract natively over Streamable HTTP and falls back to stateless 2025-11-25 Streamable HTTP for legacy clients; the SDKClientdefaults to that legacy handshake unless you opt in to modern version negotiation, so the examples’ in-memory test suites exercise only the legacy path.
The protocol-level definition sits in capability primitives; the gating discipline is Consent UX.
Related
- Consent UX - the human-in-the-loop gate, now living in the retry decision of the MRTR loop.
- trust-boundaries - why the server stays out of the host’s conversation and why cross-server context is gated.
- Multi-server composition - the host-mediated context
includeContextwould otherwise reach across. - capability primitives - the protocol-level definition of sampling alongside the other primitives.
- Serverless sessions - the invocation-lifetime mechanics behind the legacy squeeze and the MRTR relief.
- The 2026-07-28 revision - the full change set this page’s MRTR and deprecation story belongs to.
Bibliography
- Model Context Protocol Specification, Sampling, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/client/sampling
- Model Context Protocol Specification, Multi Round-Trip Requests, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr
- Model Context Protocol Specification, Deprecated Features, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/deprecated
- Model Context Protocol, Feature Lifecycle and Deprecation Policy - https://modelcontextprotocol.io/community/feature-lifecycle
- SEP-2577, Deprecate Roots, Sampling, and Logging - https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577
- Model Context Protocol, Security Best Practices - https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices
- Vercel, AI SDK Introduction - https://ai-sdk.dev/docs/introduction
- Vercel Documentation, AI Gateway - https://vercel.com/docs/ai-gateway
- Vercel Documentation, Configuring Maximum Duration for Vercel Functions - https://vercel.com/docs/functions/configuring-functions/duration
- OWASP Top 10 for Large Language Model Applications - https://owasp.org/www-project-top-10-for-large-language-model-applications/