Skip to main content
MCP has two protocol generations, and FastMCP speaks both. The legacy era is the 2025 protocol — revision 2025-11-25 and earlier. The modern era is protocol revision 2026-07-28, the largest change to MCP since launch. The two eras differ on the wire: how a session works, how a server asks the client for something, and which utility methods exist. This page installs the model that the rest of the documentation builds on. Learn it once, and every era-specific note elsewhere slots into it. The split has one governing rule. A server serves both eras. A client chooses one. A FastMCP server accepts a legacy client and a modern client at the same time, with no configuration. A FastMCP client connects with one era per connection, and you decide which through version negotiation.

The two eras

The legacy era is stateful. A connection opens with an initialize handshake, the server assigns a session, and later requests ride that session. The server can turn a request around and call the client back mid-handler — for sampling, elicitation, or roots. State, subscriptions, and server-to-client calls all lean on the live session. The modern era is stateless. Protocol revision 2026-07-28 removes the initialize handshake and the protocol-level session. Each request stands alone and carries its own envelope — protocol version, client info, and client capabilities travel in per-request metadata. Without a persistent session, several things move: the server can no longer push a request back to the client, session state has nowhere to live across requests, and the ping and logging/setLevel utility methods are gone. Each of those has a modern replacement, covered below.

Serving both eras

A FastMCP server needs no era configuration. Over HTTP it inspects each incoming request and routes it: a legacy request goes to a sessionful transport that preserves today’s behavior, and a modern request goes to a stateless handler. Over stdio the era is pinned per connection, decided from the client’s opening exchange. One server definition — the same tools, resources, and prompts — answers both. Running a server covers the transport details. Because one handler serves both eras, you write handler logic once. The features that changed most between the eras — asking the client for input, and remembering state — have era-agnostic APIs that do the right thing on each wire. Those are the input-required and state concepts.

Negotiating an era

A client picks its era through versionNegotiation. The default is { mode: 'auto' }: connect() probes the server once with server/discover. Definitive modern evidence selects the modern era. Anything else falls back to the plain legacy initialize handshake: a method-not-found answer, silence on a local stdio pipe, or a probe child that exits. Pass { mode: 'legacy' } to opt out: no probe, and the connect sequence is byte-identical to 2025 behavior. Pass { mode: { pin: '2026-07-28' } } to require the modern era outright; connect() throws if the server cannot speak it. After connecting, getProtocolEra() reports the negotiated result: 'modern', 'legacy', or undefined before you connect.
The library default is { mode: 'auto' } on every transport, and the probe is stall-safe in the SDK. Over stdio the probe runs against a short-lived sibling process, and a timeout falls back to the legacy handshake; a silent server on a local pipe is treated as a legacy server. Over HTTP, where silence means an outage rather than an old server, a probe timeout rejects with a typed error instead of hanging.

The CLI’s defaults

The fastmcp CLI follows the library: every transport defaults to { mode: 'auto' }, so a connecting command probes once and uses the modern era when the server offers it. Two flags override the default. --legacy opts a connection out: no probe, the plain 2025 handshake. --pin <version> requires that exact version on any transport; it is the strongest request, so it wins over any other era flag. --modern remains accepted as a deprecated no-op, because auto-negotiation is now the default. These flags reach list, call, and inspect; run takes no era flag, because it starts a server, and a FastMCP server serves both eras at once. See the CLI reference.

What changes between eras

Four behaviors differ between the eras. Each modern change has a replacement that a FastMCP API already routes to. Server-to-client requests move to a return value. On the legacy era a handler calls ctx.sample(), ctx.elicit(), or ctx.listRoots() to reach the client mid-handler, as long as the connection is sessionful, which is the default. The modern era has no server-to-client channel, and neither does a legacy HTTP server running in stateless mode, so a handler instead returns inputRequired(...). On the modern era, that is a real retry: the client fulfils the request itself and resends the call with the answers. A legacy HTTP server running in stateless mode reaches the same return value by a different route — covered next, not a retry at all. This is the input-required pattern, and a handler written that way serves both eras. The two “no session” cases are not quite the same, though. The modern era’s client fulfils an embedded request itself, straight off the return value, so inputRequired(...) needs no session there; that is the era’s native shape. A legacy HTTP server running in stateless mode still speaks the 2025 wire protocol to a client that expects a genuine server-to-client push, and only a live session lets FastMCP’s legacy shim perform that push. A stateless server has none, so an inputRequired({ inputRequests }) return fails there the same way ctx.elicit() does. Only the requestState-only form, with no inputRequests, survives, though not by having the client retry: the SDK’s legacy shim re-enters the handler itself, in the same process and inside the same HTTP request, pausing briefly between rounds instead of pushing anything over a session that doesn’t exist. That loop is bounded — a fixed number of rounds, each holding the request open a little longer — so it suits a handler that can finish on its own schedule, not one waiting on an external event. Input required covers the mechanism and a working example. Session state loses its home on modern HTTP. ctx.getState / setState / deleteState remember values across requests on a connection. They work on stdio and on legacy HTTP when the server is sessionful, which is the default. On a modern HTTP request, or on a legacy HTTP server running in stateless mode, each call runs statelessly, so the accessors throw a pointed error rather than drop the write silently. State and handles covers the boundary and the portable alternatives. The ping and setLogLevel utilities change shape. The modern era removes both wire methods. A FastMCP client keeps the same method names and routes them per era: ping() sends server/discover on modern, and setLogLevel() threads the level into per-request metadata instead of a logging/setLevel call. Resource subscriptions change transport. The legacy resources/subscribe / resources/unsubscribe RPCs become a single long-lived subscriptions/listen stream on the modern era. A FastMCP client routes subscribeResource() per era, so your subscription code is the same on both. A legacy HTTP server running in stateless mode supports neither RPC: subscriptions need a session to track, so it withdraws the capability and rejects both.

Unsupported versions

When a client pins a version the server cannot speak, the connection fails with -32022 UnsupportedProtocolVersion. The error names the requested version and the versions the server supports. Through the CLI a --pin mismatch reports the same information and tells you to change or drop the flag. This is the one era decision that fails loudly — 'auto' never reaches it, because auto falls back to legacy instead of throwing.