Skip to content

Latest commit

 

History

History
111 lines (83 loc) · 4.95 KB

File metadata and controls

111 lines (83 loc) · 4.95 KB

Asking for input

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.

Just asking

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.

Returning the ask

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 real elicitation/create / sampling/createMessage / roots/list and re-enters your handler with the answers. That is on by default and bounded by setInputRequiredLimits() — each round holds the originating request open, so it holds a worker for as long as the user takes — and withoutInputRequiredShim() 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.

Reading the answers

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.

requestState

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.

Capabilities

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.

What not to call

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.

Which revision called

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.