# Sampling-request handling

Canonical URL: https://vercel-mcp-reference.vercel.app/client-side/sampling-request-handling/
Markdown: https://vercel-mcp-reference.vercel.app/client-side/sampling-request-handling.md
Audience: engineer, architect, security. MCP spec version: 2026-07-28. Last reviewed: 2026-08-26. Status: stable.

> **Deprecated in 2026-07-28 (SEP-2577).** [Sampling](https://vercel-mcp-reference.vercel.app/glossary/#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](https://ai-sdk.dev/docs/introduction), and route providers, keys, and budgets through [AI Gateway](https://vercel.com/docs/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](https://modelcontextprotocol.io/specification/2026-07-28/deprecated).

**TL;DR:** [Sampling](https://vercel-mcp-reference.vercel.app/glossary/#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](https://vercel-mcp-reference.vercel.app/glossary/#client) **retries the original request** with the completion in `inputResponses`. This is the [Multi Round-Trip Requests (MRTR) pattern](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr). 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)

1. The client sends the original request (say `tools/call`).
2. The server, needing a completion, returns `resultType: "input_required"` with a `sampling/createMessage` entry in `inputRequests`, plus an opaque `requestState` blob encoding whatever context it needs to resume.
3. The host reviews the sampling request, runs (or edits, or refuses) the inference.
4. The client **retries the original request** with a **new JSON-RPC id**, the completion keyed into `inputResponses`, and `requestState` echoed back byte for byte.
5. The server reconstitutes its state from `requestState` and finishes the call (or returns another `input_required` round).

```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
    end
```

Two 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](https://vercel-mcp-reference.vercel.app/glossary/#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](https://vercel-mcp-reference.vercel.app/internals/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](https://vercel-mcp-reference.vercel.app/patterns/async-jobs/), but the pressure that used to come from a user deliberating over an approval dialog is gone. The [2026-07-28 revision page](https://vercel-mcp-reference.vercel.app/internals/spec-2026-07-28/) 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 plus `costPriority` / `speedPriority` / `intelligencePriority` values 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 the `sampling.context` sub-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](https://vercel-mcp-reference.vercel.app/patterns/trust-boundaries/) forbids. The host decides what, if anything, to actually include.
- **`maxTokens`**, temperature, stop sequences - `maxTokens` the client **MUST** respect as a ceiling; the rest are bounds the host may clamp, modify, or ignore.
- **`tools`** and **`toolChoice`** - a sampling request may carry **server-supplied tool definitions** plus a `toolChoice` directive (`auto`, `required`, or `none`), enabling tool calling *within* the sampling turn. This is gated twice. First by capability: clients **MUST** declare `sampling.tools` to 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 as `messages`), 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](https://vercel-mcp-reference.vercel.app/client-side/consent-ux/) discipline as any other tool call, and may decline or edit it. A forcing `toolChoice: { "mode": "required" }` is likewise a *request*, not an obligation. Under MRTR the multi-turn tool loop becomes multiple `input_required` rounds: the client returns the model's `tool_use` blocks in `inputResponses`, 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](https://vercel-mcp-reference.vercel.app/client-side/consent-ux/) 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](https://vercel-mcp-reference.vercel.app/client-side/tool-result-rendering/) care you apply to tool results. See the [output trust checklist](https://vercel-mcp-reference.vercel.app/security/checklist/#output-trust).
- **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.
- **`includeContext` beyond `"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 `tools` are untrusted definitions, and the host still owns execution.** Treat the definitions as part of the same injection-prone payload as `messages`, apply consent per elected call, and ignore a forcing `toolChoice` you have no reason to honor.
- **`requestState` is 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](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr) 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_required` rounds on the same logical call. The spec itself tells both parties to implement iteration limits for tool loops. Budget sampling per user, clamp `maxTokens`, cap retry rounds per originating request, and rate-limit requests per server. On Vercel the invocation's `maxDuration` bounds 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** - `modelPreferences` are hints; treating them as a selection lets a server force an expensive or weak model.
- **Honoring the deprecated `includeContext` values without the `sampling.context` capability 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`/`toolChoice` as 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 `-1` belongs to the legacy wire shape.
- **No cost or loop ceiling** - repeated `input_required` sampling 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** over `InMemoryTransport.createLinkedPair()`, declares the `sampling` capability 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-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 protocol-level definition sits in [capability primitives](https://vercel-mcp-reference.vercel.app/internals/primitives/); the gating discipline is [Consent UX](https://vercel-mcp-reference.vercel.app/client-side/consent-ux/).

## Related

- [Consent UX](https://vercel-mcp-reference.vercel.app/client-side/consent-ux/) - the human-in-the-loop gate, now living in the retry decision of the MRTR loop.
- [trust-boundaries](https://vercel-mcp-reference.vercel.app/patterns/trust-boundaries/) - why the server stays out of the host's conversation and why cross-server context is gated.
- [Multi-server composition](https://vercel-mcp-reference.vercel.app/client-side/multi-server-composition/) - the host-mediated context `includeContext` would otherwise reach across.
- [capability primitives](https://vercel-mcp-reference.vercel.app/internals/primitives/) - the protocol-level definition of sampling alongside the other primitives.
- [Serverless sessions](https://vercel-mcp-reference.vercel.app/internals/serverless-sessions/) - the invocation-lifetime mechanics behind the legacy squeeze and the MRTR relief.
- [The 2026-07-28 revision](https://vercel-mcp-reference.vercel.app/internals/spec-2026-07-28/) - 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/>
