---
title: Feature Program
---

The migration is the foundation. The forward v4 program is a sequence of post-merge PRs that build on it. Several have now merged. Each feature below carries an explicit status:

- **Shipped** — merged to `main`, with the PR cited.
- **Designed** — the approach is settled and an API sketch exists; implementation has not started.
- **Planned** — the shape is agreed but design details remain open.
- **Not started** — identified as v4 scope, not yet designed.

Code blocks marked as sketches show the *intended* API and do not resolve against the current tree.

## Sampling: deprecate now, remove in 4.0

**Status: Deprecation and era-gating shipped (#4448); removal slated for 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).

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:

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

## MRTR elicitation

**Status: Guard form shipped (4.0). Declarative `Resolve` layer designed.**

Elicitation survives the modern era through multi-round-trip (MRTR). The 2026 wire envelope carries elicitation as a multi-round input-request: a tool returns an `InputRequiredResult` and re-runs per round, each round a complete request→response cycle. Imperative `ctx.elicit` relies on the session back-channel, which is gone on `2026-07-28` foreground calls; on the modern era, elicitation is reachable through MRTR instead.

The **guard form** of this is shipped in 4.0 (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)): a tool returns an `InputRequiredResult` and reads the client's answers off `ctx.input_responses` / `ctx.request_state`, re-running each round. It mirrors the SDK's base guard model exactly — no FastMCP-invented DX, the framework owns `request_state` sealing, and returning this result on a handshake-era connection produces a clear era error.

