> ## Documentation Index
> Fetch the complete documentation index at: https://colin-feat-client-cache-mode.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Context

> The handler's back-channel to the client: logging, progress, asking for input, request state, and session state across protocol eras

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](/concepts/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.

```typescript server.ts theme={null}
import { FastMCP } from '@prefecthq/fastmcp-ts/server'
import { z } from 'zod'

const server = new FastMCP({ name: 'my-server' })

server.tool({ name: 'summarize', input: z.object({ text: z.string() }) }, async ({ text }) => {
  const ctx = server.getContext()
  await ctx.info('Summarizing document')
  return summarize(text)
})
```

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](/servers/auth/overview) 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.

```typescript server.ts theme={null}
server.tool({ name: 'sync' }, async () => {
  const ctx = server.getContext()
  await ctx.log('info', 'Starting sync')
  await ctx.info('Shorthand for the same thing')
  return runSync()
})
```

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.

```typescript server.ts theme={null}
server.tool({ name: 'process', input: z.object({ items: z.array(z.string()) }) }, async ({ items }) => {
  const ctx = server.getContext()
  for (let i = 0; i < items.length; i++) {
    await handle(items[i])
    await ctx.reportProgress(i + 1, items.length, `Processed ${i + 1} of ${items.length}`)
  }
  return 'done'
})
```

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](/concepts/protocol-eras), 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](/concepts/input-required) covers it end to end, and [reading input responses](#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](/servers/running#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.

```typescript server.ts theme={null}
server.tool({ name: 'draft', input: z.object({ topic: z.string() }) }, async ({ topic }) => {
  const ctx = server.getContext()
  const result = await ctx.sample({
    messages: [{ role: 'user', content: { type: 'text', text: `Write a haiku about ${topic}` } }],
    maxTokens: 128,
  })
  return result.content.type === 'text' ? result.content.text : ''
})
```

`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.

```typescript server.ts theme={null}
server.tool({ name: 'deploy' }, async () => {
  const ctx = server.getContext()
  const { action, content } = await ctx.elicit('Deploy to production?', {
    type: 'object',
    properties: { confirm: { type: 'boolean' } },
    required: ['confirm'],
  })
  return action === 'accept' && content?.confirm ? deploy() : 'cancelled'
})
```

`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](/clients/sampling) and [handlers](/clients/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](/concepts/input-required#the-requeststate-only-pattern) 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](/concepts/input-required) shows these readers in a full flow, and [state and handles](/concepts/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.

```typescript server.ts theme={null}
server.tool({ name: 'increment' }, () => {
  const ctx = server.getContext()
  const count = ((ctx.getState('count') as number) ?? 0) + 1
  ctx.setState('count', count)
  return count
})
```

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](/servers/running#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](/concepts/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.

```typescript server.ts theme={null}
server.tool({ name: 'openStream' }, () => {
  const ctx = server.getContext()
  const stream = openStream()
  ctx.onClose(() => stream.close())
  return 'stream opened'
})
```

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](/servers/running#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](/concepts/state-and-handles) recommends.
