2026-07-28) is stateless. Every request stands alone: the server builds a fresh connection to handle it and discards everything when it returns. There is no session to hang state on, so a tool that wants to remember something between calls — the items in a cart, the thread of a conversation, a running total — has nowhere to keep it. Store it on the connection and it vanishes the moment the request finishes.
This is a deliberate choice in the protocol. Weighing protocol-level sessions against statelessness, the MCP working group chose statelessness and moved session semantics up to the application: the server hands the client an identifier, and the client passes it back as an argument on later calls. Their own example is a shopping cart — the server returns a basket_id, and the client includes it in each subsequent add_item and checkout call.
FastMCP implements that pattern as session state, and adds the one thing the bare handle lacks: isolation. State is stored server-side and keyed to the authenticated user, so a handle is inert in anyone else’s hands. You pick one of two shapes per tool, depending on whether a user has a single bucket of state or many.
Per-user state
Most tools that remember things want one bucket per user — their preferences, their history, their accumulated context. Declare aUserSession parameter and FastMCP injects it, keyed to the authenticated user. It behaves like the request context: it never appears in the tool’s input schema and the caller passes nothing, because the user’s identity comes from their validated credentials and selects the right bucket automatically.
UserSession requires authentication. On an unauthenticated request there is no user to key on, so the tool raises a clear error rather than guessing at a bucket.
Distinct sessions
Sometimes one user needs more than one bucket — separate carts, parallel conversations, independent workflows. Now the caller has to say which session it means, so the identifier becomes a tool argument. Declare aSessionId parameter. Unlike UserSession, it appears in the input schema as a string, because the agent is the one that supplies it. FastMCP fills in that argument’s description for you — instructing the agent to obtain an id and pass it back — so the tool teaches the protocol on its own, with no prompting on your side.
An agent obtains an id by calling create_session, which comes from a SessionProvider — providers are how FastMCP contributes functionality like this. Register one whenever your tools take a session_id. Without it there is no way to mint an id, so every id is rejected and the tools cannot resolve a session — a mistake you catch the first time you run them.
get_session resolves and validates the id, returning a Session. It is a standalone function, not a context method, so it needs no foreground context and works from a background task’s worker as well as a normal request.
A session id is real and owned: create_session records it under the current user, and only an id created that way resolves. Passing an id that was never created — or one created by a different user — raises rather than quietly opening a fresh bucket, so a typo or a stolen id fails loudly instead of misrouting state.
When your application already mints its own identifiers — conversation ids, workflow ids — take them as ordinary string arguments rather than SessionId, and skip the provider entirely; SessionId is specifically the create-then-pass contract backed by create_session.
The session object
Both patterns give a tool aSession: an async view over one bucket of stored state. Read a value with await session.get(key, default=None), write one with await session.set(key, value), and remove one with await session.delete(key). Values are stored as JSON, so anything JSON-serializable round-trips.
The session’s own identifier is available as session.id — the id for a session resolved from a session_id argument, and None for an injected UserSession, which has no distinct id because its bucket is the authenticated user.
await session.clear() empties the session’s state while keeping the session itself valid — the id still resolves, the bucket is just empty. To retire a session entirely, an agent calls end_session, which deletes it so the id no longer resolves at all.
Isolation
Every session is keyed by two things, in this order: the authenticated user, then the session id. The order is the whole security model. The user is the wall; the id only organizes sessions within that wall. On an authenticated request the user comes from the validated token, which the caller cannot forge. Two different users can pass the very same session id and never reach each other’s data, because each id is namespaced under its user’s identity. This makes a session id safe to expose — it travels through the agent’s context and your logs, and on its own it grants nothing. Guessing another user’s id leads nowhere: it was created under their namespace, so in the guesser’s namespace it simply does not exist and the call is rejected. Without authentication there is no user to key on, and the guarantee changes. An unauthenticated session is a bearer handle: whoever holds the id can read and write it. Ids fromcreate_session are unguessable, which keeps a caller from stumbling onto another session, but that is guess-resistance, not isolation — a leaked id is a leaked session. Treat unauthenticated sessions as single-tenant: sound for a personal server with one trusted client, never a boundary between tenants. Multi-tenant isolation requires authentication.
Storage and lifetime
Session state lives in the server’s storage backend — in-memory by default, or Redis or another shared store when a fleet of servers must see the same sessions. Because the store owns retention, it owns expiry: FastMCP writes session state without a TTL of its own, so the store you configure is the single place session data lives and expires. To give every session a default lifetime, wrap the store so writes without an explicit TTL get one — for example, thekey-value library’s TTL-clamp wrapper takes a missing_ttl:
end_session still removes one immediately.
Relationship to request state
The request context also carries state, throughctx.set_state and ctx.get_state, and the two solve different problems. Context state is scoped to a single request — the right place for a value that a middleware sets and a handler reads within the same call. Session state is what persists across requests. When you need a value to survive from one tool call to the next, reach for UserSession or SessionId; when it only needs to live for the current request, keep it on the context.
