Some handlers cannot finish in one go: they need the user to confirm something, fill in a form, name a directory, or have the client's model draft a paragraph. There are two ways to write that: ask for it, or return the ask.
For elicitation, ask and use the answer:
static function (RequestContext $context): string {
$answer = $context->getClientGateway()->elicit('Your name?', $schema, key: 'who');
return "Hello, {$answer->content['name']}!";
}key names an ask, so its answer keeps finding the question it belongs to. Leave it out and
asks are keyed by position — elicitation_1, elicitation_2, … — which holds as long as the
handler reaches them in the same order every time.
Write the handler so it can run more than once. Some clients answer inside the open request; others answer by re-sending the whole call, which enters your handler again from the top, once per question. Everything above an ask therefore has to be safe to repeat — put side effects after the last one, and re-derive where you are from the answers rather than from anything you kept.
Answers given in an earlier round travel in the requestState, so a handler
asking more than once needs Builder::setRequestState() configured.
examples/server/elicitation
is written this way.
The explicit form: return an InputRequiredResult naming what you need, and read the
answer off RequestContext when the call comes back. It is more to write, and it is the only
way to ask several things in one round trip, to carry your own state, or to ask for
anything other than elicitation.
Revision
2026-07-28. Multi round-trip requests (MRTR) are that revision's feature, and only there does the protocol itself carry this shape. Over a handshake-era connection the SDK emulates it: the input-required shim sends each ask as the realelicitation/create/sampling/createMessage/roots/listand re-enters your handler with the answers. That is on by default and bounded bysetInputRequiredLimits()— each round holds the originating request open, so it holds a worker for as long as the user takes — andwithoutInputRequiredShim()turns it off, after which such a handler fails there. See Server builder.
use Mcp\Schema\Result\CallToolResult;
use Mcp\Schema\Result\InputRequiredResult;
static function (RequestContext $context): CallToolResult|InputRequiredResult {
$answer = $context->getInputContext()?->elicitResult('who');
if (null === $answer) {
return new InputRequiredResult(
['who' => new ElicitRequest('Your name?', $schema)],
requestState: $context->mintRequestState(['asked' => 'who']),
);
}
return new CallToolResult([new TextContent("Hello, {$answer->content['name']}!")]);
}tools/call, prompts/get and resources/read may answer this way; nothing else may.
InputContext hands them back typed — elicitResult(), samplingResult(),
rootsResult() — and returns null for an answer that is absent or malformed. Both mean
the same thing to a handler: ask again. response() is still there for the raw array.
Whatever the server needs to remember between rounds. It travels through the client, so it
is attacker-controlled on return; mintRequestState() seals it with an HMAC and a TTL, and
a state that fails verification never reaches a handler. Configure the key with
Builder::setRequestState():
->setRequestState($_ENV['MCP_REQUEST_STATE_KEY'], ttl: 600)The same key must reach every process that might serve the retry. A per-process random value works only for a single-process deployment. Nothing secret belongs in the payload — it is signed, not encrypted.
A server must not ask for input the client cannot provide. The SDK checks each ask against
the request's declared capabilities and answers -32021 — with the missing set in
data.requiredCapabilities — rather than sending an ask that could never be answered.
Url-mode elicitation needs its own elicitation.url declaration; a bare elicitation means
form mode only.
ClientGateway::sample() and listRoots() belong to the handshake era — revision
2026-07-28 removed both outright, so calling one there raises a LogicException. Take what
they gave you from tool arguments, resource URIs or server configuration instead. elicit()
and elicitUrl() are unaffected: elicitation survived that revision, as an ask carried in the
result.
Nothing above forks on it. elicit() works the same on every revision — only the mechanics
underneath differ, and the SDK picks them. The one thing to keep in mind is the rule already
stated: a handler that asks may be entered again from the top, so let it repeat safely.