Sampling and elicitation: the wire runs both ways
Everything so far flows client → server. The fourth primitive inverts the arrow: sampling lets a server, mid-execution, ask the client's model to generate text. Its younger sibling elicitation lets a server ask the user a structured question. Between them, a server gains language and judgement without owning a model, an API key, or a UI.
Why a server would want this
Suppose Paper Trail should summarise the week's customer reviews. Without sampling, the server needs its own LLM: an API key to manage, a bill to pay, a model choice frozen at deploy time — and if a hundred hosts connect, the server pays for all of them. With sampling, the server sends its prompt back across the session and lets whatever model the host already runs do the work. Capability stays on the server; inference cost, model choice, and rate limits stay with the party that already owns them. It also keeps the human loop intact: hosts can show the user what a server wants generated before running it.
The round trip, run for real
Server side — inside a tool, via the injected Context:
from fastmcp import FastMCP, Context
@mcp.tool
async def summarize_reviews(ctx: Context) -> str:
"""Summarize this week's customer reviews in one sentence."""
reviews = ["Loved the packaging", "Shipping took 9 days", "Great curation"]
result = await ctx.sample(
f"Summarize these bookstore reviews in one sentence: {reviews}",
system_prompt="You are terse.",
max_tokens=60,
)
return f"[server received from client] {result.text}"Client side, the mirror image — a sampling handler the client registers. Here is the honest part: the handler is client code, so we can demonstrate the entire protocol round trip without an LLM by returning a fixed string where a real host would call its model:
async def sampling_handler(messages, params, ctx) -> str:
print("[client] server asked us to sample:")
print("[client] system:", params.systemPrompt)
print("[client] prompt:", messages[0].content.text[:80], "…")
return "Customers praise packaging and curation; shipping speed is the sore point."The session, captured live:
[client] server asked us to sample:
[client] system: You are terse.
[client] prompt: Summarize these bookstore reviews in one sentence: ['Loved the packaging', 'Ship …
[client] final tool result: [server received from client] Customers praise packaging and
curation; shipping speed is the sore point.Read the choreography: the client called summarize_reviews; mid-execution the server sent a sampling/createMessage request back up the same session; the client's handler answered; the server's coroutine resumed holding the text and finished its result. Two nested request/response pairs, opposite directions, one session — the bidirectional wire from lesson 3 earning its keep. In production the handler body is one LLM call, and the model's reply (unlike everything else in this course) would vary run to run.
Two design notes. The server may express model preferences — cost/speed/capability priorities, even suggested model names — but they are hints; the client owns the choice and the spend. And a server must treat sampling as fallible: the host may not support it (support across hosts is still uneven — check yours before depending on it), or the user may refuse. Wrap ctx.sample in a fallback path, or degrade to returning the raw material un-summarised.
Elicitation: asking the user, properly
Sometimes mid-tool the missing ingredient is not text generation but an answer — which shipping address? proceed despite the fee? Before elicitation, servers grew an ugly workaround: return an error hoping the model would relay the question. Elicitation (added to the spec in the June 2025 revision) makes it first-class: the server sends a question plus a JSON Schema for the answer; the host renders a real form; the reply comes back accepted (with validated data), declined, or cancelled — and your code must treat those as three different facts, not one boolean. Handle "declined" as an answer in itself; only "accepted" carries data.
result = await ctx.elicit("This order qualifies for express replacement (₹80 fee). Proceed?",
response_type=bool)The security caveat writes itself: a server can now put a dialog in front of your user, and phrasing a question is a persuasion channel. Treat elicitation prompts from a server with the same scepticism as tool descriptions — which is precisely where section 4 begins.
Try this: sketch draft_refund upgraded with both primitives — ctx.elicit to confirm the refund amount with the user, then ctx.sample to draft the email in the store's voice. Decide what the tool does when the host supports neither; that fallback branch is the difference between a demo and a server.