Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ $client->disconnect();
- **Resource Access**: Read static and dynamic resources
- **Prompt Management**: List and retrieve prompt templates
- **Completion Support**: Request argument completion suggestions
- **Sampling & Elicitation**: Respond to server-initiated LLM sampling and user-input requests

### Advanced Features

Expand All @@ -233,6 +234,15 @@ $client = Client::builder()
->build();
```

- **Elicitation Support**: Respond to server requests for user input
```php
$elicitationHandler = new ElicitationRequestHandler($myCallback);
$client = Client::builder()
->setCapabilities(new ClientCapabilities(elicitation: true))
->addRequestHandler($elicitationHandler)
->build();
```

- **Logging Notifications**: Receive server log messages
```php
$loggingHandler = new LoggingNotificationHandler($myCallback);
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This roadmap is a living document that outlines the planned features and improve
## Goals for the First Major Release

- **Server**
- [ ] Implement full support for elicitations
- [x] Implement full support for elicitations
- [ ] Implement OAuth2 authentication for server
- **Client**
- [x] Implement client-side support
Expand Down
66 changes: 66 additions & 0 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
151 changes: 151 additions & 0 deletions examples/client/stdio_elicitation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?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.
*/

/**
* STDIO Client Elicitation Example.
*
* This example demonstrates how a client responds to server-initiated
* elicitation/create requests. The server's tools ask the user for additional
* information mid-execution; the client below prompts interactively on STDIN
* and accepts defaults when the user presses Enter.
*
* Run against the elicitation demo server:
* php examples/client/stdio_elicitation.php
*/

require_once __DIR__.'/../../vendor/autoload.php';

use Mcp\Client;
use Mcp\Client\Handler\Request\ElicitationCallbackInterface;
use Mcp\Client\Handler\Request\ElicitationRequestHandler;
use Mcp\Client\Transport\StdioTransport;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Elicitation\BooleanSchemaDefinition;
use Mcp\Schema\Elicitation\EnumSchemaDefinition;
use Mcp\Schema\Elicitation\NumberSchemaDefinition;
use Mcp\Schema\Elicitation\StringSchemaDefinition;
use Mcp\Schema\Enum\ElicitAction;
use Mcp\Schema\Request\ElicitRequest;
use Mcp\Schema\Result\ElicitResult;

$elicitationRequestHandler = new ElicitationRequestHandler(new class implements ElicitationCallbackInterface {
public function __invoke(ElicitRequest $request): ElicitResult
{
echo "\n[ELICIT] {$request->message}\n";

$content = [];
foreach ($request->requestedSchema->properties as $name => $definition) {
$default = $this->defaultFor($definition);
$label = $this->labelFor($definition, $name);

if (null !== $default) {
$display = is_bool($default) ? ($default ? 'true' : 'false') : (string) $default;
echo " {$label} [{$display}]: ";
} else {
echo " {$label}: ";
}

$rawInput = fgets(\STDIN);
$input = false === $rawInput ? '' : trim($rawInput);
$value = '' === $input ? $default : $this->cast($definition, $input);

$content[$name] = $value;
}

return new ElicitResult(ElicitAction::Accept, $content);
}

private function defaultFor(object $definition): mixed
{
return match (true) {
$definition instanceof EnumSchemaDefinition => $definition->default ?? $definition->enum[0],
$definition instanceof NumberSchemaDefinition => $definition->default ?? $definition->minimum ?? ($definition->integerOnly ? 1 : 1.0),
$definition instanceof BooleanSchemaDefinition => $definition->default ?? false,
$definition instanceof StringSchemaDefinition => $definition->default ?? ('date' === $definition->format ? date('Y-m-d') : ''),
default => null,
};
}

private function labelFor(object $definition, string $name): string
{
$title = match (true) {
$definition instanceof EnumSchemaDefinition => $definition->title,
$definition instanceof NumberSchemaDefinition => $definition->title,
$definition instanceof BooleanSchemaDefinition => $definition->title,
$definition instanceof StringSchemaDefinition => $definition->title,
default => null,
};

return $title ?? $name;
}

private function cast(object $definition, string $input): mixed
{
return match (true) {
$definition instanceof BooleanSchemaDefinition => filter_var($input, \FILTER_VALIDATE_BOOLEAN),
$definition instanceof NumberSchemaDefinition => $definition->integerOnly ? (int) $input : (float) $input,
default => $input,
};
}
});

$client = Client::builder()
->setClientInfo('STDIO Elicitation Test', '1.0.0')
->setInitTimeout(30)
->setRequestTimeout(120)
->setCapabilities(new ClientCapabilities(elicitation: true))
->addRequestHandler($elicitationRequestHandler)
->build();

$transport = new StdioTransport(
command: 'php',
args: [__DIR__.'/../server/elicitation/server.php'],
);

try {
echo "Connecting to MCP server...\n";
$client->connect($transport);

$serverInfo = $client->getServerInfo();
echo 'Connected to: '.($serverInfo->name ?? 'unknown')."\n\n";

echo "Calling 'book_restaurant'...\n";
$result = $client->callTool(
name: 'book_restaurant',
arguments: ['restaurantName' => 'The Test Kitchen'],
);

echo "\nResult:\n";
foreach ($result->content as $content) {
if ($content instanceof TextContent) {
echo $content->text."\n";
}
}

echo "\nCalling 'confirm_action'...\n";
$result = $client->callTool(
name: 'confirm_action',
arguments: ['actionDescription' => 'Delete all temporary files'],
);

echo "\nResult:\n";
foreach ($result->content as $content) {
if ($content instanceof TextContent) {
echo $content->text."\n";
}
}
} catch (Throwable $e) {
echo "Error: {$e->getMessage()}\n";
echo $e->getTraceAsString()."\n";
} finally {
$client->disconnect();
}
26 changes: 26 additions & 0 deletions src/Client/Handler/Request/ElicitationCallbackInterface.php
Original file line number Diff line number Diff line change
@@ -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;
}
68 changes: 68 additions & 0 deletions src/Client/Handler/Request/ElicitationRequestHandler.php
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
24 changes: 24 additions & 0 deletions src/Exception/ElicitationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?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\Exception;

/**
* Exception thrown when an elicitation request fails.
*
* When thrown from an elicitation callback, this exception's message will be
* included in the error response sent back to the server.
*
* @author Johannes Wachter <johannes@sulu.io>
*/
final class ElicitationException extends \RuntimeException implements ExceptionInterface
{
}
1 change: 1 addition & 0 deletions src/JsonRpc/MessageFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ final class MessageFactory
Schema\Request\CallToolRequest::class,
Schema\Request\CompletionCompleteRequest::class,
Schema\Request\CreateSamplingMessageRequest::class,
Schema\Request\ElicitRequest::class,
Schema\Request\GetPromptRequest::class,
Schema\Request\InitializeRequest::class,
Schema\Request\ListPromptsRequest::class,
Expand Down
Loading