docs/development/v4-notes/protocol-2026.mdx
FastMCP v4 serves the sessionless 2026-07-28 protocol era and the session-based handshake eras from a single server, with per-connection auto-detection. This page catalogs what FastMCP provides for the modern era — both the protocol machinery it inherits from the MCP Python SDK and the capabilities FastMCP implements itself on top of that layer. It is the reference for what a v4 deployment can actually do on the modern protocol today.
SEP-990 defines enterprise "on-behalf-of" access: a corporate identity provider (Okta, Microsoft Entra, etc.) issues a signed ID-JAG asserting an employee's identity, the employee's agent presents it at the MCP authorization server's token endpoint via the RFC 7523 jwt-bearer grant, and receives a short-lived access token — no browser login, no per-user consent screen, and revocation lives at the IdP.
The protocol layer for this flow — grant parsing, the exchange_identity_assertion provider hook, and metadata advertisement — comes from the SDK. The validation and issuance logic that makes the flow actually work is FastMCP's implementation, and enabling it is one parameter on the existing auth providers:
from fastmcp import FastMCP
from fastmcp.server.auth import OAuthProxy, IdentityAssertion
auth = OAuthProxy(
..., # existing upstream configuration unchanged
identity_assertion=IdentityAssertion(
trusted_issuers=["https://login.acme-corp.com"],
),
)
mcp = FastMCP("Internal API", auth=auth)
Behind that one parameter, FastMCP performs the full SEP-990 §5.1 / RFC 7523 §3 processing: JWKS-based signature verification with automatic OIDC discovery of issuer keys, typ/iss/aud/sub validation, temporal checks (exp, iat, nbf, maximum assertion lifetime), enforcement of the assertion's signed client_id and resource bindings, jti replay rejection, scope derivation from the signed assertion (client requests can narrow but never widen), short-lived token issuance with no refresh token, and revocation tracking for the issued tokens. The asserted subject flows into the normal FastMCP auth context, so tools read it through get_access_token() like any other identity. See Identity Assertion for the full documentation.
This slots into FastMCP's existing authorization-server stack — the OAuth proxy's dynamic client registration, the consent flow, and self-issued JWTs — which is what makes a one-parameter enterprise deployment possible.
The complete picture of what a FastMCP v4 server and client provide on the 2026-07-28 era:
| Capability | What FastMCP provides |
|---|---|
| Dual-era serving | One server answers both server/discover (modern, sessionless) and initialize (handshake) connections, auto-detected per connection. Any replica behind a plain load balancer can answer a modern request. |
| Identity assertion (SEP-990) | Complete server-side implementation, one parameter to enable (above). |
| Authorization server | Full AS stack: OAuthProxy bridges DCR-expecting MCP clients to non-DCR enterprise IdPs, ~18 built-in providers, consent UI, self-issued JWTs, protected-resource metadata (RFC 9728). |
| Cache hints (SEP-2549) | Server-level authoring (FastMCP(cache_ttl=..., cache_scope=...)) stamps every cacheable result; the FastMCP client honors hints with an opt-in response cache. |
| Distributed response caching | KeyValueResponseCacheStore backs the client cache with any key-value store (Redis, memory, filetree), so a fleet of clients or proxy replicas shares cache fills across processes. |
| Resource path security | Templated resource parameters are screened for traversal, absolute paths, and null bytes before handlers run — on by default, including provider-sourced and mounted templates. |
| Client protocol negotiation | Client(mode="auto") — the default as of v4 — probes server/discover and falls back to the classic handshake; the client answers multi-round-trip input_required requests through its existing handlers. Pin mode="legacy" to force the handshake. |
| Elicitation on the modern protocol (SEP-2322) | Tools request user input via multi-round trips: a tool returns an InputRequiredResult and re-runs per round, reading the client's answers off ctx.input_responses / ctx.request_state (the guard pattern). Each round is a complete request→response cycle; the framework seals request_state on the wire and unseals it before the tool runs, and a shared-key request_state_security policy carries state across replicas. On handshake-era connections returning this result produces a clear era error. |
| Spec-standard errors (SEP-2164) | Missing-resource reads return -32602; push-feature calls on modern connections fail with clear era-specific errors rather than generic method-not-found. |
| Middleware | Typed per-method hooks (on_call_tool, on_list_tools, …) and a suite of built-ins (auth, rate limiting, caching, error handling, logging, timing, and more). |
| Composition | mount(), providers, proxying, and tool transforms compose servers dynamically at runtime, with lifespans and middleware driven through the SDK session manager. |
| Pagination | Declarative FastMCP(list_page_size=...) paginates all list operations in the high-level server; the client auto-paginates with cycle detection. |
| Telemetry | OpenTelemetry spans on by default (no-op without an exporter), SDK-aligned attributes (mcp.method.name, mcp.protocol.version, gen_ai.*), plus auth and provider-delegation spans; FASTMCP_ENABLE_TELEMETRY=false disables cleanly. |
Background tasks are not in the table because they do not work on 2026-07-28. The @mcp.tool(task=True) runtime implements the 2025 task wire protocol (SEP-1686), which was removed from the MCP spec entirely — on 2026-07-28, task submission is not part of the core protocol, and tasks became the separate io.modelcontextprotocol/tasks extension. So task=True completes only on handshake-era connections, and FastMCP's 2025 task machinery (fastmcp_slim/fastmcp/server/tasks/) is slated for removal rather than being carried forward. Users who need background tasks today should stay on FastMCP 3; the 2026 tasks extension will be the modern replacement, built later through the extensions mechanism. See Known Gaps for the deletion tracking.
Elicitation on the modern protocol is now shipped in its guard form — a tool returns an InputRequiredResult and re-runs per round to gather user input via multi-round trips (see Elicitation on the modern protocol). The declarative Resolve(...) layer over that primitive remains staged, tracked in the Feature Program, along with the unified subscriptions/listen stream. The Known Gaps page tracks the upstream dependencies that gate them.