Skip to content

Commit fa8a4b9

Browse files
Improve elicitation SDK design and documentation
Move supportsElicitation() capability check from example code to the ClientGateway class as a public method, making it part of the official SDK API. This eliminates code duplication and provides a consistent way for all tools to check client capabilities. - Add ClientGateway::supportsElicitation() public method - Remove duplicate capability check from ElicitationHandlers example - Update all example tools to use SDK method via ClientGateway - Add @author tags to all 6 elicitation schema classes - Remove inline comments from example server (keep header doc) - Add comprehensive elicitation section to examples.md with usage examples, important notes, and tool descriptions
1 parent 5f03645 commit fa8a4b9

File tree

13 files changed

+128
-74
lines changed

13 files changed

+128
-74
lines changed

docs/examples.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,80 @@ public function formatText(
276276
string $format = 'sentence'
277277
): array
278278
```
279+
280+
### Elicitation
281+
282+
**File**: `examples/server/elicitation/`
283+
284+
**What it demonstrates:**
285+
- Server-to-client elicitation requests
286+
- Interactive user input during tool execution
287+
- Multi-field form schemas with validation
288+
- Boolean confirmation dialogs
289+
- Enum fields with human-readable labels
290+
- Handling accept/decline/cancel responses
291+
- Session persistence requirement for server-initiated requests
292+
293+
**Key Features:**
294+
```php
295+
// Check client support before eliciting
296+
if (!$context->getClientGateway()->supportsElicitation()) {
297+
return ['status' => 'error', 'message' => 'Client does not support elicitation'];
298+
}
299+
300+
// Build schema with multiple field types
301+
$schema = new ElicitationSchema(
302+
properties: [
303+
'party_size' => new NumberSchemaDefinition(
304+
title: 'Party Size',
305+
integerOnly: true,
306+
minimum: 1,
307+
maximum: 20
308+
),
309+
'date' => new StringSchemaDefinition(
310+
title: 'Reservation Date',
311+
format: 'date'
312+
),
313+
'dietary' => new EnumSchemaDefinition(
314+
title: 'Dietary Restrictions',
315+
enum: ['none', 'vegetarian', 'vegan'],
316+
enumNames: ['None', 'Vegetarian', 'Vegan']
317+
),
318+
],
319+
required: ['party_size', 'date']
320+
);
321+
322+
// Send elicitation request
323+
$result = $client->elicit(
324+
message: 'Please provide your reservation details',
325+
requestedSchema: $schema
326+
);
327+
328+
// Handle response
329+
if ($result->isAccepted()) {
330+
$data = $result->content; // User-provided data
331+
} elseif ($result->isDeclined() || $result->isCancelled()) {
332+
// User declined or cancelled
333+
}
334+
```
335+
336+
**Important Notes:**
337+
- Elicitation requires a session store (e.g., `FileSessionStore`)
338+
- Check client capabilities with `supportsElicitation()` before sending requests
339+
- Schema supports primitive types: string, number/integer, boolean, enum
340+
- String fields support format validation: date, date-time, email, uri
341+
- Users can accept (providing data), decline, or cancel requests
342+
343+
**Usage:**
344+
```bash
345+
# Interactive testing with MCP client that supports elicitation
346+
npx @modelcontextprotocol/inspector php examples/server/elicitation/server.php
347+
348+
# Test with Goose (confirmed working by reviewer)
349+
# Or configure in Claude Desktop or other MCP clients
350+
```
351+
352+
**Example Tools:**
353+
1. **book_restaurant** - Multi-field reservation form with number, date, and enum fields
354+
2. **confirm_action** - Simple boolean confirmation dialog
355+
3. **collect_feedback** - Rating and comments form with optional fields

examples/server/elicitation/ElicitationHandlers.php

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,6 @@ public function __construct(
3737
$this->logger->info('ElicitationHandlers instantiated.');
3838
}
3939

40-
/**
41-
* Check if the client supports elicitation.
42-
*/
43-
private function clientSupportsElicitation(RequestContext $context): bool
44-
{
45-
$capabilities = $context->getSession()->get('client_capabilities', []);
46-
47-
// MCP spec: capability presence indicates support (value is typically {} or [])
48-
return \array_key_exists('elicitation', $capabilities);
49-
}
50-
5140
/**
5241
* Book a restaurant reservation with user elicitation.
5342
*
@@ -61,7 +50,7 @@ private function clientSupportsElicitation(RequestContext $context): bool
6150
#[McpTool('book_restaurant', 'Book a restaurant reservation, collecting details via elicitation.')]
6251
public function bookRestaurant(RequestContext $context, string $restaurantName): array
6352
{
64-
if (!$this->clientSupportsElicitation($context)) {
53+
if (!$context->getClientGateway()->supportsElicitation()) {
6554
return [
6655
'status' => 'error',
6756
'message' => 'Client does not support elicitation. Please provide reservation details (party_size, date, dietary) as tool parameters instead.',
@@ -160,7 +149,7 @@ enumNames: ['None', 'Vegetarian', 'Vegan', 'Gluten-Free', 'Halal', 'Kosher'],
160149
#[McpTool('confirm_action', 'Request user confirmation before proceeding with an action.')]
161150
public function confirmAction(RequestContext $context, string $actionDescription): array
162151
{
163-
if (!$this->clientSupportsElicitation($context)) {
152+
if (!$context->getClientGateway()->supportsElicitation()) {
164153
return [
165154
'status' => 'error',
166155
'message' => 'Client does not support elicitation. Please confirm the action explicitly in your request.',
@@ -219,7 +208,7 @@ public function confirmAction(RequestContext $context, string $actionDescription
219208
#[McpTool('collect_feedback', 'Collect user feedback via elicitation form.')]
220209
public function collectFeedback(RequestContext $context, string $topic): array
221210
{
222-
if (!$this->clientSupportsElicitation($context)) {
211+
if (!$context->getClientGateway()->supportsElicitation()) {
223212
return [
224213
'status' => 'error',
225214
'message' => 'Client does not support elicitation. Please provide feedback (rating 1-5, comments) as tool parameters instead.',

examples/server/elicitation/server.php

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,10 @@
1313
*/
1414

1515
/**
16-
* MCP Elicitation Example Server
16+
* MCP Elicitation Example Server.
1717
*
18-
* This example demonstrates the elicitation feature which allows servers to
19-
* request additional information from users during tool execution.
20-
*
21-
* Elicitation enables interactive workflows where the server can:
22-
* - Ask for user preferences or choices
23-
* - Collect form data with validated fields
24-
* - Request confirmation before actions
25-
*
26-
* The server provides three example tools:
27-
* 1. book_restaurant - Multi-field form with number, date, and enum fields
28-
* 2. confirm_action - Simple boolean confirmation dialog
29-
* 3. collect_feedback - Rating and comments form with optional fields
30-
*
31-
* IMPORTANT: Elicitation requires:
32-
* - A session store (FileSessionStore is used here)
33-
* - Client support for elicitation (check client capabilities)
34-
*
35-
* Usage:
36-
* php server.php
37-
*
38-
* The server will start in stdio mode and wait for MCP client connections.
18+
* Demonstrates server-to-client elicitation for interactive user input during tool execution.
19+
* See docs/examples.md for detailed documentation and usage examples.
3920
*/
4021

4122
require_once dirname(__DIR__).'/bootstrap.php';
@@ -49,10 +30,8 @@
4930
->setServerInfo('Elicitation Demo', '1.0.0')
5031
->setLogger(logger())
5132
->setContainer(container())
52-
// Session store is REQUIRED for server-to-client requests like elicitation
5333
->setSession(new FileSessionStore(__DIR__.'/sessions'))
5434
->setCapabilities(new ServerCapabilities(logging: true, tools: true))
55-
// Auto-discover tools from ElicitationHandlers class
5635
->setDiscovery(__DIR__)
5736
->build();
5837

src/Schema/Elicitation/BooleanSchemaDefinition.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
/**
1919
* Schema definition for boolean fields in elicitation requests.
2020
*
21-
* @author
21+
* @author Johannes Wachter <johannes@sulu.io>
2222
*/
2323
final class BooleanSchemaDefinition implements \JsonSerializable
2424
{

src/Schema/Elicitation/ElicitationSchema.php

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*
2121
* Represents an object schema with primitive property definitions and optional required fields.
2222
*
23-
* @author
23+
* @author Johannes Wachter <johannes@sulu.io>
2424
*/
2525
final class ElicitationSchema implements \JsonSerializable
2626
{
@@ -38,10 +38,7 @@ public function __construct(
3838

3939
foreach ($required as $name) {
4040
if (!\array_key_exists($name, $properties)) {
41-
throw new InvalidArgumentException(\sprintf(
42-
'Required property "%s" is not defined in properties.',
43-
$name
44-
));
41+
throw new InvalidArgumentException(\sprintf('Required property "%s" is not defined in properties.', $name));
4542
}
4643
}
4744
}
@@ -68,10 +65,7 @@ public static function fromArray(array $data): self
6865
$properties = [];
6966
foreach ($data['properties'] as $name => $propertyData) {
7067
if (!\is_array($propertyData)) {
71-
throw new InvalidArgumentException(\sprintf(
72-
'Property "%s" must be an array.',
73-
$name
74-
));
68+
throw new InvalidArgumentException(\sprintf('Property "%s" must be an array.', $name));
7569
}
7670
$properties[$name] = PrimitiveSchemaDefinition::fromArray($propertyData);
7771
}

src/Schema/Elicitation/EnumSchemaDefinition.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*
2121
* Provides a list of allowed values with optional human-readable labels.
2222
*
23-
* @author
23+
* @author Johannes Wachter <johannes@sulu.io>
2424
*/
2525
final class EnumSchemaDefinition implements \JsonSerializable
2626
{
@@ -53,10 +53,7 @@ public function __construct(
5353
}
5454

5555
if (null !== $default && !\in_array($default, $enum, true)) {
56-
throw new InvalidArgumentException(\sprintf(
57-
'Default value "%s" is not in the enum array.',
58-
$default
59-
));
56+
throw new InvalidArgumentException(\sprintf('Default value "%s" is not in the enum array.', $default));
6057
}
6158
}
6259

src/Schema/Elicitation/NumberSchemaDefinition.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@
2020
*
2121
* Supports minimum and maximum value constraints.
2222
*
23-
* @author
23+
* @author Johannes Wachter <johannes@sulu.io>
2424
*/
2525
final class NumberSchemaDefinition implements \JsonSerializable
2626
{
2727
/**
28-
* @param string $title Human-readable title for the field
29-
* @param bool $integerOnly Whether to restrict to integer values only
30-
* @param string|null $description Optional description/help text
31-
* @param int|float|null $default Optional default value
32-
* @param int|float|null $minimum Optional minimum value (inclusive)
33-
* @param int|float|null $maximum Optional maximum value (inclusive)
28+
* @param string $title Human-readable title for the field
29+
* @param bool $integerOnly Whether to restrict to integer values only
30+
* @param string|null $description Optional description/help text
31+
* @param int|float|null $default Optional default value
32+
* @param int|float|null $minimum Optional minimum value (inclusive)
33+
* @param int|float|null $maximum Optional maximum value (inclusive)
3434
*/
3535
public function __construct(
3636
public readonly string $title,

src/Schema/Elicitation/PrimitiveSchemaDefinition.php

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*
2121
* Dispatches to the correct schema definition class based on the "type" field.
2222
*
23-
* @author
23+
* @author Johannes Wachter <johannes@sulu.io>
2424
*/
2525
final class PrimitiveSchemaDefinition
2626
{
@@ -40,8 +40,6 @@ final class PrimitiveSchemaDefinition
4040
* minimum?: int|float,
4141
* maximum?: int|float,
4242
* } $data
43-
*
44-
* @return StringSchemaDefinition|NumberSchemaDefinition|BooleanSchemaDefinition|EnumSchemaDefinition
4543
*/
4644
public static function fromArray(array $data): StringSchemaDefinition|NumberSchemaDefinition|BooleanSchemaDefinition|EnumSchemaDefinition
4745
{
@@ -53,10 +51,7 @@ public static function fromArray(array $data): StringSchemaDefinition|NumberSche
5351
'string' => isset($data['enum']) ? EnumSchemaDefinition::fromArray($data) : StringSchemaDefinition::fromArray($data),
5452
'integer', 'number' => NumberSchemaDefinition::fromArray($data),
5553
'boolean' => BooleanSchemaDefinition::fromArray($data),
56-
default => throw new InvalidArgumentException(\sprintf(
57-
'Unsupported primitive type "%s". Supported types are: string, integer, number, boolean.',
58-
$data['type']
59-
)),
54+
default => throw new InvalidArgumentException(\sprintf('Unsupported primitive type "%s". Supported types are: string, integer, number, boolean.', $data['type'])),
6055
};
6156
}
6257
}

src/Schema/Elicitation/StringSchemaDefinition.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*
2121
* Supports optional format validation (date, date-time, email, uri) and length constraints.
2222
*
23-
* @author
23+
* @author Johannes Wachter <johannes@sulu.io>
2424
*/
2525
final class StringSchemaDefinition implements \JsonSerializable
2626
{
@@ -43,11 +43,7 @@ public function __construct(
4343
public readonly ?int $maxLength = null,
4444
) {
4545
if (null !== $format && !\in_array($format, self::VALID_FORMATS, true)) {
46-
throw new InvalidArgumentException(\sprintf(
47-
'Invalid format "%s". Valid formats are: %s.',
48-
$format,
49-
implode(', ', self::VALID_FORMATS)
50-
));
46+
throw new InvalidArgumentException(\sprintf('Invalid format "%s". Valid formats are: %s.', $format, implode(', ', self::VALID_FORMATS)));
5147
}
5248

5349
if (null !== $minLength && $minLength < 0) {

src/Schema/Enum/ElicitAction.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
declare(strict_types=1);
44

5+
/*
6+
* This file is part of the official PHP MCP SDK.
7+
*
8+
* A collaboration between Symfony and the PHP Foundation.
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
514
namespace Mcp\Schema\Enum;
615

716
/**

0 commit comments

Comments
 (0)