--- shape: how-to --- # input_required An **`input_required`** result is how a `tools/call`, `prompts/get`, or `resources/read` handler asks the connected client for input mid-call: the handler returns the embedded requests, the client answers them and retries the call, and the handler runs again with the responses. ## Return `input_required` instead of pushing a request The handler reads what already arrived with `acceptedContent`; while the answer is missing it returns `inputRequired(...)` instead of a tool result. ```ts source="../../examples/guides/servers/input-required.examples.ts#registerTool_inputRequired" const confirmationSchema = z.object({ confirm: z.boolean().meta({ title: 'Confirm deployment' }) }); server.registerTool( 'deploy', { description: 'Deploy after the operator confirms', inputSchema: z.object({ env: z.string() }) }, async ({ env }, ctx): Promise => { const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', confirmationSchema); if (confirmed?.confirm !== true) { return inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message: `Deploy to ${env}?`, requestedSchema: confirmationSchema }) } }); } return { content: [{ type: 'text', text: `Deployed to ${env}` }] }; } ); ``` The first round converts `confirmationSchema` to MCP's restricted elicitation JSON Schema and returns it inside `resultType: 'input_required'`. The client fulfils the request and retries `deploy`; on re-entry `acceptedContent` validates the answer with that same schema and the handler finishes. The restricted wire schema is a flat object of primitive properties, so only schemas that convert to that shape are accepted: strings (including the `email`, `uri`, `date`, and `date-time` formats — `z.email()`, `z.iso.date()`, and friends), numbers and their inclusive bounds (`.min()`/`.max()`; exclusive bounds like `.positive()` or `.gt()` do not convert), booleans, enums (`z.enum` or `z.literal(['a', 'b'])` — a union of literals does not convert), multi-select enum arrays, `.optional()`, and `.default()`. Anything the wire cannot express — nested objects, `.regex()` patterns, customized zod format patterns (`z.email({ pattern })`) — throws a `TypeError` when the request is built, before anything is sent. For non-zod libraries a pattern accompanying a supported format is treated as the library's own format regex and dropped from the wire. Constraints the wire cannot advertise at all (refinements, transforms) still hold on re-entry, because `acceptedContent` validates with the original schema. Every call on this page comes from an in-memory `Client` with an `elicitation/create` handler — [Test a server](../testing.md) shows that wiring. Calling `deploy` once produces both rounds: ``` [client] elicitation/create → Deploy to prod? { content: [ { type: 'text', text: 'Deployed to prod' } ] } ``` `inputRequired(spec)` throws a `TypeError` unless `spec` carries at least one of `inputRequests` or `requestState`. Each embedded request is checked against the capabilities the client declared; a missing capability rejects the call with `-32021` before anything reaches the wire. ::: info Coming from v1? `ctx.mcpReq.elicitInput` and `ctx.mcpReq.requestSampling` are the 2025-era push channels — they throw on a 2026-07-28 request. See [Elicitation](./elicitation.md) and the [upgrade guide](../migration/upgrade-to-v2.md). ::: ## Read the responses on re-entry `ctx.mcpReq.inputResponses` comes from the client — treat it as untrusted. Pass a Zod schema as `acceptedContent`'s third argument and the value reaches your handler already validated and typed. ```ts source="../../examples/guides/servers/input-required.examples.ts#acceptedContent_schema" server.registerTool( 'tag-release', { description: 'Tag a release after the operator confirms', inputSchema: z.object({ tag: z.string() }) }, async ({ tag }, ctx): Promise => { const view = inputResponse(ctx.mcpReq.inputResponses, 'confirm'); if (view.kind === 'elicit' && view.action !== 'accept') { return { content: [{ type: 'text', text: 'Tagging cancelled by the operator' }], isError: true }; } const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', z.object({ confirm: z.boolean() })); if (confirmed?.confirm !== true) { return inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message: `Tag ${tag}?`, requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'] } }) } }); } return { content: [{ type: 'text', text: `Tagged ${tag}` }] }; } ); ``` `acceptedContent` returns `undefined` for a missing, declined, or cancelled answer alike — re-issuing the request is the right move for all three only when the request is idempotent. `inputResponse` returns a discriminated view (`missing` / `elicit` / `sampling` / `roots`) when you need to tell a refusal from a first entry. A client that declines: ``` [client] elicitation/create → Tag v2.1.0? { content: [ { type: 'text', text: 'Tagging cancelled by the operator' } ], isError: true } ``` ## Write the handler write-once Write one handler that runs on every round: read each answer first, then request only the keys still missing. `inputRequests` is a map, so one round carries every outstanding request. ```ts source="../../examples/guides/servers/input-required.examples.ts#registerTool_writeOnce" server.registerTool( 'provision', { description: 'Provision a database', inputSchema: z.object({}) }, async (_args, ctx): Promise => { const name = acceptedContent(ctx.mcpReq.inputResponses, 'name', z.object({ name: z.string() })); const region = acceptedContent(ctx.mcpReq.inputResponses, 'region', z.object({ region: z.string() })); if (name === undefined || region === undefined) { return inputRequired({ inputRequests: { ...(name === undefined && { name: inputRequired.elicit({ message: 'Database name?', requestedSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } }) }), ...(region === undefined && { region: inputRequired.elicit({ message: 'Which region?', requestedSchema: { type: 'object', properties: { region: { type: 'string' } }, required: ['region'] } }) }) } }); } return { content: [{ type: 'text', text: `Provisioned ${name.name} in ${region.region}` }] }; } ); ``` Round one finds neither key, so both requests go out together; round two finds both and the handler returns. ``` [client] elicitation/create → Database name? [client] elicitation/create → Which region? { content: [ { type: 'text', text: 'Provisioned analytics in eu-west-1' } ] } ``` `inputResponses` holds only the latest round's answers, and nothing else on the server survives between rounds. A flow whose rounds must run in **sequence** carries what it has learned in `requestState`, below. ## Pick the embedded request kind Each value in `inputRequests` is one embedded request, named by the builder that constructs it: `inputRequired.elicit` (form), `inputRequired.elicitUrl` (out-of-band URL), `inputRequired.createMessage` (sampling), and `inputRequired.listRoots()`. ```ts source="../../examples/guides/servers/input-required.examples.ts#inputRequired_kinds" const next = inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message: 'Continue?', requestedSchema: { type: 'object', properties: { ok: { type: 'boolean' } }, required: ['ok'] } }), signin: inputRequired.elicitUrl({ message: 'Sign in to continue', url: 'https://example.com/auth' }), summary: inputRequired.createMessage({ messages: [{ role: 'user', content: { type: 'text', text: 'Summarize the diff' } }], maxTokens: 200 }), roots: inputRequired.listRoots() } }); ``` `acceptedContent` only reads accepted form elicitations; read the sampling and roots responses through `inputResponse`, which discriminates all four kinds. [Elicitation](./elicitation.md) covers `requestedSchema` and URL mode in full. ::: warning Sampling and roots are deprecated as of protocol revision 2026-07-28 (SEP-2577) — see [Sampling](./sampling.md). Reach for the elicitation builders first. ::: ## Carry state across rounds with `requestState` To run rounds in sequence, return an opaque `requestState` string alongside the requests. The client echoes it back byte-for-byte on the retry, and `ctx.mcpReq.requestState()` reads its decoded payload on re-entry. Mint it with the codec from the next section. ```ts source="../../examples/guides/servers/input-required.examples.ts#requestState_mint" server.registerTool( 'wipe-cache', { description: 'Confirm, then pick a scope, then wipe', inputSchema: z.object({}) }, async (_args, ctx): Promise => { const state = ctx.mcpReq.requestState<{ step: string }>(); if (state?.step !== 'confirmed') { const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm'); if (confirmed?.confirm !== true) { return inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message: 'Really wipe the cache?', requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'] } }) } }); } // Mint only what the response above already proved: the operator confirmed. return inputRequired({ inputRequests: { scope: inputRequired.elicit({ message: 'Which scope?', requestedSchema: { type: 'object', properties: { scope: { type: 'string' } }, required: ['scope'] } }) }, requestState: await stateCodec.mint({ step: 'confirmed' }) }); } const scope = acceptedContent<{ scope: string }>(ctx.mcpReq.inputResponses, 'scope'); return { content: [{ type: 'text', text: `Wiped ${scope?.scope ?? 'all'}` }] }; } ); ``` Mint only what earlier rounds already proved. The token is bearer proof of whatever it claims: state minted as `{ step: 'confirmed' }` before the confirmation arrives grants that step to anyone who echoes it. One call drives all three entries: ``` [client] elicitation/create → Really wipe the cache? [client] elicitation/create → Which scope? { content: [ { type: 'text', text: 'Wiped sessions' } ] } ``` ## Protect `requestState` with the codec `requestState` round-trips through the client and comes back as attacker-controlled input; the SDK applies no protection of its own. `createRequestStateCodec` returns an HMAC-SHA256 `{ mint, verify }` pair — pass `verify` as `ServerOptions.requestState.verify` and it runs before every handler entry that carries state. ```ts source="../../examples/guides/servers/input-required.examples.ts#requestState_codec" const stateCodec = createRequestStateCodec<{ step: string }>({ key: crypto.getRandomValues(new Uint8Array(32)), // >= 32 bytes; share it across instances in a fleet ttlSeconds: 600 }); const server = new McpServer({ name: 'releases', version: '1.0.0' }, { requestState: { verify: stateCodec.verify } }); ``` With the hook in place, the accessor hands the handler `verify`'s decoded payload, and tampered or expired state never reaches the handler at all. Retrying `wipe-cache` with `requestState: 'tampered'` answers a wire-level protocol error: ``` -32602 Invalid or expired requestState ``` ::: warning The codec is signed, not encrypted — the client can base64url-decode the payload. Keep secrets out of it. ::: ## Let the shim serve older clients The handlers above already serve every connection. On a connection that predates 2026-07-28, the SDK's legacy shim — on by default — fulfils an `input_required` return by pushing real `elicitation/create`, `sampling/createMessage`, and `roots/list` requests over the session, then re-enters the handler with the collected responses and the byte-exact `requestState` echo. Every result quoted on this page came from such a connection. Set `ServerOptions.inputRequired.legacyShim: false` to fail loudly instead. Which revision a connection negotiates is covered in [Protocol versions](../protocol-versions.md). ## Recap - A handler asks for input by returning `inputRequired(...)`; the client answers the embedded requests and retries the call. - `inputRequired(spec)` needs at least one of `inputRequests` or `requestState`, and throws a `TypeError` without one. - `acceptedContent(ctx.mcpReq.inputResponses, key, schema)` validates the untrusted client answer before it reaches your code; `inputResponse` discriminates declines and the non-elicitation kinds. - A write-once handler re-derives its position on every entry and requests only what is still missing. - `requestState` is the only cross-round memory; protect it with `createRequestStateCodec` and mint only what earlier rounds proved. - The legacy shim serves the same handlers to pre-2026-07-28 clients.