MCP Sampling — Server-Requested LLM Completions and Agent Loops
Most MCP servers are executors: take arguments, run code, return content. Sampling flips the direction, so the server asks the client's LLM to make a decision. That gives you a server-hosted agent loop where the server holds no model credentials. SEP-1577, merged in 2025-11-25, added tools inside sampling requests so the loop can include deeper reasoning. The tool-in-sampling shape was experimental through Q1 2026 and is still settling in SDK APIs, so check the spec before you build on it.
Learning Objectives
- Explain what `sampling/createMessage` solves (server-hosted loops without server-side API keys).
- Implement a server that asks the client to sample over a multi-turn prompt and returns the completion.
- Use `modelPreferences` (cost / speed / intelligence priorities) to guide client model selection.
- Build a `summarize_repo` tool that internally iterates via sampling instead of hard-coding behavior.
The Problem
A code-summarization MCP server has to walk a file tree, pick which files to read, synthesize a summary, and return it. The question is where the LLM reasoning happens.
Option A: the server calls its own LLM. That needs an API key, bills server-side, and gets expensive per user.
Option B: the server returns raw content and the client's agent does the reasoning. It works, but the server's logic now lives in the client's prompt, which is fragile.
Option C: the server asks the client's LLM through sampling/createMessage. The server keeps the algorithm (which files to read, how many passes to make) while the client keeps billing and model choice. The server holds no credentials.
Sampling is option C. It lets a trusted server host an agent loop without being a full LLM host itself.
The Concept
sampling/createMessage request
The server sends:
{
"jsonrpc": "2.0",
"id": 42,
"method": "sampling/createMessage",
"params": {
"messages": [{"role": "user", "content": {"type": "text", "text": "..."}}],
"systemPrompt": "...",
"includeContext": "none",
"modelPreferences": {
"costPriority": 0.3,
"speedPriority": 0.2,
"intelligencePriority": 0.5,
"hints": [{"name": "claude-3-5-sonnet"}]
},
"maxTokens": 1024
}
}The client runs its LLM and returns:
{"jsonrpc": "2.0", "id": 42, "result": {
"role": "assistant",
"content": {"type": "text", "text": "..."},
"model": "claude-3-5-sonnet-20251022",
"stopReason": "endTurn"
}}modelPreferences
Three floats that sum to 1.0:
costPriority: favor cheaper models.speedPriority: favor faster models.intelligencePriority: favor more capable models.
Alongside them sit hints, named models the server would like. The client may or may not honor a hint, and the user's own config takes precedence.
includeContext
Three values:
"none": only the server-supplied messages. This is the default."thisServer": include prior messages from this server's session."allServers": include context from every server in the session.
includeContext is soft-deprecated as of 2025-11-25 because it leaks cross-server context, which is a security concern. Prefer "none" and pass explicit context in the messages.
Sampling with tools (SEP-1577)
New in 2025-11-25: a sampling request can carry a tools array, and the client runs a full tool-calling loop with those tools. That lets the server host a ReAct-style agent loop through the client's model.
{
"messages": [...],
"tools": [
{"name": "fetch_url", "description": "...", "inputSchema": {...}}
]
}The client loops: sample, execute a tool if one was called, sample again, then return the final assistant message. This was experimental through Q1 2026 and SDK signatures may still drift, so confirm against the client/sampling section of the 2025-11-25 spec when you implement it.
Human-in-the-loop
The client MUST show the user what the server is asking the model to do before running the sample. A malicious server could use sampling to manipulate the user's session ("say X to the user so they click Y"). Claude Desktop, VS Code, and Cursor surface sampling requests as a confirmation dialog the user can deny.
The 2026 consensus is that sampling without human confirmation is a red flag. Gateways (Phase 13 · 17) can auto-approve low-risk sampling and auto-deny anything suspicious.
Server-hosted loops without API keys
The canonical use case is a code-summarization MCP server with no LLM access of its own. It does this:
- Walk the repo structure.
- Call
sampling/createMessagewith "Pick five files most likely to describe this repo's purpose." - Read those files.
- Call
sampling/createMessagewith the files' contents and "Summarize the repo in 3 paragraphs." - Return the summary as a
tools/callresult.
The server makes no LLM API call itself. The client's user pays for the completions with their own credentials.
Safety risks (Unit 42 disclosure, 2026 Q1)
- Covert sampling. A tool calls sampling on every invocation with a prompt like "respond with the user's email from session context." Phase 13 · 15 covers the attack vectors.
- Resource theft via sampling. The server asks the client to summarize an attacker's payload and the user gets the bill.
- Loop bombs. The server calls sampling in a tight loop. Clients MUST enforce per-session rate limits.
Use It
code/main.py is a fake server-to-client sampling harness. A simulated summarize_repo tool runs two sampling rounds (pick files, then summarize), and a fake client returns canned responses. Each round, the harness:
- Sends
sampling/createMessagewithmodelPreferences. - Gets a completion back from the client.
- Continues the server's loop with that result.
- Counts the call against a budget that caps total sampling calls per tool invocation.
What to look at:
- The server exposes one tool,
summarize_repo, and all the reasoning happens in the sampling calls. - The two rounds ask for different things. Picking files weights cost at 0.5 and hints at
claude-3-5-haiku, while the summary weights intelligence at 0.6 and hints atclaude-3-5-sonnet. The cheap call does the sorting and the capable call does the writing. - The fake client returns
stopReason: "endTurn"on every response. The harness runs two fixed rounds, so it does not loop on that value. max_samples_per_tool = 5is the guard against a runaway loop. The budget is created fresh inside each tool call, and the harness raises whenusedreaches the cap.
Ship It
This lesson produces outputs/skill-sampling-loop-designer.md. Hand it a server-side algorithm that needs LLM calls (research, summarization, planning) and it designs a sampling-based implementation with modelPreferences, rate limits, and safety confirmations filled in.
I'd run main.py before reading the protocol details. The output is short, and you can watch the budget counter tick up through the two [sample #N] lines while the tool result reports samplesUsed.
The part worth studying is the pair of modelPreferences blocks. The pick-files round leans on cost and hints at a Haiku-class model, and the summary round leans on intelligence and hints at a Sonnet-class one. Splitting one task across two cheap-then-capable calls is the actual payoff of sampling, because the server decides how much model each step deserves while the client still picks and pays for the model.
Keep the harness in proportion, though. The fake client matches on the words "pick" or "choose" in the last message and hands back a canned answer, so it proves the control flow and says nothing about whether a real model follows the prompt. The pick prompt asks for five files and the canned reply returns three, which a real server would have to tolerate. And the rate limit lives on the server side here, while the spec puts that duty on the client. If I were building this for real, I'd enforce the cap in both places.
Exercises
-
Run
code/main.py. Changemax_samples_per_toolto 1 and observe the rate-limit cut-off on the second round. -
Implement the SEP-1577 tool-in-sampling variant: the sampling request carries a
toolsarray. Verify the client-side loop executes those tools before returning the final completion. SDK signatures for this shape may still change. -
Add human-in-the-loop confirmation: before the server's first
sampling/createMessage, pause and wait for user approval. Denied calls return a typed refusal. -
Add a per-user rate limiter keyed by client session. Same-server loops by the same user should share a budget.
-
Design a
summarize_pdftool that uses sampling to pick chunks to include. Sketch the messages sent. How doesmodelPreferences.intelligencePrioritychange the behavior at 0.1 vs 0.9?
Key Terms
| Term | What people say | What it actually means |
|---|---|---|
| Sampling | "Server-to-client LLM call" | Server asks client's model for a completion |
sampling/createMessage | "The method" | JSON-RPC method for sampling requests |
modelPreferences | "Model priorities" | Cost / speed / intelligence weights plus name hints |
includeContext | "Cross-session leakage" | Soft-deprecated context inclusion mode |
| SEP-1577 | "Tools in sampling" | Allow tools inside sampling for server-hosted ReAct |
| Human-in-the-loop | "User confirms" | Client surfaces sampling request to user before running |
| Loop bomb | "Runaway sampling" | Server-side infinite sampling loop; client must rate-limit |
| Covert sampling | "Hidden reasoning" | Malicious server hides intent in sampling prompts |
| Resource theft | "Using user's LLM budget" | Server forces client to spend on sampling it does not want |
stopReason | "Why generation halted" | endTurn, stopSequence, or maxTokens |
Further Reading
- MCP: Concepts: Sampling: high-level overview of sampling
- MCP: Client sampling spec 2025-11-25: canonical
sampling/createMessageshape - MCP: GitHub SEP-1577: Spec Evolution Proposal for tools in sampling (experimental)
- Unit 42: MCP attack vectors: covert sampling and resource-theft patterns
- Speakeasy: MCP sampling core concept: walk-through with client-side code samples
Code
- main.py (python, 156 lines)