What remains is the declarative `Resolve(...)` layer that sits *on top of* that shipped primitive. It is designed, not built: a new `fastmcp.elicitation` module — `Resolve`, `Elicit`, and `ElicitationResult` — thin wrappers over the SDK's resolver, wired into FastMCP's own tool layer (FastMCP tools do not inherit the SDK's auto-resolver wiring). It would detect `Annotated[_, Resolve(...)]` parameters, build resolver plans, and return the SDK's `InputRequiredResult` instead of the tool body on the first round.

Imperative `ctx.elicit` is **not** re-plumbed to survive the modern era. It works on the legacy eras through the session back-channel, and on `2026-07-28` foreground calls it is era-gated to raise a clear error (shipped in #4448) pointing at the guard form. The earlier plan to keep imperative `ctx.elicit` alive on modern connections through a background-task relay is dead twice over: the guard model shipped in its place, and the 2025 task machinery the relay depended on is slated for removal (see [Known Gaps](/development/v4-notes/known-gaps#the-xfail-register)).

The intended declarative DX (sketch — the module does not exist yet):

```python test="skip"
from typing import Annotated

from pydantic import BaseModel

from fastmcp import FastMCP, Context
from fastmcp.elicitation import Resolve, Elicit, ElicitationResult

mcp = FastMCP("shipping")


class Address(BaseModel):
    street: str
    city: str
    zip: str


async def ask_address(ctx: Context) -> Elicit[Address]:
    return Elicit("Where should we ship this order?", Address)


@mcp.tool
async def create_shipment(
    order_id: str,
    address: Annotated[Address, Resolve(ask_address)],  # unwrapped; decline -> ToolError
) -> str:
    return f"Shipping {order_id} to {address.city}"


@mcp.tool
async def maybe_ship(
    order_id: str,
    address: Annotated[ElicitationResult[Address], Resolve(ask_address)],  # full outcome
) -> str:
    if address.action != "accept":
        return "cancelled"
    return f"Shipping {order_id} to {address.data.city}"
```

The FastMCP client already dispatches input-requests through its elicitation callback; the remaining declarative work confirms the FastMCP client drives the input-required driver the way the SDK's own client does.

The divergence between elicitation and sampling on 2026 comes down to one fact: the SDK built the server-side emitter for elicitation (`Elicit`/`Resolve`) and not for sampling. The wire carries all three input-request types and the client dispatches all three; only elicitation can produce one server-side. That is why elicitation survives 4.0 via MRTR and push-sampling does not.

## Middleware root dispatch

**Status: Shipped (#4553).**

The migration already routed `initialize` interception through the SDK's `ServerMiddleware` list via `FastMCPServerMiddleware`. #4553 made that entry the root of middleware dispatch: FastMCP's method-agnostic hooks (`on_message`, `on_request`, `on_notification`) now fire for every inbound message — client cancellations, progress notifications, and requests that fail routing or validation — not only the ones that reach a component handler. The component methods keep running their own chain interior, and a method set plus a dispatch flag keep the two passes disjoint so each hook fires exactly once per message.

## First-class 2026 client

**Status: Partly shipped (#4572, #4574); full composition blocked upstream.**

`fastmcp.Client` now defaults to `mode="auto"` (#4572): it probes `server/discover`, falls back to the classic handshake, and answers multi-round-trip `input_required` requests through its existing handlers. The same PR surfaced `extensions=` and `result_claims=` (SEP-2133). The client also dropped its forked protocol helpers — extension folding, the evicting message handler, discover synthesis — in favor of the SDK's own (#4574).

The decision here was **compose, not wrap** (D16): rebuild `fastmcp.Client` on the SDK's high-level `mcp.Client` rather than wrapping `mcp.ClientSession`. The parts that compose cleanly have shipped. The rest is **blocked upstream**: `mcp.Client` constructs its `ClientSession` at a single hardcoded site with no injection hook, while FastMCP's `session_class` is load-bearing (`ProxyClient` substitutes a session that skips result validation so a backend's schema violation surfaces at the end client rather than becoming a proxy error). A `session_factory=` hook on `mcp.Client` — the same shape as the `notification_bindings=` parameter added earlier — would unblock the full rebuild.

This workstream also owns the server-side statelessness design holes — `ctx.session_id` / `set_state` round-tripping and stateful-proxy affinity — since they turn on the same "what is a session without a session?" question. See [Statelessness on 2026-07-28](/development/v4-notes/known-gaps#statelessness-on-2026-07-28) for the full accounting.

## Subscriptions, cache hints, extensions, OTel

**Status: Mixed — cache hints and OTel shipped; subscriptions not started.**

A cluster of protocol features tracked for v4. Their statuses have diverged:

- **Cache hints — shipped (#4464).** Server-level authoring (`FastMCP(cache_ttl=..., cache_scope=...)`, SEP-2549) stamps every cacheable result, and the FastMCP client honors hints with an opt-in response cache.
- **OpenTelemetry — shipped (#4481).** Spans are on by default (a no-op without an exporter), with SDK-aligned attributes and a `FASTMCP_ENABLE_TELEMETRY=false` off-switch.
- **Extensions — client side shipped (#4572).** `Client(extensions=..., result_claims=...)` advertises opt-in client extensions (SEP-2133). The cross-era reconciliation of the `extensions` / MCP Apps capability advertisement is still open (the capability is stripped at pre-2026 negotiated versions — sdk-feedback #2).
- **Subscriptions — not started.** A `subscriptions/listen` surface backed by a subscription bus.

## SDK delegation, round two

**Status: Planned (gated on upstream).**

The real HTTP simplification is a v4 project, not this PR. FastMCP can collapse its `create_streamable_http_app` onto the SDK's `Server.streamable_http_app()` once upstream adds three things:

1. per-session event-store scoping,
2. a user-middleware injection hook,
3. a lifespan hook.

The payoff is not only less code — FastMCP would also inherit the SDK's session-owner credential enforcement, a security gain it lacks today. These are the three upstream feature requests to file (alongside the advisory dossier described in [Known Gaps](/development/v4-notes/known-gaps)). Until they land, the four HTTP overrides in the [Change Register](/development/v4-notes/change-register#http) stay.

One latent capability worth surfacing on FastMCP's side: `session_idle_timeout` is accepted by the manager but never set by `create_streamable_http_app` — a one-line plumb if FastMCP wants to expose it.
