docs/servers/sampling.mdx
import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="2.0.0" /> <Warning> **`ctx.sample()` and `ctx.sample_step()` were removed in FastMCP 4.** The modern MCP protocol gives a server no channel to push a request to its client, so there is nothing left for those methods to do.To build a server that uses sampling, stay on FastMCP 3.x. On FastMCP 4, generate by calling an LLM directly, or ask the caller's model when borrowing their model is the point. </Warning>
A tool that needs text generated calls a model to get it, and in FastMCP 4 that call is ordinary Python: your server holds an API key, creates a provider client, and awaits a completion inside the tool. No protocol is involved, so the tool behaves the same for every client — including the many that never implemented sampling at all.
The alternative is to ask the caller. Sampling borrows the caller's model — their provider, their credentials, their bill — by returning a request for a completion that the client fulfils and hands back. Every ask costs a full round trip, so it earns its keep when using the caller's model is the point, and rarely otherwise.
Hold a provider API key in your server's environment, create the client once at module scope so connections are reused across calls, and generate inside the tool. You choose the model, control the prompt, see the token usage, and can test the tool with no client attached.
import anthropic
from fastmcp import FastMCP
mcp = FastMCP("Summarizer")
llm = anthropic.AsyncAnthropic()
@mcp.tool
async def summarize(text: str) -> str:
"""Summarize a document in two sentences."""
response = await llm.messages.create(
model="claude-sonnet-4-5",
max_tokens=512,
system="Summarize the user's text in exactly two sentences.",
messages=[{"role": "user", "content": text}],
)
return response.content[0].text
Any provider SDK works the same way — swap the client and the call, and the tool signature is unchanged. Because generation is ordinary application code, the concerns around it are ordinary too: retries, timeouts, caching, and cost accounting go wherever you want them rather than being negotiated across a protocol boundary. A tool that chains several generations pays nothing extra for the second and third, where asking the caller would pay a full round trip for each.
A tool asks for a completion by returning an InputRequiredResult whose input_requests map holds a CreateMessageRequest under a key you choose. That result completes the round normally. The client runs the completion, then re-issues the same call_tool with the answer attached, and your tool reads it from ctx.input_responses under the same key — a CreateMessageResult. Because the tool runs from the top on every round, the presence of ctx.input_responses is what tells the two rounds apart: None on the first call, populated on the continuation.
fastmcp.Client drives that loop for you and answers from the sampling_handler it already has, so a client written for a handshake-era server needs no extra wiring to satisfy a modern tool that asks this way.
from fastmcp import Context, FastMCP
from mcp.types import (
CreateMessageRequest,
CreateMessageRequestParams,
CreateMessageResult,
InputRequiredResult,
SamplingMessage,
TextContent,
)
mcp = FastMCP("Research")
@mcp.tool
async def ask_the_caller(question: str, ctx: Context) -> str | InputRequiredResult:
"""Put a question to the caller's model and report what it answered."""
responses = ctx.input_responses
if responses is None:
return InputRequiredResult(
result_type="input_required",
input_requests={
"answer": CreateMessageRequest(
method="sampling/createMessage",
params=CreateMessageRequestParams(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text=question),
)
],
max_tokens=100,
),
)
},
)
answer = responses["answer"]
if isinstance(answer, CreateMessageResult) and isinstance(
answer.content, TextContent
):
return answer.content.text
return "The client returned no completion."
Returning an InputRequiredResult needs a 2026-07-28 connection, and FastMCP names the era mismatch if an older client reaches the tool; the conformance suite exercises this route on that version. The map can carry several requests at once and mix kinds — a sampling request beside an elicitation or a roots request — with each answer coming back under its own key. Elicitation covers the mechanics of the pattern in full, including how to carry state across rounds.
Context has no sample() and no sample_step(); touching either raises AttributeError on every protocol era, rather than failing at runtime only against modern clients. FastMCP() accepts neither sampling_handler= nor sampling_handler_behavior=, and naming one raises a TypeError that points at the migration.
The reason is the distinction MCP draws between telling and asking. A notification is fire-and-forget: the server emits it and moves on, and it travels down the response stream the caller already opened, so nothing has to be held open on the server's behalf. That is why logging is untouched by any of this — ctx.info() and its siblings reach the client mid-call on every era. Sampling is the other kind. sampling/createMessage goes out and the caller must answer before the tool can continue, which needs a live, addressable connection the server can reach into, and the 2026-07-28 revision removed server-initiated requests (SEP-2577) precisely because a stateless protocol has no such thing.
What the protocol removed is the pushing, not the asking, so the capability survives in the shape described above. Keeping ctx.sample() alongside it would mean shipping a method whose outcome against a default client — one that negotiates the modern era — is a runtime failure.