Skip to content
Draft
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"Mcp\\Example\\Server\\ClientCommunication\\": "examples/server/client-communication/",
"Mcp\\Example\\Server\\ClientLogging\\": "examples/server/client-logging/",
"Mcp\\Example\\Server\\CombinedRegistration\\": "examples/server/combined-registration/",
"Mcp\\Example\\Server\\Elicitation\\": "examples/server/elicitation/",
"Mcp\\Example\\Server\\ComplexToolSchema\\": "examples/server/complex-tool-schema/",
"Mcp\\Example\\Server\\Conformance\\": "examples/server/conformance/",
"Mcp\\Example\\Server\\CustomDependencies\\": "examples/server/custom-dependencies/",
Expand Down
275 changes: 275 additions & 0 deletions examples/server/elicitation/ElicitationHandlers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
<?php

declare(strict_types=1);

/*
* 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\Example\Server\Elicitation;

use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Elicitation\BooleanSchemaDefinition;
use Mcp\Schema\Elicitation\ElicitationSchema;
use Mcp\Schema\Elicitation\EnumSchemaDefinition;
use Mcp\Schema\Elicitation\NumberSchemaDefinition;
use Mcp\Schema\Elicitation\StringSchemaDefinition;
use Mcp\Server\RequestContext;
use Psr\Log\LoggerInterface;

/**
* Example handlers demonstrating the elicitation feature.
*
* Elicitation allows servers to request additional information from users
* during tool execution. The user can accept (providing data), decline,
* or cancel the request.
*/
final class ElicitationHandlers
{
public function __construct(
private readonly LoggerInterface $logger,
) {
$this->logger->info('ElicitationHandlers instantiated.');
}

/**
* Check if the client supports elicitation.
*/
private function clientSupportsElicitation(RequestContext $context): bool
{
$capabilities = $context->getSession()->get('client_capabilities', []);

// MCP spec: capability presence indicates support (value is typically {} or [])
return \array_key_exists('elicitation', $capabilities);
}

/**
* Book a restaurant reservation with user elicitation.
*
* Demonstrates multi-field elicitation with different field types:
* - Number field for party size with validation
* - String field with date format for reservation date
* - Enum field for dietary restrictions with human-readable labels
*
* @return array{status: string, message: string, booking?: array{party_size: int, date: string, dietary: string}}
*/
#[McpTool('book_restaurant', 'Book a restaurant reservation, collecting details via elicitation.')]
public function bookRestaurant(RequestContext $context, string $restaurantName): array
{
if (!$this->clientSupportsElicitation($context)) {
return [
'status' => 'error',
'message' => 'Client does not support elicitation. Please provide reservation details (party_size, date, dietary) as tool parameters instead.',
];
}

$client = $context->getClientGateway();

$this->logger->info(\sprintf('Starting reservation process for restaurant: %s', $restaurantName));

$schema = new ElicitationSchema(
properties: [
'party_size' => new NumberSchemaDefinition(
title: 'Party Size',
integerOnly: true,
description: 'Number of guests in your party',
default: 2,
minimum: 1,
maximum: 20,
),
'date' => new StringSchemaDefinition(
title: 'Reservation Date',
description: 'Preferred date for your reservation',
format: 'date',
),
'dietary' => new EnumSchemaDefinition(
title: 'Dietary Restrictions',
enum: ['none', 'vegetarian', 'vegan', 'gluten-free', 'halal', 'kosher'],
description: 'Any dietary restrictions or preferences',
default: 'none',
enumNames: ['None', 'Vegetarian', 'Vegan', 'Gluten-Free', 'Halal', 'Kosher'],
),
],
required: ['party_size', 'date'],
);

$result = $client->elicit(
message: \sprintf('Please provide your reservation details for %s:', $restaurantName),
requestedSchema: $schema,
timeout: 120,
);

if ($result->isDeclined()) {
$this->logger->info('User declined to provide reservation details.');

return [
'status' => 'declined',
'message' => 'Reservation request was declined by user.',
];
}

if ($result->isCancelled()) {
$this->logger->info('User cancelled the reservation request.');

return [
'status' => 'cancelled',
'message' => 'Reservation request was cancelled.',
];
}

$content = $result->content ?? [];
$partySize = (int) ($content['party_size'] ?? 2);
$date = (string) ($content['date'] ?? '');
$dietary = (string) ($content['dietary'] ?? 'none');

$this->logger->info(\sprintf(
'Booking confirmed: %d guests on %s with %s dietary requirements',
$partySize,
$date,
$dietary,
));

return [
'status' => 'confirmed',
'message' => \sprintf(
'Reservation confirmed at %s for %d guests on %s.',
$restaurantName,
$partySize,
$date,
),
'booking' => [
'party_size' => $partySize,
'date' => $date,
'dietary' => $dietary,
],
];
}

/**
* Confirm an action with a simple boolean elicitation.
*
* Demonstrates the simplest elicitation pattern - a yes/no confirmation.
*
* @return array{status: string, message: string}
*/
#[McpTool('confirm_action', 'Request user confirmation before proceeding with an action.')]
public function confirmAction(RequestContext $context, string $actionDescription): array
{
if (!$this->clientSupportsElicitation($context)) {
return [
'status' => 'error',
'message' => 'Client does not support elicitation. Please confirm the action explicitly in your request.',
];
}

$client = $context->getClientGateway();

$schema = new ElicitationSchema(
properties: [
'confirm' => new BooleanSchemaDefinition(
title: 'Confirm',
description: 'Check to confirm you want to proceed',
default: false,
),
],
required: ['confirm'],
);

$result = $client->elicit(
message: \sprintf('Are you sure you want to: %s?', $actionDescription),
requestedSchema: $schema,
);

if (!$result->isAccepted()) {
return [
'status' => 'not_confirmed',
'message' => 'Action was not confirmed by user.',
];
}

$confirmed = (bool) ($result->content['confirm'] ?? false);

if (!$confirmed) {
return [
'status' => 'not_confirmed',
'message' => 'User did not check the confirmation box.',
];
}

$this->logger->info(\sprintf('User confirmed action: %s', $actionDescription));

return [
'status' => 'confirmed',
'message' => \sprintf('Action confirmed: %s', $actionDescription),
];
}

/**
* Collect user feedback using elicitation.
*
* Demonstrates elicitation with optional fields and enum with labels.
*
* @return array{status: string, message: string, feedback?: array{rating: string, comments: string}}
*/
#[McpTool('collect_feedback', 'Collect user feedback via elicitation form.')]
public function collectFeedback(RequestContext $context, string $topic): array
{
if (!$this->clientSupportsElicitation($context)) {
return [
'status' => 'error',
'message' => 'Client does not support elicitation. Please provide feedback (rating 1-5, comments) as tool parameters instead.',
];
}

$client = $context->getClientGateway();

$schema = new ElicitationSchema(
properties: [
'rating' => new EnumSchemaDefinition(
title: 'Rating',
enum: ['1', '2', '3', '4', '5'],
description: 'Rate your experience from 1 (poor) to 5 (excellent)',
enumNames: ['1 - Poor', '2 - Fair', '3 - Good', '4 - Very Good', '5 - Excellent'],
),
'comments' => new StringSchemaDefinition(
title: 'Comments',
description: 'Any additional comments or suggestions (optional)',
maxLength: 500,
),
],
required: ['rating'],
);

$result = $client->elicit(
message: \sprintf('Please provide your feedback about: %s', $topic),
requestedSchema: $schema,
);

if (!$result->isAccepted()) {
return [
'status' => 'skipped',
'message' => 'User chose not to provide feedback.',
];
}

$content = $result->content ?? [];
$rating = (string) ($content['rating'] ?? '3');
$comments = (string) ($content['comments'] ?? '');

$this->logger->info(\sprintf('Feedback received: rating=%s, comments=%s', $rating, $comments));

return [
'status' => 'received',
'message' => 'Thank you for your feedback!',
'feedback' => [
'rating' => $rating,
'comments' => $comments,
],
];
}
}
61 changes: 61 additions & 0 deletions examples/server/elicitation/server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env php
<?php

