> ## Documentation Index
> Fetch the complete documentation index at: https://fastmcp-ts.docs.prefect.io/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 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.

```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 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](/clients/sampling) and [handlers](/clients/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](/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 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](/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 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](/concepts/state-and-handles) recommends.
