* [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>
138 lines
4.5 KiB
PHP
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;
|
|
}
|
|
};
|
|
}
|
|
}
|