docs/getting-started/whats-new.mdx
FastMCP 4 makes stateful MCP applications work on MCP's sessionless protocol. Tools can ask follow-up questions across requests, preserve authenticated user state, and move long-running work into background tasks without sticky sessions or a continuously connected client.
The protocol changed completely underneath those APIs. Your application usually does not: one FastMCP server negotiates both protocol eras per connection, and most FastMCP 3 servers upgrade unchanged.
That is the theme of version 4: stateless transport without stateless application code. The release also makes protocol extensions a first-class surface, adds enterprise identity for agents acting on behalf of users, and strengthens production defaults across caching, routing, and security.
<Note> FastMCP 4 is in **beta**. Pin an exact version and expect sharp edges. See [Install the v4 prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease). </Note>A protocol migration usually forces a choice between breaking clients that have not moved yet and holding the server back with them. FastMCP 4 serves both eras from one deployment, negotiating the best mutual version for each connection. Modern clients get the sessionless protocol while handshake-era clients continue working unchanged.
Statelessness changes how that deployment scales. Each modern request carries everything needed to answer it, so any replica behind an ordinary load balancer can serve any request and session affinity stops being a requirement.
The client default follows the same rule. Client(url) probes for the modern protocol and falls back to the handshake when necessary. Pin mode="legacy" only when your application specifically needs the session back-channel.
from fastmcp import Client
# Negotiate the best mutual protocol
client = Client("https://example.com/mcp")
# Require the handshake-era protocol
legacy = Client("https://example.com/mcp", mode="legacy")
Once connected, client.protocol_version, client.server_info, client.server_capabilities, and client.instructions expose the same interface whichever era was negotiated. Application code that inspects a server does not need a protocol-version branch. See Protocol negotiation.
On modern connections, FastMCP also attaches the method, target name, and opted-in argument values as HTTP headers. Gateways and load balancers can route requests without parsing JSON-RPC bodies. See Gateway routing headers.
The modern protocol removes transport-level sessions, but applications still need conversations, user state, and long-running work. FastMCP moves those concerns into explicit application primitives that survive fresh connections. Shared stores and request-state keys extend them across replicas and worker restarts.
Many useful tools need more than one exchange. A booking tool asks for a destination, then a date, then confirmation. A destructive operation asks the user to approve it before continuing.
On the modern protocol, the tool returns a description of the input it needs. That result completes the request normally. The client fulfils the request and calls the tool again with the answer attached; the tool runs from the top, reads ctx.input_responses, and either asks another question or returns its final result.
Each request completes while the user responds. Single-process servers use an automatic process-local key to protect the state carried between rounds; load-balanced deployments configure one shared key so any replica can validate and resume the next round:
import os
from fastmcp import Context, FastMCP
from mcp.server.request_state import RequestStateSecurity
from mcp.types import ElicitRequest, ElicitRequestFormParams, InputRequiredResult
mcp = FastMCP(
"Booking",
request_state_security=RequestStateSecurity(
keys=[os.environ["REQUEST_STATE_KEY"].encode()]
),
)
@mcp.tool
async def book_flight(ctx: Context) -> str | InputRequiredResult:
answers = ctx.input_responses
if answers is None:
params = ElicitRequestFormParams(
message="Where would you like to fly?",
requested_schema={
"type": "object",
"properties": {"destination": {"type": "string"}},
"required": ["destination"],
},
)
return InputRequiredResult(
result_type="input_required",
input_requests={
"destination": ElicitRequest(
method="elicitation/create",
params=params,
)
},
)
response = answers["destination"]
if response.action != "accept" or response.content is None:
return "Booking cancelled."
destination = response.content["destination"]
return f"Booked a flight to {destination}."
Every replica must receive the same REQUEST_STATE_KEY, containing at least 32 bytes of secret key material. A FastMCP client drives the loop through its existing elicitation handler, so client code receives the terminal result without managing the intermediate rounds. See Elicitation on the modern protocol.
Application state follows the same explicit model. FastMCP stores state server-side and binds it to the authenticated user, so a session handle is inert in another user's hands.
Most tools want one state bucket per user. Declare a UserSession parameter and FastMCP injects it like Context: it never appears in the tool schema, and the caller passes nothing because their authenticated identity selects the bucket.
from fastmcp import FastMCP
from fastmcp.server.sessions import UserSession
mcp = FastMCP("Assistant")
@mcp.tool
async def remember(fact: str, session: UserSession) -> str:
facts = await session.get("facts", default=[])
facts.append(fact)
await session.set("facts", facts)
return f"Remembered {len(facts)} facts."
UserSession requires authentication, since an unauthenticated request has no user to key on. When one user needs several independent buckets, such as separate carts or conversations, SessionId exposes the handle as an explicit string argument.
The default in-memory state store is process-local. To preserve state across restarts or share it among replicas, pass a shared persistent session_state_store. See Session state.
Long-running tools create a different kind of state problem: holding a request open for several minutes invites timeouts and leaves the user unable to tell whether work is progressing. Background tasks accept the call and return a handle immediately, then let the client poll while work proceeds asynchronously.
FastMCP implements the io.modelcontextprotocol/tasks extension in the optional fastmcp-tasks package. The authoring API remains @mcp.tool(task=True), backed by Docket:
import asyncio
from fastmcp import FastMCP
from fastmcp_tasks import TasksExtension
mcp = FastMCP("MyServer")
mcp.add_extension(TasksExtension())
@mcp.tool(task=True)
async def slow_computation(duration: int) -> str:
"""Run a long computation."""
await asyncio.sleep(duration)
return f"Completed in {duration} seconds"
fastmcp.Client handles the task handle and polling cycle, so client.call_tool(...) returns the same way whether the tool ran inline or in the background. See Background tasks.
TasksExtension() uses an in-memory, single-process backend by default. Configure a Redis or Valkey backend for durable work that survives restarts and runs across separate workers.
Background tasks are built on a general extension surface. An MCP extension advertises a capability under a reverse-DNS identifier and can add behavior negotiated between a server and client.
FastMCP.add_extension() lets an extension advertise capabilities, add request methods, intercept tools/call, and own lifespan behavior with access to the component registry, Context, and authentication. Client extensions use the matching Client(extensions=...) interface.
Cross-cutting protocol behavior can therefore live in a supported plugin instead of requiring changes to FastMCP core. TasksExtension is a complete example of the interface. See Server extensions.
FastMCP 4 also lets servers answer MCP argument-completion requests. A completion handler sees the prompt or resource-template argument, its partial value, and values already supplied, so suggestions can depend on earlier choices.
from fastmcp import FastMCP
from mcp.types import PromptReference
mcp = FastMCP("Docs")
@mcp.prompt
def write_poem(theme: str) -> str:
return f"Write a poem about {theme}"
@mcp.completion
def complete(ref, argument, context):
if isinstance(ref, PromptReference) and argument.name == "theme":
options = ["nature", "love", "adventure"]
return [option for option in options if option.startswith(argument.value)]
return None
Registering the handler advertises the completion capability during negotiation, so clients only send requests to servers that support them. See Argument completion.
Interactive OAuth authorization assumes a person can complete a browser flow. Internal agents often act for employees without a person waiting at a keyboard, while the server still needs the employee's identity for authorization and audit.
Identity assertion carries that identity through the agent. A corporate identity provider signs an assertion, the agent presents it, and the server exchanges it for a short-lived token without an interactive login or consent screen. FastMCP performs signature verification, binding checks, replay rejection, and scoped token issuance through the authentication providers you already use.
from fastmcp import FastMCP
from fastmcp.server.auth import IdentityAssertion, OAuthProxy
auth = OAuthProxy(
# Existing upstream configuration
identity_assertion=IdentityAssertion(
trusted_issuers=["https://login.acme-corp.com"]
),
)
mcp = FastMCP("Internal API", auth=auth)
The asserted subject enters the normal authentication context, so tools read it through get_access_token() like any other identity. See Identity assertion.
Authorization gained a provider-neutral role check as well. require_roles accepts an extraction function for providers that store roles and groups under different claims, while scope step-up challenges tell a client exactly which scopes to request.
For clients with no user behind them, such as backend services and scheduled jobs, ClientCredentialsOAuthProvider implements the OAuth 2.0 client-credentials grant with no browser or redirect. See Machine-to-machine authentication.
A server can now attach freshness hints to its results, and a caching client can reuse those results without another round trip. Set a default time-to-live and scope on the server:
from fastmcp import FastMCP
mcp = FastMCP("Weather", cache_ttl=300, cache_scope="public")
KeyValueResponseCacheStore can place the client cache in Redis or another key-value store so a fleet of clients or proxies shares fills. See Response caching.
Resource templates now reject path traversal, absolute paths, and null bytes in their parameters before the handler runs. The protection is enabled by default and applies to mounted and proxied templates. See Path security.
OAuth defaults also distinguish native clients from web applications during Dynamic Client Registration, and missing scopes now produce an InsufficientScopeError that names the scopes required to continue. See Application type and scope shortfalls.
The sessionless protocol has no live connection for a server to call back into during execution. FastMCP 4 therefore removes ctx.sample(), ctx.sample_step(), and ctx.list_roots() from every protocol era so incompatible code fails immediately during an upgrade.
For generation, call an LLM directly from the server when your application owns the model. When borrowing the caller's model is the point, return an InputRequiredResult carrying a sampling request and read the answer on the next round. Roots use the same return-and-resume pattern. See Sampling and the guard pattern.
ctx.elicit() remains available on handshake-era connections; modern connections use the multi-round pattern described above. Code that constructs MCP protocol models directly must also use snake_case Python field names with SDK v2.
Upgrading from FastMCP 3 covers these changes and every other compatibility break.