[Client] Add client-side elicitation support (#371)

* [Client] Add client-side elicitation support

The SDK could send elicitation/create requests from a server tool via
ClientGateway::elicit(), but the client could not receive them: there was
no request handler, and ElicitRequest was not registered in MessageFactory,
so incoming elicitation/create messages failed to parse.

This adds the client half, mirroring the existing Sampling pattern:

- Register ElicitRequest in MessageFactory so elicitation/create parses.
- ElicitationRequestHandler wraps a user callback and returns an
  ElicitResult (accept/decline/cancel) or an Error.
- ElicitationCallbackInterface defines the callback contract.
- ElicitationException forwards a specific message to the server; any
  other throwable returns a generic error.

Includes unit tests, a runnable client example against the elicitation
demo server, and client docs/README updates.

* [Client] Simplify elicitation example labelFor() per review

Type the parameter as AbstractSchemaDefinition and read ->title directly
instead of matching on each concrete definition type. title is a required
non-null string on the base class, so the match and the $name fallback were
redundant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Johannes Wachter 2026-07-14 23:55:06 +02:00 • committed by GitHub
commit e4a8a224ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -534,6 +534,72 @@ $client = Client::builder()
> throw new \RuntimeException('Rate limit exceeded');
> ```
### Elicitation (User Input Requests)
Handle server requests to elicit additional information from the user during tool
execution. The server sends an `elicitation/create` request describing the fields it
needs; your callback presents them to the user and returns an `ElicitResult` with one of
three actions — accept (with the collected content), decline, or cancel:
```php
use Mcp\Client\Handler\Request\ElicitationRequestHandler;
use Mcp\Client\Handler\Request\ElicitationCallbackInterface;
use Mcp\Exception\ElicitationException;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Enum\ElicitAction;
use Mcp\Schema\Request\ElicitRequest;
use Mcp\Schema\Result\ElicitResult;
class ConsoleElicitationCallback implements ElicitationCallbackInterface
{
public function __invoke(ElicitRequest $request): ElicitResult
{
echo $request->message.\PHP_EOL;
// Present $request->requestedSchema->properties to the user and collect input.
$content = [];
foreach ($request->requestedSchema->properties as $name => $definition) {
$answer = readline($definition->title.': ');
if (false === $answer) {
// No input available — let the server know the user cancelled.
return new ElicitResult(ElicitAction::Cancel);
}
$content[$name] = $answer;
}
return new ElicitResult(ElicitAction::Accept, $content);
}
}
$client = Client::builder()
->setCapabilities(new ClientCapabilities(elicitation: true))
->addRequestHandler(new ElicitationRequestHandler(new ConsoleElicitationCallback))
->build();
```
Return `new ElicitResult(ElicitAction::Decline)` when the user refuses to provide the
information, and `new ElicitResult(ElicitAction::Cancel)` when they dismiss the request.
Only the `Accept` action carries content.
> [!IMPORTANT]
> **Error Handling in Elicitation Callbacks:**
>
> - **Throw `ElicitationException`** to forward a specific error message to the server
> - **Any other exception** is logged but returns a generic error to the server
>
> ```php
> // Good: Server receives "No interactive console available" message
> throw new ElicitationException('No interactive console available');
>
> // Bad: Server receives generic "Error while processing elicitation" message
> throw new \RuntimeException('No interactive console available');
> ```
See `examples/client/stdio_elicitation.php` for a runnable example against the
elicitation demo server.
## Error Handling
The client throws exceptions for various error conditions: