php-sdk/tests/Unit/Client/Handler/Request/ElicitationRequestHandlerTest.php
Johannes Wachter e4a8a224ff
[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>
2026-07-14 23:55:06 +02:00

138 lines
4.5 KiB
PHP

<?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\Tests\Unit\Client\Handler\Request;
use Mcp\Client\Handler\Request\ElicitationCallbackInterface;
use Mcp\Client\Handler\Request\ElicitationRequestHandler;
use Mcp\Exception\ElicitationException;
use Mcp\Schema\Enum\ElicitAction;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ElicitRequest;
use Mcp\Schema\Request\PingRequest;
use Mcp\Schema\Result\ElicitResult;
use PHPUnit\Framework\TestCase;
class ElicitationRequestHandlerTest extends TestCase
{
public function testSupportsElicitRequest(): void
{
$handler = new ElicitationRequestHandler($this->callbackReturning(
new ElicitResult(ElicitAction::Decline),
));
$this->assertTrue($handler->supports($this->createElicitRequest()));
}
public function testDoesNotSupportOtherRequests(): void
{
$handler = new ElicitationRequestHandler($this->callbackReturning(
new ElicitResult(ElicitAction::Decline),
));
$ping = PingRequest::fromArray([
'jsonrpc' => '2.0',
'method' => PingRequest::getMethod(),
'id' => 'ping-1',
]);
$this->assertFalse($handler->supports($ping));
}
public function testHandleReturnsResponseOnAccept(): void
{
$result = new ElicitResult(ElicitAction::Accept, ['name' => 'Ada']);
$handler = new ElicitationRequestHandler($this->callbackReturning($result));
$request = $this->createElicitRequest();
$response = $handler->handle($request);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame($request->getId(), $response->id);
$this->assertSame($result, $response->result);
}
public function testHandleReturnsErrorOnElicitationException(): void
{
$handler = new ElicitationRequestHandler($this->callbackThrowing(
new ElicitationException('user input unavailable'),
));
$request = $this->createElicitRequest();
$response = $handler->handle($request);
$this->assertInstanceOf(Error::class, $response);
$this->assertSame($request->getId(), $response->getId());
$this->assertSame('user input unavailable', $response->message);
}
public function testHandleReturnsGenericErrorOnThrowable(): void
{
$handler = new ElicitationRequestHandler($this->callbackThrowing(
new \RuntimeException('boom'),
));
$request = $this->createElicitRequest();
$response = $handler->handle($request);
$this->assertInstanceOf(Error::class, $response);
$this->assertSame($request->getId(), $response->getId());
$this->assertSame('Error while processing elicitation', $response->message);
}
private function createElicitRequest(): ElicitRequest
{
return ElicitRequest::fromArray([
'jsonrpc' => '2.0',
'method' => ElicitRequest::getMethod(),
'id' => 'elicit-'.uniqid(),
'params' => [
'message' => 'Please provide your name.',
'requestedSchema' => [
'type' => 'object',
'properties' => [
'name' => ['type' => 'string', 'title' => 'Name'],
],
'required' => ['name'],
],
],
]);
}
private function callbackReturning(ElicitResult $result): ElicitationCallbackInterface
{
return new class($result) implements ElicitationCallbackInterface {
public function __construct(private readonly ElicitResult $result)
{
}
public function __invoke(ElicitRequest $request): ElicitResult
{
return $this->result;
}
};
}
private function callbackThrowing(\Throwable $exception): ElicitationCallbackInterface
{
return new class($exception) implements ElicitationCallbackInterface {
public function __construct(private readonly \Throwable $exception)
{
}
public function __invoke(ElicitRequest $request): ElicitResult
{
throw $this->exception;
}
};
}
}