Skip to content

Commit b1e0586

Browse files
authored
Remove server-initiated sampling and roots from the server API (#4648)
* Remove server-initiated sampling and roots from the server API Deletes fastmcp/server/sampling/, Context.sample/sample_step/list_roots, and FastMCP(sampling_handler=). The proxy's handshake-era relay now reaches the front session through the SDK directly. * Update tests for the removed sampling and roots server API * Era-gate client.set_logging_level on modern connections * Document that server-initiated sampling and roots are not in FastMCP 4 * Silence ty deprecation diagnostics and drop stale sampling doc mentions * Baseline tools-call-sampling; fix removal leftovers flagged by ruff * Document sampling handlers on both protocol routes; qualify log-level override * Docs: sampling and roots work on modern via the guard pattern The imperative ctx.sample()/ctx.list_roots() stay removed, but both capabilities survive as input-required requests, as tests/conformance exercises on 2026-07-28. Direct LLM calls remain the recommendation for generation; roots has no round-trip-budget objection. * Change register: record the guard route for sampling and roots * Editorial pass on the sampling and roots docs * Flag the sampling removal at the top of the page * Restore the version badge and point sampling users at 3.x * Keep the sampling conformance scenario live; fix roots example URIs * Upgrade guide: staying on 3.x is an option for sampling servers * Elicitation: state the era split once, not twice
2 parents 9ea5a40 + b2b2b0f commit b1e0586

46 files changed

Lines changed: 417 additions & 5960 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dev-docs/v4-notes/change-register.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -432,31 +432,44 @@ The push-style Context features that require the server to call back into the cl
432432
| `ctx.info` / logging notifications | Supported | Supported |
433433
| Tools, resources, prompts, completions | Supported | Supported |
434434
| `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) |
435-
| `ctx.sample` / `ctx.sample_step` | Supported (deprecated) | Removed — call an LLM server-side |
436-
| `ctx.list_roots` | Supported | Via the [guard pattern](https://gofastmcp.com/servers/elicitation#elicitation-on-the-modern-protocol) |
435+
| `ctx.sample` / `ctx.sample_step` | Not in the API | Not in the API — call an LLM server-side |
436+
| `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) |
437+
| `client.set_logging_level()` | Supported | Raises — `logging/setLevel` is absent from the era's registry |
437438
| Background tasks (`task=True`) | Runs synchronously — never tasked | Supported via the tasks extension |
438439

439-
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).
440+
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).
440441

441-
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.
442+
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.
442443

443444
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).
444445

445446
*Verify:* `docs/getting-started/upgrading/from-fastmcp-3.mdx` (the published matrix and SDK-warning note), `tests/server/test_protocol_eras.py`.
446447

447-
### Sampling deprecated, era-gated — Deprecated
448+
### Server-initiated sampling and roots removed from the server API — Breaking
448449

449-
`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.
450+
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.
450451

451-
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.
452+
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`.
452453

453-
*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).
454+
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. What the protocol removed is the *pushing*, not the asking: both capabilities remain reachable through the guard pattern, where a tool returns an `InputRequiredResult` whose `input_requests` map carries a `CreateMessageRequest` or a `ListRootsRequest`, the client answers it, and the tool re-runs and reads `ctx.input_responses`. `Client._drive_input_required()` dispatches those to the same `sampling_handler` / `roots` handler a handshake-era server would have pushed to, and `tests/conformance/server.py` exercises both routes. For roots that guard round is the recommended modern path. For generation it is available but usually the wrong tool — each round is a full request-response cycle, so an agentic loop exhausts the round-trip budget — and the recommended migration stays a direct LLM call from the server.
455+
456+
**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.
457+
458+
**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.
459+
460+
*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).
461+
462+
### `client.set_logging_level()` era-gated — Breaking (modern era)
463+
464+
`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.
465+
466+
*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`).
454467

455468
### Push-feature degradation quality — Resolved (was sdk-feedback #10)
456469

457-
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.
470+
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.
458471

459-
*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).
472+
*Verify:* `tests/server/test_protocol_eras.py` (`test_elicit_degradation_message_is_clear_on_modern`, now a real test), `server/context.py` (era gate).
460473

461474
### Server-level cache hints (SEP-2549) — New (opt-in feature)
462475

dev-docs/v4-notes/feature-program.md

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,15 @@ Code blocks marked as sketches show the *intended* API and do not resolve agains
1313

