[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
2 changed files with 495 additions and 1 deletions

View file

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mcp\Client\Handler\Request;
use Mcp\Schema\Request\ElicitRequest;
use Mcp\Schema\Result\ElicitResult;
/**
* Contract for callbacks used by ElicitationRequestHandler.
*
* Implementations present the requested schema to the user and collect their
* response when the server sends an elicitation/create request.
*/
interface ElicitationCallbackInterface
{
public function __invoke(ElicitRequest $request): ElicitResult;
}

View file

@ -0,0 +1,68 @@
<?php
/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mcp\Client\Handler\Request;
use Mcp\Exception\ElicitationException;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ElicitRequest;
use Mcp\Schema\Result\ElicitResult;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* Handler for elicitation requests from the server.
*
* The MCP server may request additional information from the user during tool
* execution. This handler wraps a user-provided callback that presents the
* requested schema to the user and returns their response.
*
* @implements RequestHandlerInterface<ElicitResult>
*
* @author Johannes Wachter <johannes@sulu.io>
*/
class ElicitationRequestHandler implements RequestHandlerInterface
{
public function __construct(
private readonly ElicitationCallbackInterface $callback,
private readonly LoggerInterface $logger = new NullLogger(),
) {
}
public function supports(Request $request): bool
{
return $request instanceof ElicitRequest;
}
/**
* @return Response<ElicitResult>|Error
*/
public function handle(Request $request): Response|Error
{
\assert($request instanceof ElicitRequest);
try {
$result = $this->callback->__invoke($request);
return new Response($request->getId(), $result);
} catch (ElicitationException $e) {
$this->logger->error('Elicitation failed: '.$e->getMessage(), ['exception' => $e]);
return Error::forInternalError($e->getMessage(), $request->getId());
} catch (\Throwable $e) {
$this->logger->error('Unexpected error during elicitation', ['exception' => $e]);
return Error::forInternalError('Error while processing elicitation', $request->getId());
}
}
}