Home Blog Contact
Home/Blog/How to Implement Sampling So an MCP Server Ca…
How toLLM EngineeringMCPsamplingPython SDK

How to Implement Sampling So an MCP Server Calls the Model

9 min readBy Miloš Mitrović

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 a CreateMessageResult whose content.text holds the generation.
  • The client must declare the sampling capability 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 mcp package. 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 sampling capability. 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.

  1. Install and pin the SDK. Pin below 2.0 so the imports in step 2 match.

    pip install "mcp[cli]==1.9.4"
  2. Write a tool that requests a completion. The tool takes a Context parameter, builds a SamplingMessage, and awaits ctx.session.create_message. The kwargs are snake_case (max_tokens, system_prompt, model_preferences), and the required one is max_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
  3. Ask for the right class of model, not a fixed one. The hints are advisory substrings the client matches loosely (sonnet can match claude-3-5-sonnet-20241022, or map to another provider's equivalent), and the three priorities are floats from 0 to 1. Raise intelligence_priority for reasoning, raise speed_priority for a fast classifier, raise cost_priority for 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,
    )
  4. Answer the request from a client. A server request is useless if nothing responds. A client that supports sampling passes a sampling_callback to its ClientSession. The callback receives the request context and the params, and returns a CreateMessageResult. 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()
  5. Run it and confirm the round trip. Start the server, connect the client, and call the summarize tool. 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.

ApproachWho runs the modelServer needs an API key?Best when
Sampling (sampling/createMessage)The client, on the user's accountNoYou distribute the server to others and want zero key management or billing on your side
Direct provider call inside the toolThe server, on your accountYesYou control deployment, want a specific model, and accept the cost and key handling
Multi Round-Trip Requests (SEP-2322)The client, via stateless payloadsNoYou 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 sampling at initialization. Confirm it passed a sampling_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 on mcp 2.0, which renamed the server class and moved the import. Either pin mcp<2 to match this guide, or switch the import to from mcp.server import MCPServer and adapt.
  • A validation error on the request. max_tokens is a required keyword argument and the priorities must be floats between 0 and 1. A missing max_tokens is 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 tools array and a toolChoice in 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_message and client SamplingFnT define 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.

Sources

M
Miloš Mitrović
Email Marketing for Ecommerce

Have a question or a project?

Whether it is about this post or a system you want built, I'm happy to talk.

Get in touch

404

Post not found. It may have been moved or the link is incorrect.

← Back to the blog
Summarize with AI
ChatGPT, Perplexity, and Grok open with the prompt ready to run. Claude, Gemini, and Copilot open a chat with the prompt copied; press Ctrl+V (Cmd+V on Mac) to paste. The full text is included, so it works even without web access.