docs/development/v4-notes/feature-program.mdx
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:
main, with the PR cited.Code blocks marked as sketches show the intended API and do not resolve against the current tree.
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:
ctx.sample / ctx.sample_step emit a FastMCPDeprecationWarning (once per process, gated on settings.deprecation_warnings).ToolError on 2026-07-28 before the wire, which also fixed the opaque "Method not found" of sdk-feedback #10.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.
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): 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).
The intended declarative DX (sketch — the module does not exist yet):
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.
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.
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 on two counts. First, 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 solve this. Second, mcp.Client.__aenter__ refuses reentry, but FastMCP's client is deliberately reentrant (its refcounted context manager exists to fix a proxy session-reuse deadlock), so the rebuild also needs the SDK client to tolerate reentrant entry. Both must land upstream before the full rebuild is possible; session_factory= alone is necessary but not sufficient.
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 for the full accounting.
Status: Mixed — cache hints and OTel shipped; subscriptions not started.
A cluster of protocol features tracked for v4. Their statuses have diverged:
FastMCP(cache_ttl=..., cache_scope=...), SEP-2549) stamps every cacheable result, and the FastMCP client honors hints with an opt-in response cache.FASTMCP_ENABLE_TELEMETRY=false off-switch.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/listen surface backed by a subscription bus.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:
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). Until they land, the four HTTP overrides in the Change Register 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.