Your MCP server needs a summary, a classification, or a short rewrite, but handing it an API key and a billing account defeats the point of running inside someone else's host. Sampling fixes that. The server asks the client to run a completion on the user's own model, so the intelligence and the cost stay with the host and no server-side keys change hands. The one-line version: a tool calls sampling/createMessage (in the Python SDK, ctx.session.create_message), the client reviews it, runs its model, and returns the text. This guide builds a working server tool and a client that answers it.
Key Takeaways
- Short answer: call
ctx.session.create_message(...)from inside a tool; the client runs a model and returns aCreateMessageResultwhosecontent.textholds the generation. - The client must declare the
samplingcapability during initialization or the call errors out, and many hosts still do not declare it, so test against a client you control. - Servers ask for a class of model using hints plus cost, speed, and intelligence priorities, never a hard model name the client may not have.
- There should always be a human able to review and reject a sampling request; the spec marks this a SHOULD, not a nicety.
- Sampling is fully supported in the 2025-11-25 spec and deprecated in 2026-07-28 under a minimum twelve-month window, so pin your SDK version and read the note below before you build.
What You Need Before You Start
Three things, and a clear idea of who runs the model.
- Python 3.10 or newer, which the official SDK requires.
- The
mcppackage. Pin the 1.x line for the code here, because the 2.0.0 release reorganized the server imports (more on that in troubleshooting):pip install "mcp[cli]==1.9.4". - A running MCP server to add the tool to. If you do not have one yet, stand up a minimal server first with the FastMCP quickstart for a Python MCP server, then come back.
- A client or host that declares the
samplingcapability. You will build a small one below so you are not blocked waiting on host support.
How to Add Sampling to Your MCP Server in Five Steps
Finish this section and you have a server tool that calls the host's model and a client that answers it. The deeper sections after it explain the choices.
-
Install and pin the SDK. Pin below 2.0 so the imports in step 2 match.
pip install "mcp[cli]==1.9.4" -
Write a tool that requests a completion. The tool takes a
Contextparameter, builds aSamplingMessage, and awaitsctx.session.create_message. The kwargs are snake_case (max_tokens,system_prompt,model_preferences), and the required one ismax_tokens.from mcp.server.fastmcp import FastMCP, Context from mcp.types import SamplingMessage, TextContent, ModelPreferences, ModelHint mcp = FastMCP("summarizer") @mcp.tool() async def summarize(text: str, ctx: Context) -> str: """Summarize text using the host's own model, no server API key.""" result = await ctx.session.create_message( messages=[ SamplingMessage( role="user", content=TextContent(type="text", text=f"Summarize:\n\n{text}"), ) ], system_prompt="You are a precise technical summarizer.", max_tokens=300, model_preferences=ModelPreferences( hints=[ModelHint(name="claude-3-5-sonnet")], intelligence_priority=0.8, speed_priority=0.4, ), ) return result.content.text -
Ask for the right class of model, not a fixed one. The
hintsare advisory substrings the client matches loosely (sonnetcan matchclaude-3-5-sonnet-20241022, or map to another provider's equivalent), and the three priorities are floats from 0 to 1. Raiseintelligence_priorityfor reasoning, raisespeed_priorityfor a fast classifier, raisecost_priorityfor high-volume work.model_preferences=ModelPreferences( hints=[ModelHint(name="claude-3-5-sonnet"), ModelHint(name="claude")], cost_priority=0.3, speed_priority=0.8, intelligence_priority=0.5, ) -
Answer the request from a client. A server request is useless if nothing responds. A client that supports sampling passes a
sampling_callbackto itsClientSession. The callback receives the request context and the params, and returns aCreateMessageResult. Wire it to your provider of choice; the stub below shows the exact shape.from mcp import ClientSession, types async def handle_sampling( context, params: types.CreateMessageRequestParams, ) -> types.CreateMessageResult: # Show params.messages to the user, run your model, then return: return types.CreateMessageResult( role="assistant", content=types.TextContent(type="text", text="...model output..."), model="claude-3-5-sonnet-20241022", stopReason="endTurn", ) # When opening the session: # async with ClientSession(read, write, sampling_callback=handle_sampling) as session: # await session.initialize() -
Run it and confirm the round trip. Start the server, connect the client, and call the
summarizetool. The client's callback fires, runs the model, and the tool returns the text. Keep a human in the loop: the review step is where a user catches a prompt they did not expect the server to send.
Should the Server Sample, Hold a Key, or Wait for the New Pattern?
Sampling is one of three ways a tool can get model output. Pick by who should pay, who should choose the model, and how portable the server needs to be.
| Approach | Who runs the model | Server needs an API key? | Best when |
|---|---|---|---|
Sampling (sampling/createMessage) | The client, on the user's account | No | You distribute the server to others and want zero key management or billing on your side |
| Direct provider call inside the tool | The server, on your account | Yes | You control deployment, want a specific model, and accept the cost and key handling |
| Multi Round-Trip Requests (SEP-2322) | The client, via stateless payloads | No | You target the 2026-07-28 spec and want the successor to server-initiated requests |
Sampling reaches the model without keys, which is why it fits servers you ship to other people. It also joins the other server-to-client interactions worth knowing: exposing resources a client can read and offering reusable prompt templates.
What the Client, Not the Server, Decides
The server states preferences; the client makes the call. Per the MCP sampling specification, hints are advisory and the client MAY map them to another provider's model. The client also owns approval, model access, and rate limiting, which is the whole reason the design keeps keys out of the server.
One field to leave alone: includeContext defaults to none, and the thisServer and allServers values are soft-deprecated unless the client declares a sampling.context capability. Omit it and keep your prompt self-contained.
Read the Deprecation Before You Commit
Sampling works today and is part of the current 2025-11-25 release, but the 2026-07-28 specification deprecates it (with roots and logging, under SEP-2577) in favor of the Multi Round-Trip Requests pattern. Deprecated does not mean gone: a feature must stay deprecated for at least twelve months from that revision before it is eligible for removal, so servers built on sampling keep working for the foreseeable future. Pin your SDK version, and plan a migration path if you are starting fresh.
Troubleshooting the Errors You Will Actually Hit
- The call raises or the client returns an error about an unsupported capability. The client did not declare
samplingat initialization. Confirm it passed asampling_callback, or test against your own client instead of a host that has not implemented sampling. ModuleNotFoundError: No module named 'mcp.server.fastmcp'after an upgrade. You are onmcp2.0, which renamed the server class and moved the import. Either pinmcp<2to match this guide, or switch the import tofrom mcp.server import MCPServerand adapt.- A validation error on the request.
max_tokensis a required keyword argument and the priorities must be floats between 0 and 1. A missingmax_tokensis the most common cause. - The request comes back rejected with code
-1. That is a user declining the sampling request, which is expected behavior, not a bug. Handle it and move on. - You cannot tell whether the server ever sent the request. Inspect the traffic with the official tool; the workflow is in debugging an MCP server with the MCP Inspector.
What to Do Next
- Add tool use inside sampling. The 2025-11-25 spec supports a
toolsarray and atoolChoicein the request so the model can call tools mid-generation; read the sampling spec section on tools for the multi-turn loop and the tool-result balance rules. - Study the callback contract in the official Python SDK, whose
ServerSession.create_messageand clientSamplingFnTdefine the exact signatures. - If you are building for 2026 and beyond, prototype against the Multi Round-Trip Requests pattern in the 2026-07-28 revision so your server ages well.