1414
## Sampling removal
1515

16-
**Status: Deprecation and era-gating shipped (#4448); removal slated for 4.0.**
16+
**Status: Shipped in 4.0.**
1717

18-
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).
18+
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).
1919

20-
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:
20+
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.
2121

22-
- **Done:** `ctx.sample` / `ctx.sample_step` emit a `FastMCPDeprecationWarning` (once per process, gated on `settings.deprecation_warnings`).
23-
- **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.
24-
- **Pending 4.0:** remove `ctx.sample`, `ctx.sample_step`, `server/sampling/`, `SamplingTool`, and structured-result sampling.
22+
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`.
2523

26-
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.
27-
28-
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.
29-
30-
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.
24+
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.
3125

3226
## MRTR elicitation
3327

dev-docs/v4-notes/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ FastMCP v4.0 is an engine swap. Three forces drive the major version:
1616

1717
**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.
1818

19-
**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.
19+
**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.
2020

2121
## Release strategy
2222

docs/clients/client.mdx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,11 @@ Set `mode="legacy"` to force the initialize handshake. This behaves identically
185185
client = Client("https://example.com/mcp", mode="legacy")
186186
```
187187

188-
Legacy mode is also what you need for the capabilities that depend on a live session between client and server. The handshake opens a persistent back-channel the server can push requests down, and the modern era removed it. Pin `mode="legacy"` when your code relies on any of these:
189-
190-
- **[Sampling](/clients/sampling)** — server-initiated LLM completion requests
191-
- **[Roots](/clients/roots)** — server-initiated requests for the client's roots
192-
- **[Elicitation](/clients/elicitation)** — server-initiated requests for user input, which modern connections replace with [input-required rounds](/clients/elicitation#input-required-rounds)
193-
- `client.ping()` and `transport.get_session_id()`
188+
Legacy mode is also what carries the *pushed* form of a server's requests. The handshake opens a persistent back-channel down which a server can send a sampling, roots, or elicitation request mid-call, and the modern era removed it. Your handlers are unaffected by that: a [sampling](/clients/sampling), [roots](/clients/roots), or [elicitation](/clients/elicitation) handler you register answers a modern server's [input-required rounds](/clients/elicitation#input-required-rounds) from the same registration. Pin `mode="legacy"` when you connect to a server that pushes, or when your code calls `client.ping()` or `transport.get_session_id()`, which need the session the modern era does not open.
194189

195190
Conversely, [background tasks](/clients/tasks) are **modern-only**: the tasks capability is negotiated over `2026-07-28` connections, so `mode="legacy"` never triggers one and a task-enabled tool just runs synchronously.
196191

197-
A FastMCP server serves both eras, so a default client negotiates the modern one and these raise an era-specific error. Pinning the handshake restores them.
192+
A FastMCP server serves both eras, so a default client negotiates the modern one and the session-dependent calls raise an era-specific error there. Pinning the handshake restores them.
198193

199194
You can also pin a specific modern protocol version to adopt it directly, without a discovery probe:
200195

@@ -342,7 +337,7 @@ See [Prompts](/clients/prompts) for detailed documentation including argument se
342337

343338
The client supports callback handlers for advanced server interactions. These let you respond to server-initiated requests and receive notifications.
344339

345-
Sampling, elicitation, and roots are all server-initiated, so they belong to the handshake era described under [protocol negotiation](#protocol-negotiation). A default client negotiates the newest era both peers share, where the server has no back-channel to push those requests down, so an example that exercises them pins `mode="legacy"`. Logging and progress arrive as notifications on the response stream and work in either era.
340+
Sampling, elicitation, and roots are the requests a server makes of the client. A server reaches your handler by whichever route its [era](#protocol-negotiation) allows — pushed down the open session on the handshake, returned as an input-required result on the modern protocol — and both routes dispatch to the same handler, so one registration covers both. Logging and progress arrive as notifications on the response stream and work in either era.
346341

347342
```python
348343
from fastmcp import Client
@@ -360,7 +355,6 @@ async def sampling_handler(messages, params, context):
360355

361356
client = Client(
362357
"my_mcp_server.py",
363-
mode="legacy",
364358
log_handler=log_handler,
365359
progress_handler=progress_handler,
366360
sampling_handler=sampling_handler,

0 commit comments

Comments
 (0)