Skip to main content
A handler runs in the middle of a live request, and sometimes it needs to talk back to the client that made it — to report progress on slow work, to ask for input, or to remember something for a later request. The context is that back-channel. It is a request-scoped handle to the client, available to any tool, resource, or prompt handler, exposing everything a handler can send or ask during the request it is serving. The context’s most powerful methods split along the protocol eras. A legacy connection is stateful, so a handler can turn the request around and call the client back, and it can remember state across a session. The modern era is stateless, so those same actions take a different shape or move off the context entirely. Each section below marks where the era matters.

Reaching the context

You reach the context by calling server.getContext() inside a handler. It is ambient: FastMCP stashes the context for the current request in an AsyncLocalStorage, so getContext() finds it anywhere in the call tree — a helper three functions deep gets the same context as the handler, with nothing threaded through the arguments. There is no prop-drilling and no ctx parameter to pass around.
server.ts
Because the context belongs to a request, getContext() throws when there is no live request — at module load, in a background timer, or anywhere outside a handler’s execution. That is by design: the methods below have no meaning without a client on the other end. If you need to do work after a request finishes, capture what you need from the context while the handler is still running. The context also carries two read-only fields about the request itself: auth, the verified AccessToken for the caller when authentication is configured, and requestId, the MCP request ID from the incoming message.

Logging

A handler logs to the client with ctx.log(level, message), where the level is one of the RFC 5424 severities. The client decides what to do with each log — surface it, file it, or drop it below its configured level — so logging is how a handler narrates its work without deciding how that narration is presented.
server.ts
Each severity has a shorthand so you rarely write the level string: debug, info, notice, warning, error, critical, alert, and emergency each send a log at their level. All of them, and log itself, take an optional logger name as a final argument when you want to tag where a message came from.

Progress

Long-running work reports progress with ctx.reportProgress(progress, total?, message?), which sends notifications/progress to the client so a UI can show a bar or a count. Call it as the work advances; the client correlates the updates and renders them.
server.ts
Progress is meaningful only when the client asked for it. A client opts in by attaching a progress token to its request; without one, reportProgress is a no-op. You don’t have to check — call it unconditionally and it simply does nothing when no one is listening, so the same handler works whether or not the caller wants updates.

Asking the client for input

Sometimes a handler needs something only the client can provide — an LLM completion, a form answer, or the client’s filesystem roots. How a handler asks depends on the protocol era, and that difference shapes this whole section. The recommended, era-agnostic way is to return inputRequired(...) and read the answer when the client retries the call. A handler written that way serves both eras from one code path — the input-required guide covers it end to end, and reading input responses below covers the retry side on the context. The context also carries the older push-style calls described here. They still work on a legacy connection, but they are deprecated as of protocol revision 2026-07-28, and they throw on a modern request. ctx.sample(params) asks the client to run LLM inference on the server’s behalf and returns a completion from a real model. This is how a server uses a model without holding API keys of its own — the client owns the LLM, and the server borrows it through the context.
server.ts
ctx.elicit(message, schema) asks the client to collect input from the user: it renders a form from the schema you pass, validates the answer, and returns the result. Reach for it when a handler discovers mid-execution that it needs something only the user can provide.
server.ts
ctx.listRoots() returns the filesystem roots the client has declared it will expose, so a server can scope its work to paths the user has sanctioned. On a legacy connection each of the three depends on the client advertising the matching capability — sampling, elicitation, or roots — and throws when it is absent. On a modern request all three throw regardless, because the modern era has no server-to-client channel; the error names inputRequired(...) as the replacement. The client side of fulfilment is covered in client sampling and handlers.

Reading input responses

When a handler returns inputRequired(...), the client fulfils the requests and retries the call. On that retry the context carries the answers, and a small set of readers reach them. ctx.inputResponses holds the current round’s embedded responses, keyed by the names you used in the request. It is undefined on a flow’s first call, since there is no prior round to answer — that is the signal that tells a handler whether it is asking or finishing. Read it with acceptedContent(ctx.inputResponses, key) or inputResponse(ctx.inputResponses, key) — both re-exported from @prefecthq/fastmcp-ts/server — rather than indexing the object, because those readers validate the response shape first. ctx.requestState<T>() reads state the handler carried across the round-trip, and ctx.mintRequestState(payload) seals a payload into the opaque string you return from inputRequired({ requestState }). When you configure FastMCPOptions.requestState with an HMAC key, every minted state is signed and verified before your handler sees it. Without a key, mintRequestState returns an unsigned string and warns — never let unsigned state influence authorization, resource access, or business logic, because the client can read and tamper with it. The input-required guide shows these readers in a full flow, and state and handles places requestState alongside the other ways a handler remembers data.

Session state

Session state remembers a value across the requests of one connection. The context exposes getState(key), setState(key, value), and deleteState(key), backed by a key-value store that lives as long as the connection. Write a value in one request and read it back in the next, without a database.
server.ts
State is scoped to one connection, never shared across them, so two clients never see each other’s values. But it depends on a persistent session, and the modern era’s HTTP transport has none. On a modern HTTP request each accessor throws a pointed error rather than drop the write against a fresh per-request store; the message names ctx.requestState() and ctx.mintRequestState() as the replacements. Session state persists on stdio and on legacy HTTP, and throws on modern HTTP. State and handles covers this boundary and the portable alternatives — request state and server-minted handles — that work on every transport.

Session cleanup

ctx.onClose(callback) registers a callback to run when the connection’s session closes. Use it to release per-session resources — close a handle, flush a buffer — when a client disconnects.
server.ts
The callback fires when a sessionful HTTP session closes. A stateless modern HTTP request has no session, so onClose is a no-op there, and stdio never fires it either — the callbacks run only at a legacy HTTP session’s close. Do not rely on it for correctness. For cleanup that must run on every transport, expire data on a timer behind a server-minted handle instead, the pattern state and handles recommends.