
# Multi Round-Trip Requests

> Ask the client for input by returning, not by calling.

Modern servers no longer send `sampling/createMessage`, `elicitation/create`, or `roots/list` as requests of their own. Instead a `tools/call`, `resources/read`, or `prompts/get` handler returns an **interim result** describing what it needs, and the client retries the original request with the answers attached.

```ts
const whoamiTool = defineMcpTool({
  name: "whoami",
  handler: (event) => {
    const answer = event.context.mcp?.inputResponses?.username as any;

    if (!answer?.content?.name) {
      return {
        resultType: "input_required",
        inputRequests: {
          username: {
            method: "elicitation/create",
            params: { mode: "form", message: "What is your name?" },
          },
        },
        requestState: signedState(),
      };
    }

    return { content: [{ type: "text", text: `Hello, ${answer.content.name}!` }] };
  },
});
```

The shape of the handler is the important part: it is **re-entrant**. The same code runs on the first call and on the retry, branching on whether the answers are present. There is no suspended continuation on the server — that is what makes the modern era stateless.

## Anatomy

| Field            | Direction | Purpose                                                            |
| ---------------- | --------- | ------------------------------------------------------------------ |
| `inputRequests`  | out       | Named requests for the client to satisfy (`elicitation/create`, …) |
| `requestState`   | out       | Opaque string the client echoes back, so you can resume            |
| `inputResponses` | in        | The answers, keyed by the same names, on `event.context.mcp`       |
| `requestState`   | in        | Your string, verbatim, on `event.context.mcp`                      |

Several inputs can be requested at once — key them and read them back by key. Interim results never carry caching hints.

## `requestState` Is Attacker-Controlled

`requestState` round-trips through the client, which means the value you read back is whatever the client chose to send. `h3-mcp` validates only that it is a bounded string.

> [!IMPORTANT]
> If `requestState` influences authorization or business logic, integrity-protect it. Sign it (HMAC) or encrypt it (AEAD), include the tool name and an expiry, and reject anything that fails verification. Storing a user id in plain `requestState` and trusting it on the retry is a privilege-escalation bug.

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

function sign(payload: object): string {
  const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
  const mac = createHmac("sha256", process.env.STATE_SECRET!).update(body).digest("base64url");
  return `${body}.${mac}`;
}

function verify(state: string | undefined): any {
  if (!state) return undefined;
  const [body, mac] = state.split(".");
  if (!body || !mac) return undefined;
  const expected = createHmac("sha256", process.env.STATE_SECRET!).update(body).digest("base64url");
  if (mac.length !== expected.length) return undefined;
  if (!timingSafeEqual(Buffer.from(mac), Buffer.from(expected))) return undefined;
  const payload = JSON.parse(Buffer.from(body, "base64url").toString());
  return payload.exp > Date.now() ? payload : undefined;
}
```

## Requiring a Capability

Before asking for input, confirm the client can provide it. `requireClientCapability` throws `-32021` (`MissingRequiredClientCapability`) listing what was missing:

```ts
event.context.mcp!.requireClientCapability?.("elicitation");
```

Capabilities come from `_meta["io.modelcontextprotocol/clientCapabilities"]` on the request in flight.

> [!NOTE]
> There is no helper yet for constructing or signing `requestState` — `resultType: "input_required"` passes through and `inputResponses` / `requestState` are surfaced, but the ergonomic wrapper is still on the roadmap. Until then, use a signing helper like the one above.
