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

# Input required

> Ask the client for input by returning inputRequired, and serve both protocol eras from one handler

Sometimes a handler cannot finish in one shot. It needs the user to confirm something, or it needs the client's LLM to draft a value. On the [legacy era](/concepts/protocol-eras) a handler asks by calling the client back mid-execution — `ctx.sample()`, `ctx.elicit()`, `ctx.listRoots()`. The [modern era](/concepts/protocol-eras) has no server-to-client channel, so a handler cannot push a request back. It **asks by returning** instead.

A handler returns `inputRequired(...)` with the requests the client must fulfil. The client fulfils them and retries the same call, this time carrying the answers. The handler runs again, reads the answers, and finishes. This back-and-forth is a multi-round-trip request (MRTR). The key property is write-once: a handler written with `inputRequired(...)` serves both eras unchanged.

## Returning inputRequired

`inputRequired`, `acceptedContent`, and `inputResponse` are exported from `@prefecthq/fastmcp-ts/server`. To ask for input, return `inputRequired({ inputRequests })`. Each entry in `inputRequests` is one embedded request, built with a per-kind helper: `inputRequired.elicit(...)` for a form, `inputRequired.createMessage(...)` for LLM sampling, and `inputRequired.listRoots()` for the client's roots.

A handler that asks for confirmation returns early with an elicitation request, then completes on the retry once the answer arrives.

```typescript server.ts theme={null}
import { FastMCP, inputRequired, acceptedContent } from '@prefecthq/fastmcp-ts/server'

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

server.tool(
  { name: 'deleteFiles', description: 'Delete files, asking for confirmation first' },
  async (args: Record<string, unknown>) => {
    const count = args.count as number
    const ctx = server.getContext()

    const accepted = acceptedContent<{ confirm: boolean }>(ctx.inputResponses, 'confirm')
    if (!accepted?.confirm) {
      return inputRequired({
        inputRequests: {
          confirm: inputRequired.elicit({
            message: `Delete ${count} files?`,
            requestedSchema: {
              type: 'object',
              properties: { confirm: { type: 'boolean' } },
              required: ['confirm'],
            },
          }),
        },
      })
    }

    return `Deleted ${count} files`
  },
)
```

## Reading the response

On the retry, the client's answers arrive on `ctx.inputResponses`, keyed by the same names you used in `inputRequests`. Read them through `acceptedContent(ctx.inputResponses, key)` rather than by indexing the object. The reader validates the response shape and returns the accepted content, or `undefined` when the entry is missing, declined, or cancelled. For a non-elicitation answer — a sampling result or a roots list — use `inputResponse(ctx.inputResponses, key)`, which returns the raw response view.

`ctx.inputResponses` is `undefined` on the flow's first call, because there is no prior round to answer. That is why the handler above tests `accepted?.confirm`: an undefined or unconfirmed answer means "ask", and an accepted answer means "proceed". The same branch drives both rounds.

The values arrive from the client and are untrusted. `acceptedContent` checks the response shape, not your business rules — validate the content against your own schema before acting on it.

## Carrying state across rounds

The confirmation handler above needs no carried state: the retry re-sends the original tool arguments, so `count` is present on both rounds. A longer flow — several questions, or a value computed in an early round — needs to carry state the client cannot forge. That is `requestState`.

Mint state with `ctx.mintRequestState(payload)` and return it alongside the requests. On the retry, read it back with `ctx.requestState<T>()`. The state round-trips through the client as an opaque string.

```typescript server.ts theme={null}
server.tool(
  { name: 'deleteFilesStateful', description: 'Confirm, carrying server-minted state across the round-trip' },
  async (args: Record<string, unknown>) => {
    const count = args.count as number
    const ctx = server.getContext()

    const accepted = acceptedContent<{ confirm: boolean }>(ctx.inputResponses, 'confirm')
    if (!accepted?.confirm) {
      return inputRequired({
        inputRequests: {
          confirm: inputRequired.elicit({
            message: `Delete ${count} files?`,
            requestedSchema: {
              type: 'object',
              properties: { confirm: { type: 'boolean' } },
              required: ['confirm'],
            },
          }),
        },
        requestState: await ctx.mintRequestState({ count }),
      })
    }

    const state = ctx.requestState<{ count: number }>()
    return `Deleted ${state?.count} files`
  },
)
```

`requestState` comes back as attacker-controlled input, so integrity matters. Configure `FastMCPOptions.requestState` with an HMAC key, and every minted state is signed and verified before your handler sees it. A tampered or expired state is rejected with `-32602` before the handler runs. Without that option, `mintRequestState` falls back to an unsigned string and warns — never let unsigned state influence authorization, resource access, or business logic. That option is not only a safety measure, either: without it, `ctx.requestState()` on the next round hands back the raw string you minted, unparsed, not the object you passed in — only a configured verify step decodes it, so a flow that reads structured state back needs `FastMCPOptions.requestState` configured to work at all. [State and handles](/concepts/state-and-handles) covers `requestState` in full.

```typescript server.ts theme={null}
const server = new FastMCP({
  name: 'my-server',
  requestState: { key: process.env.REQUEST_STATE_KEY! },   // at least 32 bytes
})
```

### The requestState-only pattern

