From 1a43a3b8e962d9697e05f37e24322da08a2ac783 Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Sun, 26 Jul 2026 14:43:18 -0400
Subject: [PATCH] Document that server-initiated sampling and roots are not in
FastMCP 4
---
dev-docs/v4-notes/change-register.md | 33 +++++++---
dev-docs/v4-notes/feature-program.md | 16 ++---
dev-docs/v4-notes/index.md | 2 +-
docs/clients/sampling.mdx | 4 +-
docs/deployment/http.mdx | 2 +-
.../upgrading/from-fastmcp-3.mdx | 34 +++++-----
docs/getting-started/whats-new.mdx | 2 +-
docs/servers/context.mdx | 15 ++---
docs/servers/elicitation.mdx | 2 +-
docs/servers/sampling.mdx | 66 +++++++++++++++++++
docs/servers/server.mdx | 12 +---
11 files changed, 126 insertions(+), 62 deletions(-)
create mode 100644 docs/servers/sampling.mdx
diff --git a/dev-docs/v4-notes/change-register.md b/dev-docs/v4-notes/change-register.md
index 1e791f735..af6d36049 100644
--- a/dev-docs/v4-notes/change-register.md
+++ b/dev-docs/v4-notes/change-register.md
@@ -432,31 +432,44 @@ The push-style Context features that require the server to call back into the cl
| `ctx.info` / logging notifications | Supported | Supported |
| Tools, resources, prompts, completions | Supported | Supported |
| `ctx.elicit` (imperative) | Supported | Not on the back-channel — use [elicitation on the modern protocol](https://gofastmcp.com/servers/elicitation#elicitation-on-the-modern-protocol) |
-| `ctx.sample` / `ctx.sample_step` | Supported (deprecated) | Removed — call an LLM server-side |
-| `ctx.list_roots` | Supported | Via the [guard pattern](https://gofastmcp.com/servers/elicitation#elicitation-on-the-modern-protocol) |
+| `ctx.sample` / `ctx.sample_step` | Not in the API | Not in the API — call an LLM server-side |
+| `ctx.list_roots` | Not in the API | Not in the API — take paths as arguments, or use the [guard pattern](https://gofastmcp.com/servers/elicitation#elicitation-on-the-modern-protocol) |
+| `client.set_logging_level()` | Supported | Raises — `logging/setLevel` is absent from the era's registry |
| Background tasks (`task=True`) | Runs synchronously — never tasked | Supported via the tasks extension |
-Tools that rely on `ctx.elicit` or `ctx.list_roots` continue to work against clients on the session-based eras. On the modern era, elicitation is reachable through the multi-round "guard" pattern instead (a tool returns an `InputRequiredResult`; see the New entry below). Sampling is the exception: it is deprecated on every era and will not return on modern connections (see the Deprecated entry below).
+Tools that rely on `ctx.elicit` continue to work against clients on the session-based eras; on the modern era, elicitation is reachable through the multi-round "guard" pattern instead (a tool returns an `InputRequiredResult`; see the New entry below). Sampling and roots have no era row to speak of — they left the server API entirely (see the Removed entry below).
-Ordinary `ctx.info` usage emits an SDK-level `MCPDeprecationWarning` ("The logging capability is deprecated as of 2026-07-28 (SEP-2577)"). That warning comes from the SDK, not FastMCP, and is benign — logging keeps working on session-based connections per the matrix. `ctx.sample`/`ctx.sample_step` additionally emit a FastMCP-owned `FastMCPDeprecationWarning` (see below). The upgrade guide calls both out explicitly.
+Ordinary `ctx.info` usage emits an SDK-level `MCPDeprecationWarning` ("The logging capability is deprecated as of 2026-07-28 (SEP-2577)"). That warning comes from the SDK, not FastMCP, and is benign — logging *notifications* ride the request's own stream and work on every era, including the modern one. The upgrade guide calls it out explicitly.
Wire interop across the transition is verified: a 3.4.3 client against a v4 server and a v4 client against a 3.4.3 server are bidirectionally clean across 9 operations over HTTP (WS2).
*Verify:* `docs/getting-started/upgrading/from-fastmcp-3.mdx` (the published matrix and SDK-warning note), `tests/server/test_protocol_eras.py`.
-### Sampling deprecated, era-gated — Deprecated
+### Server-initiated sampling and roots removed from the server API — Breaking
-`ctx.sample()` and `ctx.sample_step()` are deprecated and slated for removal in a future FastMCP release. Server-initiated sampling relies on the `createMessage` back-channel that SEP-2577 removed from the wire as of `2026-07-28`, and unlike elicitation it has no multi-round-trip replacement (the agentic loop would exhaust the round-trip budget). Both methods now emit a `FastMCPDeprecationWarning` once per process (gated on `settings.deprecation_warnings`), and on a `2026-07-28` connection they raise a clear `ToolError` before touching the wire. The client-side sampling handler infrastructure (anthropic/openai/google_genai) is retained for future MRTR work and is not deprecated. The migration is to call an LLM directly from your server rather than borrowing the client's model.
+FastMCP 4 is a modern MCP toolkit, so the capabilities the modern protocol removed are not in its server-authoring API. `Context.sample()`, `Context.sample_step()`, and `Context.list_roots()` are gone, along with the whole `fastmcp/server/sampling/` package (`SamplingTool`, `SampleStep`, `SamplingResult`, the tool loop, structured-result sampling) and the server-side handler arguments `FastMCP(sampling_handler=..., sampling_handler_behavior=...)`. These were previously deprecated-and-era-gated; they are now absent. Calling them raises `AttributeError`; the constructor kwargs raise a `TypeError` naming SEP-2577 and the migration.
-The dead TODO at `server/context.py` (a background-task sampling relay that was never built) is removed: that relay is not being built, so the note is gone rather than left as a promise.
+The motivating failure is that the gate had become the default experience. `Client` now defaults to `mode="auto"`, which negotiates `2026-07-28` against a FastMCP server, so an unmodified `ctx.sample()` server failed on an ordinary client connection. Four shipped examples (`examples/sampling/`) were broken by that flip; they are deleted rather than ported, and remain available on `release/3.x`.
-*Verify:* `fastmcp_slim/fastmcp/server/context.py` (`_warn_sampling_deprecated`, `_is_modern_protocol`, the `sample`/`sample_step` gates), `docs/servers/sampling.mdx` (deprecation banner), `tests/server/test_protocol_eras.py` (warning + era-gate tests).
+Server-initiated sampling and roots are *requests* — the server sends one and blocks for the answer — which needs a back-channel the sessionless protocol does not have. Unlike elicitation, generation has no multi-round-trip replacement (an agentic loop would exhaust the round-trip budget). The migration is to call an LLM directly from the server for generation, and to accept paths as tool arguments (or via `InputRequiredResult.input_requests`, which still carries a `ListRootsRequest`) for roots.
+
+**What is deliberately kept.** Client-side `Client(sampling_handler=..., roots=...)` and the provider handlers (anthropic/openai/google_genai) stay: a FastMCP client must still answer a legacy server's requests, and removing them would break interop with older servers. `docs/clients/sampling.mdx` and `docs/clients/roots.mdx` stay as real documentation. Logging is untouched — `ctx.log`/`info`/`debug`/`warning`/`error` are notifications that ride the request's own stream and work on every era.
+
+**Proxy relay.** `ProxyClient`'s default `roots` and `sampling_handler` are client-side handlers that relay a handshake-era backend's requests to the proxy's own front client. They are kept, because a proxy is a client to its backend and falls squarely under the interop guarantee above. They no longer route through the removed `Context` methods: both now call the SDK session directly (`ctx.session.list_roots()` / `ctx.session.create_message()`), an internal path with no public authoring surface. The relay is reachable only when both legs speak the handshake era.
+
+*Verify:* `fastmcp_slim/fastmcp/server/context.py` (no `sample`/`sample_step`/`list_roots`), `fastmcp_slim/fastmcp/server/server.py` (`_REMOVED_KWARGS`), `fastmcp_slim/fastmcp/server/providers/proxy.py` (`default_proxy_roots_handler`, `default_proxy_sampling_handler`), `docs/servers/sampling.mdx` (rewritten in place as the explainer), `tests/server/test_protocol_eras.py` (`test_removed_server_initiated_methods_are_absent`), `tests/server/providers/proxy/test_proxy_client.py` (relay still green).
+
+### `client.set_logging_level()` era-gated — Breaking (modern era)
+
+`logging/setLevel` asks a server to remember a level for the rest of the session, and it is absent from the `2026-07-28` method registry because that era has no session to remember it in. It previously surfaced the SDK's opaque "Method not found". `Client.set_logging_level()` now raises a `RuntimeError` naming the era and pointing at level-filtering in the client's `log_handler`; it is unchanged on handshake-era connections. It is never a silent no-op.
+
+*Verify:* `fastmcp_slim/fastmcp/client/client.py` (`set_logging_level`), `tests/server/test_protocol_eras.py` (`test_set_logging_level_is_era_gated_on_modern`).
### Push-feature degradation quality — Resolved (was sdk-feedback #10)
-On a `2026-07-28` connection the degradation error used to differ by feature: `ctx.list_roots` raised a clear `NoBackChannelError`, while `ctx.elicit` / `ctx.sample` surfaced a bare "Method not found" because those methods attach a `related_request_id` and reach client dispatch before failing. FastMCP now era-gates `ctx.elicit` and `ctx.sample`/`ctx.sample_step` to raise a clear, era-aware `ToolError` before the wire ("server-initiated sampling is not available on MCP 2026-07-28 connections…" and "elicitation via server-initiated requests is unavailable on 2026-07-28 connections."). The strict xfail that captured #10 is flipped to a passing test.
+On a `2026-07-28` connection `ctx.elicit` used to surface a bare "Method not found", because it attaches a `related_request_id` and reaches client dispatch before failing. FastMCP now era-gates `ctx.elicit` to raise a clear, era-aware `ToolError` before the wire ("elicitation via server-initiated requests is unavailable on 2026-07-28 connections."). The strict xfail that captured #10 is flipped to a passing test. The sampling half of #10 is moot: `ctx.sample` no longer exists.
-*Verify:* `tests/server/test_protocol_eras.py` (`test_elicit_sample_degradation_message_is_clear_on_modern`, now a real test), `server/context.py` (era gates).
+*Verify:* `tests/server/test_protocol_eras.py` (`test_elicit_degradation_message_is_clear_on_modern`, now a real test), `server/context.py` (era gate).
### Server-level cache hints (SEP-2549) — New (opt-in feature)
diff --git a/dev-docs/v4-notes/feature-program.md b/dev-docs/v4-notes/feature-program.md
index eb135f910..4e185d6dd 100644
--- a/dev-docs/v4-notes/feature-program.md
+++ b/dev-docs/v4-notes/feature-program.md
@@ -13,21 +13,15 @@ Code blocks marked as sketches show the *intended* API and do not resolve agains
## Sampling removal
-**Status: Deprecation and era-gating shipped (#4448); removal slated for 4.0.**
+**Status: Shipped in 4.0.**
-Sampling is the push-shaped API where a server borrows the client's model mid-call (`ctx.sample`, `ctx.sample_step`). The `2026-07-28` era removes server-initiated requests, so this API cannot work on modern connections. Background-task sampling is dead under v2 — a worker's back-channel is gone once the submitting request returns, and no sampling relay was ever built (sdk-feedback #9).
+Sampling was the push-shaped API where a server borrows the client's model mid-call (`ctx.sample`, `ctx.sample_step`). The `2026-07-28` era removes server-initiated requests, so it cannot work on modern connections, and `Client`'s flip to `mode="auto"` made a modern connection the default — the era gate had become the default experience rather than an edge case. Background-task sampling was dead under v2 in any event: a worker's back-channel is gone once the submitting request returns, and no relay was ever built (sdk-feedback #9).
-The plan is Option A: **deprecate the push-sampling API now and remove it in the 4.0 release.** The first two steps shipped in #4448:
+Deprecation and era-gating shipped in #4448. The removal completes the plan: `ctx.sample`, `ctx.sample_step`, `ctx.list_roots`, `server/sampling/` (including `SamplingTool` and structured-result sampling), `FastMCP(sampling_handler=..., sampling_handler_behavior=...)`, and `examples/sampling/` are all gone. The server-authoring API is now the modern protocol's API, with nothing in it that only works against old clients.
-- **Done:** `ctx.sample` / `ctx.sample_step` emit a `FastMCPDeprecationWarning` (once per process, gated on `settings.deprecation_warnings`).
-- **Done:** both are era-gated to raise a clear, era-aware `ToolError` on `2026-07-28` before the wire, which also fixed the opaque "Method not found" of sdk-feedback #10.
-- **Pending 4.0:** remove `ctx.sample`, `ctx.sample_step`, `server/sampling/`, `SamplingTool`, and structured-result sampling.
+The migration story is honest: there is **no drop-in**. The guidance is architectural — call an LLM from your server directly, with your own API key, rather than borrowing the client's model. For roots, take paths as tool arguments or ask through the guard pattern, whose `input_requests` map still carries a `ListRootsRequest`.
-The migration story is honest: there is **no drop-in** on modern connections. The guidance is architectural — call an LLM from your server directly, with your own API key, rather than borrowing the client's model. That shift is the real answer, and it is why the removal justifies a major version.
-
-The client-side provider handlers (Anthropic, OpenAI, Google GenAI) are **retained** regardless: MRTR needs them to answer sampling input-requests from the client side. What is removed is the server-side push emitter, which the SDK never built for the modern era.
-
-Sampling still functions on the legacy eras. Users also see an SDK-level `MCPDeprecationWarning` on ordinary `ctx.sample` usage (the SDK deprecated the capability wire-side per SEP-2577). FastMCP's own deprecation — the warning with migration guidance, plus the era-gating — shipped in #4448; only the final removal remains for 4.0.
+The client-side provider handlers (Anthropic, OpenAI, Google GenAI) and `Client(sampling_handler=..., roots=...)` are **retained**: a FastMCP client still has to answer a legacy server's requests, and MRTR needs them from the client side. What is removed is the server-side push emitter. `ProxyClient`'s default relay handlers are retained for the same interop reason and now call the SDK session directly.
## MRTR elicitation
diff --git a/dev-docs/v4-notes/index.md b/dev-docs/v4-notes/index.md
index f09d19dcd..282c37af2 100644
--- a/dev-docs/v4-notes/index.md
+++ b/dev-docs/v4-notes/index.md
@@ -16,7 +16,7 @@ FastMCP v4.0 is an engine swap. Three forces drive the major version:
**Protocol version 2026-07-28.** The SDK v2 serves multiple protocol eras from one server. Alongside the session-based handshake eras, it introduces the sessionless `2026-07-28` era, which discovers capabilities through `server/discover` and removes server-initiated requests (SEP-2577). This formally supersedes FastMCP's earlier "latest protocol only" stance: a single server now works with clients across the protocol transition.
-**Sampling removal.** The `2026-07-28` era removes the server's ability to push a request back to the client mid-call. That takes the push-shaped sampling API (`ctx.sample`, `ctx.sample_step`) off the table on modern connections. Rather than leave it half-working, v4 deprecates it now and removes it in the 4.0 release — a real architectural shift for servers that borrowed the client's model, and one that justifies the major bump.
+**Sampling and roots removed from the server API.** The `2026-07-28` era removes the server's ability to push a request back to the client mid-call, which takes `ctx.sample`, `ctx.sample_step`, and `ctx.list_roots` off the table. Rather than leave them half-working against old clients only, 4.0 removes them from the server API entirely — a real architectural shift for servers that borrowed the client's model, and one that justifies the major bump. Client-side handlers stay, because a modern client still has to answer a legacy server.
## Release strategy
diff --git a/docs/clients/sampling.mdx b/docs/clients/sampling.mdx
index 1b83ae45d..fbbf4ff6d 100644
--- a/docs/clients/sampling.mdx
+++ b/docs/clients/sampling.mdx
@@ -11,10 +11,12 @@ import { VersionBadge } from "/snippets/version-badge.mdx";
Use this when you need to respond to server requests for LLM completions.
-MCP servers can request LLM completions from clients during tool execution. This enables servers to delegate AI reasoning to the client, which controls which LLM is used and how requests are made.
+A handshake-era MCP server can request an LLM completion from its client during tool execution, delegating the reasoning to whichever model the client controls. Answering those requests is what a sampling handler does, and it is how a FastMCP client stays interoperable with servers built before the protocol changed.
**Sampling requires the older MCP protocol.** A server requests sampling by sending a request down to the client, and protocol version `2026-07-28` removed the server's ability to do that. Clients default to `mode="auto"`, which negotiates the newest version both sides support, so every example on this page passes `mode="legacy"`. See [protocol negotiation](/clients/client#protocol-negotiation).
+
+This page is about the client answering. Writing a FastMCP *server* is the other direction, and there is no `ctx.sample()` there — see [Sampling](/servers/sampling) under Servers for why, and for calling an LLM from your own server instead.
## Handler Template
diff --git a/docs/deployment/http.mdx b/docs/deployment/http.mdx
index 3eb7bf10b..573f726d1 100644
--- a/docs/deployment/http.mdx
+++ b/docs/deployment/http.mdx
@@ -698,7 +698,7 @@ When deploying FastMCP behind a load balancer or running multiple server instanc
#### Understanding Sessions
-By default, FastMCP's Streamable HTTP transport maintains server-side sessions. Sessions enable stateful MCP features like [elicitation](/servers/elicitation) and [sampling](/servers/sampling), where the server needs to maintain context across multiple requests from the same client.
+By default, FastMCP's Streamable HTTP transport maintains server-side sessions. Sessions carry the handshake-era back-channel that server-initiated requests like [elicitation](/servers/elicitation) depend on, where the server needs to maintain context across multiple requests from the same client.
This works perfectly for single-instance deployments. However, sessions are stored in memory on each server instance, which creates challenges when scaling horizontally.
diff --git a/docs/getting-started/upgrading/from-fastmcp-3.mdx b/docs/getting-started/upgrading/from-fastmcp-3.mdx
index a076ef30c..b2365e89b 100644
--- a/docs/getting-started/upgrading/from-fastmcp-3.mdx
+++ b/docs/getting-started/upgrading/from-fastmcp-3.mdx
@@ -227,37 +227,40 @@ The camelCase bridge is a migration aid, not a permanent fixture. It works today
## SDK deprecation warnings you may see
-Ordinary use of `ctx.info` (client logging) and `ctx.sample` now emits an SDK-level `MCPDeprecationWarning`:
+Ordinary use of `ctx.info` (client logging) emits an SDK-level `MCPDeprecationWarning`:
```
-The logging/sampling capability is deprecated as of 2026-07-28 (SEP-2577)
+The logging capability is deprecated as of 2026-07-28 (SEP-2577)
```
-These warnings come from the MCP SDK, not from FastMCP. For logging they are benign: `ctx.info` keeps working on session-based connections exactly as the protocol table below describes, and the SDK is only signaling the protocol's direction. For sampling, FastMCP additionally emits its own `FastMCPDeprecationWarning`: `ctx.sample` and `ctx.sample_step` are deprecated and slated for removal, so treat that warning as a prompt to migrate to server-side LLM calls rather than as informational.
+The warning comes from the MCP SDK, not from FastMCP, and it is benign. `ctx.info` and the rest of the logging methods keep working on every era, including the modern one — a log message is a *notification*, which rides the response stream the caller already opened. The SDK is signaling the protocol's direction for the capability declaration, not the notification itself.
## Protocol version support
FastMCP servers built on the SDK v2 serve multiple protocol eras from the same server. The SDK negotiates the era each client speaks: the sessionless `2026-07-28` era (which discovers capabilities through `server/discover`) and earlier session-based handshake versions are all handled simultaneously. This formally supersedes FastMCP's earlier "latest protocol only" stance — a single server now works with clients across the protocol transition.
-Not every Context feature is available on every era yet. The imperative push APIs that call back into the client mid-execution — `ctx.elicit`, `ctx.sample`, and `ctx.list_roots` — depend on the session-based back-channel of the earlier eras, so on a `2026-07-28` connection they raise a clear, era-aware error rather than reaching the client. Elicitation itself still reaches the user on the modern era, through the guard pattern: a tool *returns* an `InputRequiredResult` describing what it needs, and the client answers with a fresh call (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). Logging notifications and the request/response features flow on every era.
+FastMCP 4 is a modern MCP toolkit, so the server API is the modern protocol's API. Where a capability survived the transition in a different shape, FastMCP carries it across in that shape. Where the protocol removed a capability outright, FastMCP 4 does not carry a version of it that only works on old connections: **`ctx.sample()`, `ctx.sample_step()`, and `ctx.list_roots()` are gone from `Context` entirely**, along with the `sampling_handler=` and `sampling_handler_behavior=` arguments to `FastMCP()`. Calling them raises `AttributeError` on every era, not an era-specific runtime error, and `FastMCP(sampling_handler=...)` raises `TypeError` naming the migration.
-Sampling is the exception that does not come back, and the reason is the protocol rather than an unfinished FastMCP feature. SEP-2577 deprecated server-initiated sampling, so `ctx.sample` and `ctx.sample_step` are **deprecated** and will be removed in a future FastMCP release. Elicitation moved to the guard pattern because the modern protocol still carries elicitation requests; sampling has no equivalent path because the protocol deprecated the pattern itself. The migration is to call an LLM directly from your server rather than borrowing the client's model. See [Sampling](/servers/sampling) for details.
+This is a deliberate stance rather than an unfinished port. Server-initiated sampling and roots are *requests*: the server sends one and blocks for an answer, which needs a live back-channel the sessionless protocol does not have. Since `fastmcp.Client` now negotiates the modern protocol by default, keeping these methods would mean shipping an API whose default outcome is a runtime failure. Elicitation is the one server-initiated capability that survives, because the modern protocol carries it in a new shape: a tool *returns* an `InputRequiredResult` describing what it needs, and the client answers with a fresh call (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)).
+
+Migrating is direct in both cases. For sampling, [call an LLM from your server](/servers/sampling) with your own API key — your tool then behaves the same for every client, including the many that never implemented sampling. For roots, accept the paths you need as tool arguments, or ask for them through the same guard pattern, whose `input_requests` map carries a roots request alongside elicitation.
| Context feature | Earlier eras (session-based) | `2026-07-28` (sessionless) |
| --- | --- | --- |
| `ctx.info` / logging notifications | Supported | Supported |
| Tools, resources, prompts, completions | Supported | Supported |
| `ctx.elicit` | Supported | Use the guard pattern (return `InputRequiredResult`) |
-| `ctx.sample` / `ctx.sample_step` | Supported (deprecated) | Removed — call an LLM server-side |
-| `ctx.list_roots` | Supported | Via the guard pattern (`input_requests` carries roots requests) |
+| `ctx.sample` / `ctx.sample_step` | Removed from the API — call an LLM server-side | Removed from the API — call an LLM server-side |
+| `ctx.list_roots` | Removed from the API — take paths as arguments, or use the guard pattern | Removed from the API — take paths as arguments, or use the guard pattern |
+| `client.set_logging_level()` | Supported | Raises — `logging/setLevel` needs session state the era lacks |
| `Middleware.on_initialize` | Runs on connect | Never runs — there is no `initialize` handshake |
| Session state (`ctx.set_state` across calls) | Persists for the session | Does not persist — every request is a fresh connection |
| Background tasks (`task=True`) | Runs synchronously — never tasked | Supported via the tasks extension |
-If your tools rely on `ctx.elicit` or `ctx.list_roots`, they continue to work against clients on the earlier eras; on the modern era, reach for the guard pattern instead (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). Sampling is deprecated on every era and will not return on modern connections — migrate those tools to server-side LLM calls.
-
Two of these bite by default now, because **`fastmcp.Client` defaults to `mode="auto"`** in v4 — an ordinary `Client(server)` negotiates the newest protocol both sides share, which against a FastMCP server is the sessionless `2026-07-28` era. On that era there is no `initialize` handshake, so a `Middleware.on_initialize` hook never runs; and each request is a fresh connection, so state written with `ctx.set_state` in one call is not visible in the next. A server that gates access in `on_initialize` or relies on per-session state must keep its clients on the session-based era. The narrow escape is per-client: `Client(server, mode="legacy")`. The durable, server-side answer is to declare the versions the server actually serves so a modern client is refused at connect time rather than silently losing those features — see the server's protocol-version restriction (added alongside this change).
+The client side is unaffected by any of this. A `fastmcp.Client` still answers a legacy server's sampling and roots requests through `sampling_handler=` and `roots=` — see [client sampling](/clients/sampling) and [client roots](/clients/roots) — because a modern client still has to interoperate with servers built before the protocol changed.
+
## Upgrade checklist
Most servers upgrade untouched. Work down this list to find the ones that don't:
@@ -265,11 +268,12 @@ Most servers upgrade untouched. Work down this list to find the ones that don't:
1. **Bump your environment.** Raise any pin below `pydantic>=2.12`; upgrade FastAPI if your resolver complains about Starlette `<1.0.1`.
2. **Fix imports that moved out.** Replace `from mcp.types import X` with `from mcp_types import X`, and update any import from the [removed modules](#moved-imports) (`fastmcp.server.proxy`, `fastmcp.server.openapi`, `fastmcp.server.apps`, the `fastmcp.tools.tool` / `resources.resource` / `prompts.prompt` component shims).
3. **Update removed server APIs.** Swap `as_proxy` → `create_proxy`, `import_server` → `mount`, `mount(prefix=)` → `mount(namespace=)`, and the [other removed methods and keywords](#removed-server-methods-and-mount-keywords).
-4. **Update removed tool parameters.** Replace tool `serializer=` (return a `ToolResult`), `exclude_args=` (use `Depends()`), and `StreamableHttpTransport(sse_read_timeout=)`.
-5. **Fix `McpError` construction.** Positional `McpError(ErrorData(...))` becomes keyword `McpError(code=..., message=...)`. Catching is unchanged.
-6. **Move httpx to httpx2.** Grep for `except httpx.` and for custom `httpx_client_factory` / `httpx.Auth` objects handed to FastMCP, and swap the import to `httpx2`.
-7. **Decide the client era.** `Client` now defaults to `mode="auto"`. If a server relies on `on_initialize` or per-session state, keep its clients on `mode="legacy"` or restrict the server's served protocol versions.
-8. **Verify behavior changes.** Confirm templated resources that legitimately accept `..` or absolute paths are exempted, and update any client that matched the old `-32002` resource-not-found code.
-9. **Run with the camelCase bridge off.** Set `mcp_camelcase_compat = False` (or `FASTMCP_MCP_CAMELCASE_COMPAT=false`) in CI to surface every remaining camelCase read as a hard `AttributeError` before the shims are removed.
+4. **Replace `ctx.sample` and `ctx.list_roots`.** Both are gone from `Context`, as are `FastMCP(sampling_handler=...)` and `sampling_handler_behavior=`. Call an LLM directly from your server for generation; take file paths as tool arguments for roots.
+5. **Update removed tool parameters.** Replace tool `serializer=` (return a `ToolResult`), `exclude_args=` (use `Depends()`), and `StreamableHttpTransport(sse_read_timeout=)`.
+6. **Fix `McpError` construction.** Positional `McpError(ErrorData(...))` becomes keyword `McpError(code=..., message=...)`. Catching is unchanged.
+7. **Move httpx to httpx2.** Grep for `except httpx.` and for custom `httpx_client_factory` / `httpx.Auth` objects handed to FastMCP, and swap the import to `httpx2`.
+8. **Decide the client era.** `Client` now defaults to `mode="auto"`. If a server relies on `on_initialize` or per-session state, keep its clients on `mode="legacy"` or restrict the server's served protocol versions.
+9. **Verify behavior changes.** Confirm templated resources that legitimately accept `..` or absolute paths are exempted, and update any client that matched the old `-32002` resource-not-found code.
+10. **Run with the camelCase bridge off.** Set `mcp_camelcase_compat = False` (or `FASTMCP_MCP_CAMELCASE_COMPAT=false`) in CI to surface every remaining camelCase read as a hard `AttributeError` before the shims are removed.
The executable version of this checklist lives in [`tests/test_upgrade_from_v3.py`](https://github.com/PrefectHQ/fastmcp/blob/main/tests/test_upgrade_from_v3.py): it builds representative 3.x-style servers and asserts they run unchanged, and pins every removed surface to the exact error it now raises.
diff --git a/docs/getting-started/whats-new.mdx b/docs/getting-started/whats-new.mdx
index 24eee9ac5..8fe57e719 100644
--- a/docs/getting-started/whats-new.mdx
+++ b/docs/getting-started/whats-new.mdx
@@ -25,7 +25,7 @@ A FastMCP 4 server answers clients across the protocol transition from one deplo
The same negotiation runs from the client, and its default flipped. A plain `Client(url)` now probes for the modern protocol and adopts it when the server offers it, falling back to the handshake otherwise — where every earlier FastMCP version pinned the handshake outright. That flip is what brings the modern capabilities within reach of ordinary client code: a task-enabled tool hands back a handle to poll, and multi-round-trip elicitation resolves across successive requests, neither requiring the caller to opt in. Set `mode="legacy"` to pin the handshake when you need the session-based back-channel or the classic `initialize` result. See [Protocol negotiation](/clients/client#protocol-negotiation).
-The modern protocol is sessionless, so it drops the server's ability to call back into the client mid-request (SEP-2577). Imperative `ctx.elicit` and `ctx.list_roots` move to a request-shaped pattern on modern connections, and server-initiated sampling — which has no such replacement — is [deprecated](/servers/sampling). Everything else about writing a server is unchanged.
+The modern protocol is sessionless, so it drops the server's ability to call back into the client mid-request (SEP-2577), and FastMCP 4's server API reflects that rather than papering over it. `ctx.elicit` moves to a request-shaped pattern that works on modern connections. `ctx.sample`, `ctx.sample_step`, and `ctx.list_roots` are not in the API at all — the capabilities they wrapped no longer exist in the protocol FastMCP 4 targets, so shipping methods that only work against old clients would be shipping a trap. [Call an LLM from your server](/servers/sampling) for generation, and take file paths as tool arguments for roots. Logging is untouched: `ctx.info` and its siblings are notifications, which ride the response stream and reach the client on every era. Everything else about writing a server is unchanged.
## State without a session
diff --git a/docs/servers/context.mdx b/docs/servers/context.mdx
index 48fcb74d0..bd0a4faa2 100644
--- a/docs/servers/context.mdx
+++ b/docs/servers/context.mdx
@@ -21,7 +21,6 @@ The `Context` object provides a clean interface to access MCP features within yo
- **Progress Reporting**: Update the client on the progress of long-running operations
- **Resource Access**: List and read data from resources registered with the server
- **Prompt Access**: List and retrieve prompts registered with the server
-- **LLM Sampling**: Request the client's LLM to generate text based on provided messages
- **User Elicitation**: Request structured input from users during tool execution
- **Request State**: Pass values and non-serializable resources between middleware and handlers within a request (for state that persists across requests, see [Session State](/servers/sessions))
- **Session Visibility**: [Control which components are visible](/servers/visibility#per-session-visibility) to the current session
@@ -152,17 +151,13 @@ if result.action == "accept":
See [User Elicitation](/servers/elicitation) for detailed examples and supported response types.
-### LLM Sampling
+### Server-initiated requests
-
+
+`Context` has no `sample()` or `list_roots()`. Both were server→client *requests*, and the modern MCP protocol has no channel to carry them ([SEP-2577](/servers/sampling)). For generation, [call an LLM directly from your server](/servers/sampling). For roots, accept paths as tool arguments, or ask for them through the [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol), which carries a roots request in `input_requests`.
-Request the client's LLM to generate text based on provided messages, useful for leveraging AI capabilities within your tools.
-
-```python
-response = await ctx.sample("Analyze this data", temperature=0.7)
-```
-
-See [LLM Sampling](/servers/sampling) for comprehensive usage and advanced techniques.
+Logging is unaffected — `ctx.info()` and friends are *notifications*, which ride the response stream and work on every protocol era.
+
### Progress Reporting
diff --git a/docs/servers/elicitation.mdx b/docs/servers/elicitation.mdx
index 9b7b0c164..9712db02c 100644
--- a/docs/servers/elicitation.mdx
+++ b/docs/servers/elicitation.mdx
@@ -545,7 +545,7 @@ If you need to support both eras, branch on `ctx.protocol_version`: return an `I
Elicitation is the most common request to carry this way, and **roots** requests work identically — the `input_requests` map holds them the same way, and each answer comes back in `ctx.input_responses` under its key (an `ElicitResult` or `ListRootsResult`). See [Client Roots](/clients/roots) for what a roots request contains. `fastmcp.Client` answers both from the handlers you already configured, so a guard tool that mixes them needs no extra client wiring.
-The map can structurally hold a **sampling** request too (its answer would be a `CreateMessageResult`), but SEP-2577 deprecated server-initiated sampling on the modern protocol, so reach for a direct server-side LLM call instead of routing generation through a guard round. See [Sampling](/servers/sampling).
+The map can structurally hold a **sampling** request too (its answer would be a `CreateMessageResult`), but MCP removed server-initiated sampling as a pattern rather than only one spelling of it, so generation belongs in your server. See [Sampling](/servers/sampling) for how to call an LLM directly.
### Middleware
diff --git a/docs/servers/sampling.mdx b/docs/servers/sampling.mdx
new file mode 100644
index 000000000..5aeacd413
--- /dev/null
+++ b/docs/servers/sampling.mdx
@@ -0,0 +1,66 @@
+---
+title: Sampling
+sidebarTitle: Sampling
+description: Server-initiated sampling is not part of FastMCP 4 — here is why, and what to build instead.
+icon: robot
+---
+
+FastMCP 4 targets the modern MCP protocol, and that protocol has no channel for a server to send a request to a client. A tool cannot pause mid-execution to borrow the caller's model and wait for a completion, so server-initiated sampling is not part of the FastMCP 4 server API. There is no `ctx.sample()` and no server-side sampling handler. Generation belongs to your server now: you call an LLM with your own credentials, the same way you would call any other service.
+
+This follows the protocol rather than getting ahead of it. MCP removed server-initiated requests in the `2026-07-28` revision ([SEP-2577](https://modelcontextprotocol.io/community/sep-guidelines)), and FastMCP 4's client negotiates that revision by default. Keeping `ctx.sample()` around would mean shipping a method whose ordinary, default outcome is a runtime error.
+
+## Requests and notifications
+
+The distinction that makes this make sense is between *asking* and *telling*.
+
+A notification is fire-and-forget. Your server emits it and moves on, and it travels down the response stream the caller already opened for the request in flight. Nothing has to be held open on the server's behalf, so notifications survive the move to a stateless protocol untouched. This is why [logging](/servers/logging) still works exactly as it always has: `ctx.info()`, `ctx.debug()`, and the rest reach the client mid-call on every protocol era.
+
+```python
+from fastmcp import Context, FastMCP
+
+mcp = FastMCP("Reports")
+
+
+@mcp.tool
+async def build_report(rows: int, ctx: Context) -> str:
+ await ctx.info(f"Processing {rows} rows")
+ return "done"
+```
+
+Sampling is the other kind. It is a *request* — the server sends `sampling/createMessage` and then blocks until an answer comes back the other way. That requires a live, addressable connection the server can reach into, which is precisely the thing a stateless protocol does not have. There is no version of sampling that fits, which is why it has no replacement in the way elicitation does. Elicitation moved to the [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol), where a tool *returns* a description of what it needs and the client answers with a fresh call; generation does not decompose into rounds that way, because an agentic loop would spend the round-trip budget several times over.
+
+## Calling an LLM directly
+
+Your server calls the model. Hold a provider API key in your server's environment, create the client once at module scope so connections are reused across calls, and generate inside the tool. The result is a plain async function call with no protocol involvement, which also means you choose the model, control the prompt, see the token usage, and can test the tool without a client attached.
+
+```python
+import anthropic
+from fastmcp import FastMCP
+
+mcp = FastMCP("Summarizer")
+llm = anthropic.AsyncAnthropic()
+
+
+@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
+```
+
+Any provider SDK works the same way — swap the client and the call, and the tool signature is unchanged. Because generation is now ordinary application code, the surrounding concerns become ordinary too: retries, timeouts, caching, and cost accounting are yours to place where you want them rather than negotiated across a protocol boundary.
+
+The trade this makes is explicit. Sampling let a server borrow the caller's model and the caller's bill; calling directly means you supply the key and pay for the tokens. In exchange your tool behaves identically for every client, including the many that never implemented sampling at all.
+
+## Clients answering servers
+
+The client half of sampling is unaffected. A `fastmcp.Client` connecting to a handshake-era server may still receive `sampling/createMessage` requests from it, and passing `sampling_handler=` is how you answer them — see [Sampling](/clients/sampling) under Clients. That path exists for interoperating with older servers and has nothing to do with authoring one.
+
+
+Servers on FastMCP 3 still have `ctx.sample()` and `ctx.sample_step()`, documented in the [FastMCP 3 sampling guide](/v3/servers/sampling). Nothing changes for them until they upgrade.
+
diff --git a/docs/servers/server.mdx b/docs/servers/server.mdx
index 9f6dbe8fa..082830b12 100644
--- a/docs/servers/server.mdx
+++ b/docs/servers/server.mdx
@@ -211,19 +211,9 @@ These parameters tune how the server processes requests and communicates with cl
-### Handlers and Storage
-
-These parameters provide custom handlers for MCP capabilities and persistent storage for session state.
+### Storage
-
- Custom handler for MCP sampling requests (server-initiated LLM calls). See [Sampling](/servers/sampling) for details
-
-
-
- When `"fallback"`, the sampling handler is used only when no tool-specific handler exists. When `"always"`, this handler is used for all sampling requests
-
-
Persistent key-value store for session state that survives across requests. Defaults to an in-memory store. Provide a custom implementation for persistence across server restarts