Reaching the context
You reach the context by callingserver.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
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 withctx.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
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 withctx.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
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 returninputRequired(...) 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 returnsinputRequired(...), 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 exposesgetState(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
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
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.