This example will not run without `FastMCPOptions.requestState` configured with a key — and neither will the combined example above it. Without a key, `ctx.requestState()` hands the handler back the raw string `mintRequestState` minted, not the object you passed in, so `state?.index` and `state?.total` below are always `undefined` and the handler never advances. On a legacy connection — sessionful or stateless — that surfaces as the call failing once `inputRequired.maxRounds` rounds are used up (about 2 seconds at the default 250ms pacing), with the error "still required input after 8 rounds". See the `requestState` snippet above for the minimum config: a `FastMCP` constructed with `requestState: { key: /* at least 32 bytes */ }`.

The example above pairs `requestState` with an elicitation, so the client still has something to fulfil each round. Some flows have nothing to ask — the handler just needs another turn to keep working. Drop `inputRequests` entirely and return `requestState` alone, and this is also the one multi-round-trip pattern that survives on a [stateless](/servers/running#stateless-mode) legacy HTTP server, for reasons covered in [one handler, both eras](#one-handler-both-eras) below.

A handler that sums a large array a few items at a time can use it to carry its running position and total between rounds. This needs `FastMCPOptions.requestState` configured, per the note above:

```typescript server.ts theme={null}
server.tool(
  { name: 'sumChunked', description: 'Sum a large array a few items at a time' },
  async (args: Record<string, unknown>) => {
    const items = args.items as number[]
    const CHUNK = 100
    const ctx = server.getContext()

    const state = ctx.requestState<{ index: number; total: number }>()
    const index = state?.index ?? 0
    const total = state?.total ?? 0

    const next = items.slice(index, index + CHUNK).reduce((sum, n) => sum + n, total)
    const nextIndex = index + CHUNK

    if (nextIndex < items.length) {
      return inputRequired({ requestState: await ctx.mintRequestState({ index: nextIndex, total: next }) })
    }

    return `Sum: ${next}`
  },
)
```

On a modern client this is a real retry: the client gets back `input_required` with nothing to answer, and resends the same call carrying `requestState`. On a legacy connection — sessionful or stateless — nothing is retried; the next section covers what happens instead.

## One handler, both eras

The handlers above are modern-era code by shape, yet they serve legacy clients unchanged. On a legacy connection the SDK's legacy shim turns an `inputRequired(...)` return into a real server-to-client request over the live session, collects the answer, and re-enters the handler with `ctx.inputResponses` populated. You write the flow once; the shim bridges it for 2025-era clients.

This is why `inputRequired(...)` is the recommended way to ask for input in new code. The older `ctx.sample()`, `ctx.elicit()`, and `ctx.listRoots()` calls still work on a sessionful legacy connection, which is the default, but they throw on a modern request, or on a legacy HTTP server running in [stateless mode](/servers/running#stateless-mode) — the error names `inputRequired(...)` as the replacement either way, and on a stateless server it points at the `requestState`-only form specifically. Reaching for the return-value form from the start means one code path for both eras.

That replacement is not a full escape from the stateless limit, though. An `inputRequests` entry, such as `inputRequired.elicit(...)`, still needs the legacy shim to turn it into a real server-to-client request over a live session. That is the same session `ctx.elicit()` needs, so it fails the same way on a stateless server.

A bare `requestState`, with no `inputRequests`, is the one form that survives — but not because the client resends anything. On a legacy connection, sessionful or stateless, the shim recognizes there is nothing to push, sleeps 250ms (`REQUEST_STATE_ONLY_LEG_PACING_MS`), and re-enters your handler itself, in the same process, inside the single HTTP request the client already sent. It repeats that — sleep, re-enter, check the result — until your handler stops returning `inputRequired(...)`, or until it has looped `maxRounds` times (default 8), at which point it fails the call rather than loop forever. The client sees one request and one final response; on this path it never receives an intermediate `input_required` result and never retries anything itself. A modern client is different: there, `requestState` alone really does end the round in the client's hands, and it really does resend the call — see [the requestState-only pattern](#the-requeststate-only-pattern) above for both sides of that split.

Two things follow from a loop the client cannot see or pace. Each round holds the original HTTP request open at least 250ms longer, so a flow that needs several rounds can hold a request open for whole seconds — worth weighing against the request-time budget that [stateless mode](/servers/running#stateless-mode) exists to protect. And the loop paces itself on a fixed clock; it cannot wait on anything else. A handler that expects to pause between rounds for an external event — a human approval, a webhook — has no way to signal that to the shim, and will instead spin through its rounds on the 250ms cadence until `maxRounds` runs out and the call fails. This form depends on the same legacy shim the `inputRequests` form uses; [serving knobs](#serving-knobs) below covers `legacyShim: false`, which turns off both alike. The client side of fulfilment is covered in [handlers](/clients/handlers) and [sampling](/clients/sampling).

## Serving knobs

`FastMCPOptions.inputRequired` tunes the legacy shim. `maxRounds` caps how many times the shim re-enters a handler before it fails — the default is 8, and it is the only bound on a `requestState`-only flow, whose 250ms pacing between rounds is fixed and not configurable. `roundTimeoutMs` sets the per-leg timeout for the shim's embedded requests, defaulting to 10 minutes to allow for human-paced answers — it does not apply to the `requestState`-only pacing, which has no embedded request to time out. Set `legacyShim: false` to disable the bridge entirely, so an `inputRequired(...)` return on a legacy request fails loudly instead, whether or not it carries `inputRequests`. These knobs affect the legacy era only; a modern client fulfils the requests itself.
