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 sessionful legacy connection, which is the default, but they are deprecated as of protocol revision 2026-07-28, and they throw on a modern request or on a legacy HTTP server running in stateless mode. 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 sessionful 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, or on a legacy HTTP server running in stateless mode, all three throw regardless: the modern era has no server-to-client channel, and a stateless server has no session for the push and its reply to share, so the error names inputRequired(...) as the replacement either way. The client side of fulfilment is covered in client sampling and handlers. On a stateless server that error steers to the requestState-only form specifically, because the rest of the return value has the same limit, for the same reason. An inputRequired({ inputRequests }) entry still relies on a live session to carry the legacy shim’s push and its reply, so it throws too. Only inputRequired({ requestState }), with no inputRequests, survives, and not because the client retries anything: on a legacy connection the SDK’s shim re-enters your handler itself, in-process, inside the one HTTP request already open, pausing briefly between rounds instead of pushing anything over a session that isn’t there. ctx.requestState() and ctx.mintRequestState() genuinely work here, since carrying state needs no session — but the re-entries are capped and each one holds that request open a little longer, so this is not a free pass. Input required covers the mechanism, its limits, and a full example.

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 both the modern era’s HTTP transport and a legacy HTTP server running in stateless mode have none. On either kind of 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 when the server is sessionful, which is the default, and throws on modern HTTP and on stateless legacy HTTP alike. 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 only when a sessionful legacy HTTP session closes, which is the default for legacy HTTP. Neither a modern HTTP request nor a legacy HTTP server running in stateless mode has a session to close, so onClose is a no-op on both, and stdio never fires it either. 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.