MCP PHP SDK

ElicitationReplay

FinalYes

Answers a handler's {@see \Mcp\Server\ClientGateway::elicit()} on a lifecycle that has no server-initiated requests.

The 2026-07-28 revision carries an ask in the result and lets the client re-send the whole call with the answer, so the blocking call a handler makes cannot block: there is nobody to ask while the request is open. What there is instead is a way to end the request by asking, and to be entered again once the answer exists — which is the same call, resolved one round later.

So an ask is served twice. The first time nothing has been answered, and the gateway's suspension becomes an InputRequiredResult that ends the request. The second time the handler runs from the top again, reaches the same ask, and this time the answer is here, so the call returns it and the handler continues past it as if it had never stopped. A handler written against the handshake era therefore serves this one unchanged — at the price of running once per ask, which is what makes anything it does before its last ask happen once per round.

Answers are kept in the requestState because the client only ever echoes the round it just answered (Protocol), so round three would otherwise no longer know what round one said. That state is signed and not encrypted, and it is the client's own answers travelling back to the client that gave them — but a handler that elicits something it would not hand back should ask for it in one round and use it in that same round.

Tags
author

Christopher Hertel mail@christopher-hertel.de

Table of Contents

Constants

CARRIED_ANSWERS  : mixed = '_mcp.answers'
The `requestState` member carrying answers from earlier rounds. Reserved, like every `_mcp.` key: a handler's own payload travels beside it.

Properties

$answers  : array<string, array<string, mixed>>
$asked  : int
$codec  : RequestStateCodec|null
$payload  : array<string, mixed>
$rejected  : array<string, true>

Methods

__construct()  : mixed
answer()  : array<string, mixed>|null
The answer to `$key`, or null when there is none to give the handler.
ask()  : InputRequiredResult
The result that ends this request by asking, carrying everything already answered so the next round does not have to ask for it again.
key()  : string
The key an ask is filed under: the one the handler named, or its position among this run's asks.
state()  : string|null

Constants

CARRIED_ANSWERS

The `requestState` member carrying answers from earlier rounds. Reserved, like every `_mcp.` key: a handler's own payload travels beside it.

public mixed CARRIED_ANSWERS = '_mcp.answers'

Properties

$answers

private array<string, array<string, mixed>> $answers = []

raw answers, keyed as they were asked

$payload

private array<string, mixed> $payload

the verified state this round arrived with

$rejected

private array<string, true> $rejected = []

keys whose answer this run could not read

Methods

answer()

The answer to `$key`, or null when there is none to give the handler.

public answer(string $key[, ElicitationMode $mode = ElicitationMode::Form ]) : array<string, mixed>|null

Null covers an answer that does not parse as well as one that never arrived: the specification says a server SHOULD ask again for what it still needs, and a malformed answer left the server still needing it.

Parameters
$key : string
$mode : ElicitationMode = ElicitationMode::Form
Return values
array<string, mixed>|null

key()

The key an ask is filed under: the one the handler named, or its position among this run's asks.

public key(string|null $key) : string

Positional keys hold across rounds only because the handler reaches its asks in the same order every time — which it does whenever it is re-enterable at all. A handler whose asks depend on a coin flip should name them.

Parameters
$key : string|null
Return values
string
On this page

Search results