declare(strict_types=1);

/*
* 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.
*/

/**
* MCP Elicitation Example Server
*
* This example demonstrates the elicitation feature which allows servers to
* request additional information from users during tool execution.
*
* Elicitation enables interactive workflows where the server can:
* - Ask for user preferences or choices
* - Collect form data with validated fields
* - Request confirmation before actions
*
* The server provides three example tools:
* 1. book_restaurant - Multi-field form with number, date, and enum fields
* 2. confirm_action - Simple boolean confirmation dialog
* 3. collect_feedback - Rating and comments form with optional fields
*
* IMPORTANT: Elicitation requires:
* - A session store (FileSessionStore is used here)
* - Client support for elicitation (check client capabilities)
*
* Usage:
* php server.php
*
* The server will start in stdio mode and wait for MCP client connections.
*/

require_once dirname(__DIR__).'/bootstrap.php';
chdir(__DIR__);

use Mcp\Schema\ServerCapabilities;
use Mcp\Server;
use Mcp\Server\Session\FileSessionStore;

$server = Server::builder()
->setServerInfo('Elicitation Demo', '1.0.0')
->setLogger(logger())
->setContainer(container())
// Session store is REQUIRED for server-to-client requests like elicitation
->setSession(new FileSessionStore(__DIR__.'/sessions'))
->setCapabilities(new ServerCapabilities(logging: true, tools: true))
// Auto-discover tools from ElicitationHandlers class
->setDiscovery(__DIR__)
->build();

$result = $server->run(transport());

shutdown($result);
13 changes: 13 additions & 0 deletions src/Schema/ClientCapabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function __construct(
public readonly ?bool $roots = false,
public readonly ?bool $rootsListChanged = null,
public readonly ?bool $sampling = null,
public readonly ?bool $elicitation = null,
public readonly ?array $experimental = null,
) {
}
Expand All @@ -36,6 +37,7 @@ public function __construct(
* listChanged?: bool,
* },
* sampling?: bool,
* elicitation?: bool,
* experimental?: array<string, mixed>,
* } $data
*/
Expand All @@ -56,10 +58,16 @@ public static function fromArray(array $data): self
$sampling = true;
}

$elicitation = null;
if (isset($data['elicitation'])) {
$elicitation = true;
}

return new self(
$rootsEnabled,
$rootsListChanged,
$sampling,
$elicitation,
$data['experimental'] ?? null
);
}
Expand All @@ -68,6 +76,7 @@ public static function fromArray(array $data): self
* @return array{
* roots?: object,
* sampling?: object,
* elicitation?: object,
* experimental?: object,
* }
*/
Expand All @@ -85,6 +94,10 @@ public function jsonSerialize(): array
$data['sampling'] = new \stdClass();
}

if ($this->elicitation) {
$data['elicitation'] = new \stdClass();
}

if ($this->experimental) {
$data['experimental'] = (object) $this->experimental;
}
Expand Down
Loading
Loading