← Back to Blog

MCP Sampling Is Deprecated: Migrate Your LLM-Borrowing Tools

If your MCP server has a tool that borrows the caller's model to summarize, classify, or rewrite text, that tool breaks the moment a client negotiates MCP 2026-07-28. The failure is abrupt: ctx.session.create_message() raises NoBackChannelError, and the error text never mentions what replaced sampling. This guide covers what the protocol removed, the two supported replacements, runnable code for each, and a short checklist to choose between them.

Prerequisites

  • Python 3.10 or newer
  • pip install "mcp>=1.27,<2" for the stable v1 line, or the v2 line if you want to track the newer SDK (v2 went GA in August 2026)
  • fastmcp>=4 if your server uses FastMCP
  • An Anthropic or OpenAI API key for the direct-call route
  • One MCP tool that currently calls sampling, so you have something real to migrate

What changed

The 2026-07-28 revision made MCP stateless at the protocol layer. Two consequences follow for sampling.

First, a server can no longer push a request to the client while it handles one. SEP-2322 replaces server-initiated calls (roots/list, sampling/createMessage, elicitation/create) with Multi Round-Trip Requests. The server returns an InputRequiredResult carrying resultType: "input_required", an inputRequests map, and an opaque requestState token. The client gathers the answers and re-issues the same tool call with inputResponses plus the token.

Second, sampling is deprecated as a feature (SEP-2577), alongside roots and logging. The changelog's suggested migration is blunt: "integrate directly with LLM provider APIs instead of Sampling."

Neither change removes the ability to ask the caller's model. What was removed is the push channel, not the question. You can still put a question to the caller's model, only now the tool has to return and let the client come back.

Step 1: find out which revision you are on

Confirm what your client negotiates before touching code:

curl -si http://localhost:8000/mcp \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover"}' | head -20

server/discover is mandatory in 2026-07-28, and it returns the protocol versions, capabilities, and identity your server advertises. If the client sends no MCP-Protocol-Version header, you are on the handshake-era path and create_message still works there.

Three symptoms mean the rewrite is due:

  • NoBackChannelError from ctx.session.create_message() or ctx.elicit()
  • MCPDeprecationWarning on the first ctx.info(...) call
  • AttributeError: 'Context' object has no attribute 'sample' on FastMCP 4, which dropped sample() and sample_step() for every protocol era

Path A: call the provider directly

This is the migration the spec recommends, and for most servers it is less code than the sampling call it replaces. Hold the key in the server's environment, create the client once at module scope, and generate inside the tool.

import anthropic
from fastmcp import FastMCP

mcp = FastMCP("Summarizer")
llm = anthropic.AsyncAnthropic()  # module scope, so connections are reused

@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

The tool now behaves the same for every client, including the many that never implemented the sampling capability. You pick the model, you can read token usage, and retries, timeouts, caching, and cost accounting become ordinary application code instead of a protocol negotiation. It also tests with no client attached, which the sampling version could not.

Path B: borrow the caller's model through a round trip

Sometimes the caller's model is the point. An enterprise client may require generation to run through its own gateway, under its own credentials and its own bill, or it may refuse to hand your server an API key at all. There you keep the ask and drop the push.

Return an InputRequiredResult on the first pass and read the answer from ctx.input_responses on the retry:

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."

Two constraints come with this path. The client must negotiate 2026-07-28, since older clients have no way to return input for a call that is still in flight. And every ask costs a full round trip, during which the tool sits blocked. To carry values across several rounds, put them in requestState and echo it on the retry, because any server instance may handle the second request.

On the official Python SDK v2 you rarely write that loop by hand. Declare the dependency with Resolve and let the client SDK drive the round trips.

Path C: stay on v1 while your clients catch up

The feature lifecycle policy gives every deprecated feature at least twelve months before it can be removed, so sampling/createMessage keeps working against clients that speak 2025-11-25 or earlier. Pin the stable line and treat it as a deadline, not a destination:

pip install "mcp>=1.27,<2"

Set that upper bound today if you publish a package that depends on mcp. Otherwise a later install pulls v2 and surprises your users.

Choosing a path

Situation Use
Server holds its own key and the tool must behave the same for every client Path A
Client requires generation on its own gateway, credentials, or bill Path B
Legacy stdio client on a pre-2026 connection Path C, with a migration date
Tool needs no generation at all Delete the sampling call
You want token usage, retries, and caching under your own control Path A

Errors you will hit

NoBackChannelError means the connection is 2026-07-28 and there is no channel for a server-initiated request. Move to Path A or Path B. MCPDeprecationWarning on logging calls is the same deprecation pass reaching ctx.info(); write to stderr on stdio or ship OpenTelemetry. If an MRTR tool never gets answered, check the negotiated revision first, then check that the client knows how to fulfill a sampling/createMessage input request.

Conclusion

Sampling is deprecated, and the channel it ran on was removed. Two replacements cover the work: call the provider directly when your server owns the key (Path A, the default), or return an InputRequiredResult when borrowing the caller's model is genuinely the requirement (Path B). If you still run v1 for legacy clients, pin mcp>=1.27,<2 and put a date on the migration.

Next step: grep -rn "create_message\|ctx.sample(\|ctx.elicit(" your-servers/ and decide per tool. For anything that lands in Path A, add a test that calls the tool with no client attached, since that is the failure mode the old design let you ship without noticing.

References

Need Help Implementing This?

I help teams design and build scalable cloud infrastructure, DevOps pipelines, and production-grade systems.

Book a Free Consultation