MCP elicitation after 2026-07-28: ask for input without a session
An MCP tool sometimes reaches a point where it cannot continue safely. A deployment needs confirmation. A search needs a missing account ID. A provider connection must send the user through an OAuth page. Returning a text message that says "please provide more information" pushes the problem back into the model's conversation and leaves the server with no structured answer.
MCP elicitation gives the server a typed way to ask the client for user input. In the 2026-07-28 protocol revision, that interaction no longer depends on a server pushing a request over a persistent bidirectional session. The handler returns an input_required result. The client collects the requested input, then retries the original operation with the responses attached.
The handler must treat every entry as a fresh request, validate every answer, and keep secrets out of form fields and round-trip state.
The flow is a retry, not a suspended function
Older MCP connections can send elicitation/create from the server to the client while a tool call is running. The modern protocol has a stateless core, so the server returns the work it needs instead:
- The client calls a tool.
- The handler checks which inputs are already available.
- If something is missing, it returns
input_requiredwith one or more named requests. - The client presents those requests to the user.
- The client retries the original tool call with
inputResponsesand the echoedrequestState. - The handler validates the responses and either finishes or requests another round.
No instance must keep a JavaScript stack or an in-memory session alive between steps. Any retry can land on another server instance. The general MCP 2026-07-28 migration checklist covers that wider protocol change; elicitation is the practical user-input part of it.
The TypeScript SDK can automatically fulfil input_required results and retry the call. Its modern client driver defaults to a maximum of 10 rounds. A server should still design for one or two deliberate rounds rather than using the cap as a workflow engine.
Write one handler for every round
A reliable handler first reads accepted input, then asks only for what remains missing. The same code runs on the initial request and every retry.
server.registerTool(
"clear_done",
{ description: "Delete completed tasks after confirmation" },
async (_args, ctx) => {
const answer = acceptedContent(
ctx.mcpReq.inputResponses,
"confirm",
z.object({ confirm: z.boolean() })
);
if (answer?.confirm !== true) {
return inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: "Delete all completed tasks?",
requestedSchema: {
type: "object",
properties: { confirm: { type: "boolean" } },
required: ["confirm"]
}
})
}
});
}
return deleteCompletedTasks();
}
);
The accepted content is untrusted client input. The SDK validates the response's wire shape, but accepted form content is not automatically checked again against the original requested schema. Pass the schema to acceptedContent before using the value.
Keep the request keys stable. A name such as confirm, scope, or account becomes the lookup key in inputResponses. Changing it between rounds makes an accepted answer appear missing and can create a loop.
Use forms for ordinary data, URLs for secrets
Form elicitation uses a restricted JSON Schema. It is suited to flat objects containing primitive fields: strings, numbers with inclusive bounds, booleans, enums, optional values, defaults, and multi-select enum arrays. Nested objects, regular-expression constraints, transforms, and some library-specific refinements do not fit the wire format.
Use an elicitation form for a small decision or missing value. A full settings workflow belongs in the application UI.
Sensitive information must not go through form elicitation. The MCP TypeScript SDK documentation directs servers to URL mode or another out-of-band flow for API keys, payments, OAuth, and similar secrets. URL elicitation lets the client open a secure page while the MCP exchange carries only the request and completion state.
Choose the mode by data sensitivity, not convenience:
| Need | Mode |
|---|---|
| confirmation, rating, scope, non-sensitive identifier | form |
| sign-in, API key, payment, identity verification | URL |
| complex application workflow | URL or existing application UI |
Validate the URL target and bind completion to the correct operation. Credentials, authorization codes, and personal data do not belong in a URL query string or requestState.
Treat decline and cancellation as normal outcomes
User input is optional by nature. A client may return accept, decline, or cancel. A destructive tool should not reinterpret a missing or declined confirmation as permission to continue.
The SDK's acceptedContent helper returns undefined for missing, declined, cancelled, or invalid accepted content. Use inputResponse when the handler must distinguish those states. For example, a first entry may request confirmation, while an explicit decline should return a final cancelled result instead of asking again.
Retries also raise an idempotency problem. The handler may be entered several times, so work performed before input_required must be safe to repeat. Delay side effects until all required input has been validated. If preparation creates a durable draft, store and reuse its identifier rather than creating another draft on each round. The agent-tool interface guide covers idempotency and prepare-then-commit patterns in more detail.
Carry only sealed, minimal state
requestState is an opaque string that the client echoes byte for byte. The server can use it to remember verified progress across sequential rounds, but it comes back through an untrusted client. Protect it with the SDK's request-state codec or an equivalent authenticated encoding before trusting its contents.
Store the least state needed to resume: a workflow version, a draft ID, or a completed-step marker. Do not put secrets, raw form answers, database records, or authorization claims in it. Recheck current permissions and resource state before the final write because both may have changed while the user was responding.
Test both protocol eras
The TypeScript SDK's default legacy shim can serve the same input_required handler to pre-2026-07-28 clients by performing the older push-style requests and re-entering the handler. That reduces duplicate application code, but it does not remove the need for compatibility tests.
Cover at least these cases:
- a modern client accepts the form and the tool finishes;
- the user declines and no side effect occurs;
- accepted content fails schema validation;
- a URL flow completes, expires, or is cancelled;
- the round limit is reached without an infinite loop;
- the legacy shim produces the same business result;
- a retry lands on another server instance;
- tampered
requestStateis rejected.
Record these trajectories in the same evaluation suite used for other agent tools. ISH chat can help compare how models explain the same prompt and result, while the ISH API dashboard keeps repeat API runs visible. The protocol handles the round trip; the application still owns consent, validation, idempotency, and authorization.



