Add elicitation support for server-to-client user input requests (#228)

- Add ElicitAction enum (accept/decline/cancel)
- Add schema definitions for elicitation fields (string, number, boolean, enum)
- Add ElicitationSchema wrapper for requestedSchema
- Add ElicitRequest and ElicitResult for elicitation/create method
- Update ClientCapabilities to support elicitation capability
- Add elicit() method to ClientGateway
- Store client capabilities in session during initialization
- Add comprehensive unit tests
- Add elicitation example server with book_restaurant, confirm_action, and collect_feedback tools
This commit is contained in:
Johannes Wachter 2026-02-15 23:34:19 +01:00 • committed by GitHub
commit 02e86377e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 2582 additions and 5 deletions

View file

@ -0,0 +1,110 @@
<?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\Tests\Unit\Schema\Elicitation;
use Mcp\Exception\InvalidArgumentException;
use Mcp\Schema\Elicitation\BooleanSchemaDefinition;
use PHPUnit\Framework\TestCase;
final class BooleanSchemaDefinitionTest extends TestCase
{
public function testConstructorWithMinimalParams(): void
{
$schema = new BooleanSchemaDefinition('Confirm');
$this->assertSame('Confirm', $schema->title);
$this->assertNull($schema->description);
$this->assertNull($schema->default);
}
public function testConstructorWithAllParams(): void
{
$schema = new BooleanSchemaDefinition(
title: 'Confirmation',
description: 'Do you confirm this action?',
default: false,
);
$this->assertSame('Confirmation', $schema->title);
$this->assertSame('Do you confirm this action?', $schema->description);
$this->assertFalse($schema->default);
}
public function testConstructorWithTrueDefault(): void
{
$schema = new BooleanSchemaDefinition(
title: 'Subscribe',
default: true,
);
$this->assertTrue($schema->default);
}
public function testFromArrayWithMinimalParams(): void
{
$schema = BooleanSchemaDefinition::fromArray(['title' => 'Confirm']);
$this->assertSame('Confirm', $schema->title);
$this->assertNull($schema->description);
$this->assertNull($schema->default);
}
public function testFromArrayWithAllParams(): void
{
$schema = BooleanSchemaDefinition::fromArray([
'title' => 'Confirmation',
'description' => 'Do you confirm this action?',
'default' => true,
]);
$this->assertSame('Confirmation', $schema->title);
$this->assertSame('Do you confirm this action?', $schema->description);
$this->assertTrue($schema->default);
}
public function testFromArrayWithMissingTitle(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Missing or invalid "title"');
/* @phpstan-ignore argument.type */
BooleanSchemaDefinition::fromArray([]);
}
public function testJsonSerializeWithMinimalParams(): void
{
$schema = new BooleanSchemaDefinition('Confirm');
$this->assertSame([
'type' => 'boolean',
'title' => 'Confirm',
], $schema->jsonSerialize());
}
public function testJsonSerializeWithAllParams(): void
{
$schema = new BooleanSchemaDefinition(
title: 'Confirmation',
description: 'Do you confirm this action?',
default: false,
);
$this->assertSame([
'type' => 'boolean',
'title' => 'Confirmation',
'description' => 'Do you confirm this action?',
'default' => false,
], $schema->jsonSerialize());
}
}

View file

@ -0,0 +1,221 @@
<?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\Tests\Unit\Schema\Elicitation;
use Mcp\Exception\InvalidArgumentException;
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 PHPUnit\Framework\TestCase;
final class ElicitationSchemaTest extends TestCase
{
public function testConstructorWithMinimalParams(): void
{
$properties = [
'name' => new StringSchemaDefinition('Name'),
];
$schema = new ElicitationSchema($properties);
$this->assertCount(1, $schema->properties);
$this->assertSame([], $schema->required);
}
public function testConstructorWithRequiredFields(): void
{
$properties = [
'name' => new StringSchemaDefinition('Name'),
'email' => new StringSchemaDefinition('Email'),
];
$schema = new ElicitationSchema($properties, ['name']);
$this->assertCount(2, $schema->properties);
$this->assertSame(['name'], $schema->required);
}
public function testConstructorWithMultipleTypes(): void
{
$properties = [
'name' => new StringSchemaDefinition('Name'),
'age' => new NumberSchemaDefinition('Age', integerOnly: true),
'subscribe' => new BooleanSchemaDefinition('Subscribe'),
'rating' => new EnumSchemaDefinition('Rating', ['1', '2', '3', '4', '5']),
];
$schema = new ElicitationSchema($properties, ['name', 'age']);
$this->assertCount(4, $schema->properties);
$this->assertInstanceOf(StringSchemaDefinition::class, $schema->properties['name']);
$this->assertInstanceOf(NumberSchemaDefinition::class, $schema->properties['age']);
$this->assertInstanceOf(BooleanSchemaDefinition::class, $schema->properties['subscribe']);
$this->assertInstanceOf(EnumSchemaDefinition::class, $schema->properties['rating']);
}
public function testConstructorWithEmptyProperties(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('properties array must not be empty');
new ElicitationSchema([]);
}
public function testConstructorWithInvalidRequired(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Required property "unknown" is not defined in properties');
new ElicitationSchema(
['name' => new StringSchemaDefinition('Name')],
['unknown'],
);
}
public function testFromArrayWithMinimalParams(): void
{
$schema = ElicitationSchema::fromArray([
'properties' => [
'name' => ['type' => 'string', 'title' => 'Name'],
],
]);
$this->assertCount(1, $schema->properties);
$this->assertInstanceOf(StringSchemaDefinition::class, $schema->properties['name']);
}
public function testFromArrayWithExplicitObjectType(): void
{
$schema = ElicitationSchema::fromArray([
'type' => 'object',
'properties' => [
'name' => ['type' => 'string', 'title' => 'Name'],
],
]);
$this->assertCount(1, $schema->properties);
}
public function testFromArrayWithInvalidType(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('ElicitationSchema type must be "object"');
ElicitationSchema::fromArray([
'type' => 'array',
'properties' => [
'name' => ['type' => 'string', 'title' => 'Name'],
],
]);
}
public function testFromArrayWithMissingProperties(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Missing or invalid "properties"');
/* @phpstan-ignore argument.type */
ElicitationSchema::fromArray([]);
}
public function testFromArrayWithRequiredFields(): void
{
$schema = ElicitationSchema::fromArray([
'properties' => [
'name' => ['type' => 'string', 'title' => 'Name'],
'email' => ['type' => 'string', 'title' => 'Email', 'format' => 'email'],
],
'required' => ['name'],
]);
$this->assertSame(['name'], $schema->required);
}
public function testFromArrayWithMultipleTypes(): void
{
$schema = ElicitationSchema::fromArray([
'properties' => [
'name' => ['type' => 'string', 'title' => 'Name'],
'age' => ['type' => 'integer', 'title' => 'Age', 'minimum' => 0],
'confirm' => ['type' => 'boolean', 'title' => 'Confirm'],
'rating' => ['type' => 'string', 'title' => 'Rating', 'enum' => ['1', '2', '3']],
],
]);
$this->assertInstanceOf(StringSchemaDefinition::class, $schema->properties['name']);
$this->assertInstanceOf(NumberSchemaDefinition::class, $schema->properties['age']);
$this->assertInstanceOf(BooleanSchemaDefinition::class, $schema->properties['confirm']);
$this->assertInstanceOf(EnumSchemaDefinition::class, $schema->properties['rating']);
}
public function testJsonSerializeWithMinimalParams(): void
{
$schema = new ElicitationSchema([
'name' => new StringSchemaDefinition('Name'),
]);
$result = $schema->jsonSerialize();
$this->assertSame('object', $result['type']);
$this->assertArrayHasKey('name', $result['properties']);
$this->assertSame('string', $result['properties']['name']['type']);
$this->assertArrayNotHasKey('required', $result);
}
public function testJsonSerializeWithRequiredFields(): void
{
$schema = new ElicitationSchema(
[
'name' => new StringSchemaDefinition('Name'),
'email' => new StringSchemaDefinition('Email'),
],
['name'],
);
$result = $schema->jsonSerialize();
$this->assertSame(['name'], $result['required']);
}
public function testJsonSerializeWithFullSchema(): void
{
$schema = new ElicitationSchema(
[
'name' => new StringSchemaDefinition('Full Name', description: 'Your full name'),
'age' => new NumberSchemaDefinition('Age', integerOnly: true, minimum: 0, maximum: 150),
'subscribe' => new BooleanSchemaDefinition('Subscribe', default: false),
],
['name', 'age'],
);
$result = $schema->jsonSerialize();
$this->assertSame('object', $result['type']);
$this->assertCount(3, $result['properties']);
$this->assertSame(['name', 'age'], $result['required']);
$this->assertSame('string', $result['properties']['name']['type']);
$this->assertSame('Full Name', $result['properties']['name']['title']);
$this->assertSame('Your full name', $result['properties']['name']['description']);
$this->assertSame('integer', $result['properties']['age']['type']);
$this->assertSame(0, $result['properties']['age']['minimum']);
$this->assertSame(150, $result['properties']['age']['maximum']);
$this->assertSame('boolean', $result['properties']['subscribe']['type']);
$this->assertFalse($result['properties']['subscribe']['default']);
}
}

View file

@ -0,0 +1,167 @@
<?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\Tests\Unit\Schema\Elicitation;
use Mcp\Exception\InvalidArgumentException;
use Mcp\Schema\Elicitation\EnumSchemaDefinition;
use PHPUnit\Framework\TestCase;
final class EnumSchemaDefinitionTest extends TestCase
{
public function testConstructorWithMinimalParams(): void
{
$schema = new EnumSchemaDefinition('Rating', ['1', '2', '3', '4', '5']);
$this->assertSame('Rating', $schema->title);
$this->assertSame(['1', '2', '3', '4', '5'], $schema->enum);
$this->assertNull($schema->description);
$this->assertNull($schema->default);
$this->assertNull($schema->enumNames);
}
public function testConstructorWithAllParams(): void
{
$schema = new EnumSchemaDefinition(
title: 'Satisfaction',
enum: ['poor', 'fair', 'good', 'excellent'],
description: 'Rate your satisfaction',
default: 'good',
enumNames: ['Poor', 'Fair', 'Good', 'Excellent'],
);
$this->assertSame('Satisfaction', $schema->title);
$this->assertSame(['poor', 'fair', 'good', 'excellent'], $schema->enum);
$this->assertSame('Rate your satisfaction', $schema->description);
$this->assertSame('good', $schema->default);
$this->assertSame(['Poor', 'Fair', 'Good', 'Excellent'], $schema->enumNames);
}
public function testConstructorWithEmptyEnum(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('enum array must not be empty');
new EnumSchemaDefinition('Test', []);
}
public function testConstructorWithNonStringEnumValue(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('All enum values must be strings');
/* @phpstan-ignore argument.type */
new EnumSchemaDefinition('Test', ['a', 1, 'b']);
}
public function testConstructorWithEnumNamesMismatch(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('enumNames length must match enum length');
new EnumSchemaDefinition(
title: 'Test',
enum: ['a', 'b', 'c'],
enumNames: ['A', 'B'],
);
}
public function testConstructorWithInvalidDefault(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Default value "invalid" is not in the enum array');
new EnumSchemaDefinition(
title: 'Test',
enum: ['a', 'b', 'c'],
default: 'invalid',
);
}
public function testFromArrayWithMinimalParams(): void
{
$schema = EnumSchemaDefinition::fromArray([
'title' => 'Rating',
'enum' => ['1', '2', '3'],
]);
$this->assertSame('Rating', $schema->title);
$this->assertSame(['1', '2', '3'], $schema->enum);
}
public function testFromArrayWithAllParams(): void
{
$schema = EnumSchemaDefinition::fromArray([
'title' => 'Satisfaction',
'enum' => ['poor', 'fair', 'good'],
'description' => 'Rate your satisfaction',
'default' => 'fair',
'enumNames' => ['Poor', 'Fair', 'Good'],
]);
$this->assertSame('Satisfaction', $schema->title);
$this->assertSame(['poor', 'fair', 'good'], $schema->enum);
$this->assertSame('Rate your satisfaction', $schema->description);
$this->assertSame('fair', $schema->default);
$this->assertSame(['Poor', 'Fair', 'Good'], $schema->enumNames);
}
public function testFromArrayWithMissingTitle(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Missing or invalid "title"');
/* @phpstan-ignore argument.type */
EnumSchemaDefinition::fromArray(['enum' => ['a', 'b']]);
}
public function testFromArrayWithMissingEnum(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Missing or invalid "enum"');
/* @phpstan-ignore argument.type */
EnumSchemaDefinition::fromArray(['title' => 'Test']);
}
public function testJsonSerializeWithMinimalParams(): void
{
$schema = new EnumSchemaDefinition('Rating', ['1', '2', '3']);
$this->assertSame([
'type' => 'string',
'title' => 'Rating',
'enum' => ['1', '2', '3'],
], $schema->jsonSerialize());
}
public function testJsonSerializeWithAllParams(): void
{
$schema = new EnumSchemaDefinition(
title: 'Satisfaction',
enum: ['poor', 'fair', 'good'],
description: 'Rate your satisfaction',
default: 'fair',
enumNames: ['Poor', 'Fair', 'Good'],
);
$this->assertSame([
'type' => 'string',
'title' => 'Satisfaction',
'enum' => ['poor', 'fair', 'good'],
'description' => 'Rate your satisfaction',
'default' => 'fair',
'enumNames' => ['Poor', 'Fair', 'Good'],
], $schema->jsonSerialize());
}
}

View file

@ -0,0 +1,193 @@
<?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\Tests\Unit\Schema\Elicitation;
use Mcp\Exception\InvalidArgumentException;
use Mcp\Schema\Elicitation\NumberSchemaDefinition;
use PHPUnit\Framework\TestCase;
final class NumberSchemaDefinitionTest extends TestCase
{
public function testConstructorWithMinimalParams(): void
{
$schema = new NumberSchemaDefinition('Age');
$this->assertSame('Age', $schema->title);
$this->assertFalse($schema->integerOnly);
$this->assertNull($schema->description);
$this->assertNull($schema->default);
$this->assertNull($schema->minimum);
$this->assertNull($schema->maximum);
}
public function testConstructorWithAllParams(): void
{
$schema = new NumberSchemaDefinition(
title: 'Party Size',
integerOnly: true,
description: 'Number of guests',
default: 2,
minimum: 1,
maximum: 10,
);
$this->assertSame('Party Size', $schema->title);
$this->assertTrue($schema->integerOnly);
$this->assertSame('Number of guests', $schema->description);
$this->assertSame(2, $schema->default);
$this->assertSame(1, $schema->minimum);
$this->assertSame(10, $schema->maximum);
}
public function testConstructorWithFloatValues(): void
{
$schema = new NumberSchemaDefinition(
title: 'Temperature',
default: 36.5,
minimum: 35.0,
maximum: 42.0,
);
$this->assertSame(36.5, $schema->default);
$this->assertSame(35.0, $schema->minimum);
$this->assertSame(42.0, $schema->maximum);
}
public function testConstructorWithMinimumGreaterThanMaximum(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('minimum cannot be greater than maximum');
new NumberSchemaDefinition('Test', minimum: 10, maximum: 5);
}
public function testConstructorWithDefaultBelowMinimum(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('default value cannot be less than minimum');
new NumberSchemaDefinition('Test', default: 5, minimum: 10);
}
public function testConstructorWithDefaultAboveMaximum(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('default value cannot be greater than maximum');
new NumberSchemaDefinition('Test', default: 15, maximum: 10);
}
public function testConstructorWithNonIntegerDefaultWhenIntegerOnly(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('default value must be an integer when integerOnly is true');
new NumberSchemaDefinition('Test', integerOnly: true, default: 5.5);
}
public function testFromArrayWithIntegerType(): void
{
$schema = NumberSchemaDefinition::fromArray([
'type' => 'integer',
'title' => 'Count',
]);
$this->assertTrue($schema->integerOnly);
}
public function testFromArrayWithNumberType(): void
{
$schema = NumberSchemaDefinition::fromArray([
'type' => 'number',
'title' => 'Price',
]);
$this->assertFalse($schema->integerOnly);
}
public function testFromArrayWithAllParams(): void
{
$schema = NumberSchemaDefinition::fromArray([
'type' => 'integer',
'title' => 'Party Size',
'description' => 'Number of guests',
'default' => 2,
'minimum' => 1,
'maximum' => 10,
]);
$this->assertSame('Party Size', $schema->title);
$this->assertTrue($schema->integerOnly);
$this->assertSame('Number of guests', $schema->description);
$this->assertSame(2, $schema->default);
$this->assertSame(1, $schema->minimum);
$this->assertSame(10, $schema->maximum);
}
public function testFromArrayWithMissingTitle(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Missing or invalid "title"');
/* @phpstan-ignore argument.type */
NumberSchemaDefinition::fromArray(['type' => 'integer']);
}
public function testJsonSerializeAsInteger(): void
{
$schema = new NumberSchemaDefinition(
title: 'Count',
integerOnly: true,
);
$this->assertSame([
'type' => 'integer',
'title' => 'Count',
], $schema->jsonSerialize());
}
public function testJsonSerializeAsNumber(): void
{
$schema = new NumberSchemaDefinition(
title: 'Price',
integerOnly: false,
);
$this->assertSame([
'type' => 'number',
'title' => 'Price',
], $schema->jsonSerialize());
}
public function testJsonSerializeWithAllParams(): void
{
$schema = new NumberSchemaDefinition(
title: 'Party Size',
integerOnly: true,
description: 'Number of guests',
default: 2,
minimum: 1,
maximum: 10,
);
$this->assertSame([
'type' => 'integer',
'title' => 'Party Size',
'description' => 'Number of guests',
'default' => 2,
'minimum' => 1,
'maximum' => 10,
], $schema->jsonSerialize());
}
}

View file

@ -0,0 +1,115 @@
<?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\Tests\Unit\Schema\Elicitation;
use Mcp\Exception\InvalidArgumentException;
use Mcp\Schema\Elicitation\BooleanSchemaDefinition;
use Mcp\Schema\Elicitation\EnumSchemaDefinition;
use Mcp\Schema\Elicitation\NumberSchemaDefinition;
use Mcp\Schema\Elicitation\PrimitiveSchemaDefinition;
use Mcp\Schema\Elicitation\StringSchemaDefinition;
use PHPUnit\Framework\TestCase;
final class PrimitiveSchemaDefinitionTest extends TestCase
{
public function testFromArrayCreatesStringSchema(): void
{
$schema = PrimitiveSchemaDefinition::fromArray([
'type' => 'string',
'title' => 'Name',
]);
$this->assertInstanceOf(StringSchemaDefinition::class, $schema);
$this->assertSame('Name', $schema->title);
}
public function testFromArrayCreatesEnumSchemaForStringWithEnum(): void
{
$schema = PrimitiveSchemaDefinition::fromArray([
'type' => 'string',
'title' => 'Rating',
'enum' => ['1', '2', '3'],
]);
$this->assertInstanceOf(EnumSchemaDefinition::class, $schema);
$this->assertSame('Rating', $schema->title);
$this->assertSame(['1', '2', '3'], $schema->enum);
}
public function testFromArrayCreatesIntegerSchema(): void
{
$schema = PrimitiveSchemaDefinition::fromArray([
'type' => 'integer',
'title' => 'Age',
]);
$this->assertInstanceOf(NumberSchemaDefinition::class, $schema);
$this->assertSame('Age', $schema->title);
$this->assertTrue($schema->integerOnly);
}
public function testFromArrayCreatesNumberSchema(): void
{
$schema = PrimitiveSchemaDefinition::fromArray([
'type' => 'number',
'title' => 'Price',
]);
$this->assertInstanceOf(NumberSchemaDefinition::class, $schema);
$this->assertSame('Price', $schema->title);
$this->assertFalse($schema->integerOnly);
}
public function testFromArrayCreatesBooleanSchema(): void
{
$schema = PrimitiveSchemaDefinition::fromArray([
'type' => 'boolean',
'title' => 'Confirm',
]);
$this->assertInstanceOf(BooleanSchemaDefinition::class, $schema);
$this->assertSame('Confirm', $schema->title);
}
public function testFromArrayWithMissingType(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Missing or invalid "type"');
/* @phpstan-ignore argument.type */
PrimitiveSchemaDefinition::fromArray(['title' => 'Test']);
}
public function testFromArrayWithUnsupportedType(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Unsupported primitive type "object"');
PrimitiveSchemaDefinition::fromArray([
'type' => 'object',
'title' => 'Test',
]);
}
public function testFromArrayWithArrayType(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Unsupported primitive type "array"');
PrimitiveSchemaDefinition::fromArray([
'type' => 'array',
'title' => 'Test',
]);
}
}

View file

@ -0,0 +1,159 @@
<?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\Tests\Unit\Schema\Elicitation;
use Mcp\Exception\InvalidArgumentException;
use Mcp\Schema\Elicitation\StringSchemaDefinition;
use PHPUnit\Framework\TestCase;
final class StringSchemaDefinitionTest extends TestCase
{
public function testConstructorWithMinimalParams(): void
{
$schema = new StringSchemaDefinition('Name');
$this->assertSame('Name', $schema->title);
$this->assertNull($schema->description);
$this->assertNull($schema->default);
$this->assertNull($schema->format);
$this->assertNull($schema->minLength);
$this->assertNull($schema->maxLength);
}
public function testConstructorWithAllParams(): void
{
$schema = new StringSchemaDefinition(
title: 'Email Address',
description: 'Your primary email',
default: 'user@example.com',
format: 'email',
minLength: 5,
maxLength: 100,
);
$this->assertSame('Email Address', $schema->title);
$this->assertSame('Your primary email', $schema->description);
$this->assertSame('user@example.com', $schema->default);
$this->assertSame('email', $schema->format);
$this->assertSame(5, $schema->minLength);
$this->assertSame(100, $schema->maxLength);
}
public function testConstructorWithValidFormats(): void
{
foreach (['date', 'date-time', 'email', 'uri'] as $format) {
$schema = new StringSchemaDefinition('Test', format: $format);
$this->assertSame($format, $schema->format);
}
}
public function testConstructorWithInvalidFormat(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid format "invalid"');
new StringSchemaDefinition('Test', format: 'invalid');
}
public function testConstructorWithNegativeMinLength(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('minLength must be non-negative');
new StringSchemaDefinition('Test', minLength: -1);
}
public function testConstructorWithNegativeMaxLength(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('maxLength must be non-negative');
new StringSchemaDefinition('Test', maxLength: -1);
}
public function testConstructorWithMinLengthGreaterThanMaxLength(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('minLength cannot be greater than maxLength');
new StringSchemaDefinition('Test', minLength: 10, maxLength: 5);
}
public function testFromArrayWithMinimalParams(): void
{
$schema = StringSchemaDefinition::fromArray(['title' => 'Name']);
$this->assertSame('Name', $schema->title);
}
public function testFromArrayWithAllParams(): void
{
$schema = StringSchemaDefinition::fromArray([
'title' => 'Email Address',
'description' => 'Your primary email',
'default' => 'user@example.com',
'format' => 'email',
'minLength' => 5,
'maxLength' => 100,
]);
$this->assertSame('Email Address', $schema->title);
$this->assertSame('Your primary email', $schema->description);
$this->assertSame('user@example.com', $schema->default);
$this->assertSame('email', $schema->format);
$this->assertSame(5, $schema->minLength);
$this->assertSame(100, $schema->maxLength);
}
public function testFromArrayWithMissingTitle(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Missing or invalid "title"');
/* @phpstan-ignore argument.type */
StringSchemaDefinition::fromArray([]);
}
public function testJsonSerializeWithMinimalParams(): void
{
$schema = new StringSchemaDefinition('Name');
$this->assertSame([
'type' => 'string',
'title' => 'Name',
], $schema->jsonSerialize());
}
public function testJsonSerializeWithAllParams(): void
{
$schema = new StringSchemaDefinition(
title: 'Email Address',
description: 'Your primary email',
default: 'user@example.com',
format: 'email',
minLength: 5,
maxLength: 100,
);
$this->assertSame([
'type' => 'string',
'title' => 'Email Address',
'description' => 'Your primary email',
'default' => 'user@example.com',
'format' => 'email',
'minLength' => 5,
'maxLength' => 100,
], $schema->jsonSerialize());
}
}

View file

@ -0,0 +1,52 @@
<?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\Tests\Unit\Schema\Enum;
use Mcp\Schema\Enum\ElicitAction;
use PHPUnit\Framework\TestCase;
final class ElicitActionTest extends TestCase
{
public function testEnumValues(): void
{
$this->assertSame('accept', ElicitAction::Accept->value);
$this->assertSame('decline', ElicitAction::Decline->value);
$this->assertSame('cancel', ElicitAction::Cancel->value);
}
public function testFromValidValues(): void
{
$this->assertSame(ElicitAction::Accept, ElicitAction::from('accept'));
$this->assertSame(ElicitAction::Decline, ElicitAction::from('decline'));
$this->assertSame(ElicitAction::Cancel, ElicitAction::from('cancel'));
}
public function testFromInvalidValue(): void
{
$this->expectException(\ValueError::class);
ElicitAction::from('invalid');
}
public function testTryFromValidValues(): void
{
$this->assertSame(ElicitAction::Accept, ElicitAction::tryFrom('accept'));
$this->assertSame(ElicitAction::Decline, ElicitAction::tryFrom('decline'));
$this->assertSame(ElicitAction::Cancel, ElicitAction::tryFrom('cancel'));
}
public function testTryFromInvalidValue(): void
{
$this->assertNull(ElicitAction::tryFrom('invalid'));
}
}

View file

@ -0,0 +1,74 @@
<?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\Tests\Unit\Schema\Request;
use Mcp\Schema\Elicitation\ElicitationSchema;
use Mcp\Schema\Elicitation\NumberSchemaDefinition;
use Mcp\Schema\Elicitation\StringSchemaDefinition;
use Mcp\Schema\Request\ElicitRequest;
use PHPUnit\Framework\TestCase;
final class ElicitRequestTest extends TestCase
{
public function testConstructor(): void
{
$schema = new ElicitationSchema([
'name' => new StringSchemaDefinition('Name'),
]);
$request = new ElicitRequest('Please provide your name', $schema);
$this->assertSame('Please provide your name', $request->message);
$this->assertSame($schema, $request->requestedSchema);
}
public function testGetMethod(): void
{
$this->assertSame('elicitation/create', ElicitRequest::getMethod());
}
public function testJsonSerialization(): void
{
$schema = new ElicitationSchema(
[
'name' => new StringSchemaDefinition('Name'),
'age' => new NumberSchemaDefinition('Age', integerOnly: true, minimum: 0),
],
['name'],
);
$request = new ElicitRequest('Please provide your details', $schema);
$request = $request->withId(1);
$json = json_encode($request);
$this->assertIsString($json);
$decoded = json_decode($json, true);
$this->assertIsArray($decoded);
$this->assertSame('2.0', $decoded['jsonrpc']);
$this->assertSame('elicitation/create', $decoded['method']);
$this->assertArrayHasKey('params', $decoded);
$params = $decoded['params'];
$this->assertSame('Please provide your details', $params['message']);
$this->assertArrayHasKey('requestedSchema', $params);
$requestedSchema = $params['requestedSchema'];
$this->assertSame('object', $requestedSchema['type']);
$this->assertArrayHasKey('name', $requestedSchema['properties']);
$this->assertArrayHasKey('age', $requestedSchema['properties']);
$this->assertSame(['name'], $requestedSchema['required']);
}
}

View file

@ -0,0 +1,163 @@
<?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\Tests\Unit\Schema\Result;
use Mcp\Exception\InvalidArgumentException;
use Mcp\Schema\Enum\ElicitAction;
use Mcp\Schema\Result\ElicitResult;
use PHPUnit\Framework\TestCase;
final class ElicitResultTest extends TestCase
{
public function testConstructorWithAcceptAndContent(): void
{
$content = ['name' => 'John', 'age' => 30];
$result = new ElicitResult(ElicitAction::Accept, $content);
$this->assertSame(ElicitAction::Accept, $result->action);
$this->assertSame($content, $result->content);
}
public function testConstructorWithDecline(): void
{
$result = new ElicitResult(ElicitAction::Decline);
$this->assertSame(ElicitAction::Decline, $result->action);
$this->assertNull($result->content);
}
public function testConstructorWithCancel(): void
{
$result = new ElicitResult(ElicitAction::Cancel);
$this->assertSame(ElicitAction::Cancel, $result->action);
$this->assertNull($result->content);
}
public function testFromArrayWithAccept(): void
{
$result = ElicitResult::fromArray([
'action' => 'accept',
'content' => ['name' => 'John', 'email' => 'john@example.com'],
]);
$this->assertSame(ElicitAction::Accept, $result->action);
$this->assertSame(['name' => 'John', 'email' => 'john@example.com'], $result->content);
}
public function testFromArrayWithDecline(): void
{
$result = ElicitResult::fromArray([
'action' => 'decline',
]);
$this->assertSame(ElicitAction::Decline, $result->action);
$this->assertNull($result->content);
}
public function testFromArrayWithCancel(): void
{
$result = ElicitResult::fromArray([
'action' => 'cancel',
]);
$this->assertSame(ElicitAction::Cancel, $result->action);
$this->assertNull($result->content);
}
public function testFromArrayWithMissingAction(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Missing or invalid "action"');
/* @phpstan-ignore argument.type */
ElicitResult::fromArray([]);
}
public function testFromArrayWithInvalidAction(): void
{
$this->expectException(\ValueError::class);
ElicitResult::fromArray(['action' => 'invalid']);
}
public function testFromArrayWithAcceptActionRequiresContent(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Content must be provided when action is "accept"');
ElicitResult::fromArray(['action' => 'accept']);
}
public function testIsAccepted(): void
{
$acceptResult = new ElicitResult(ElicitAction::Accept, ['name' => 'John']);
$declineResult = new ElicitResult(ElicitAction::Decline);
$cancelResult = new ElicitResult(ElicitAction::Cancel);
$this->assertTrue($acceptResult->isAccepted());
$this->assertFalse($declineResult->isAccepted());
$this->assertFalse($cancelResult->isAccepted());
}
public function testIsDeclined(): void
{
$acceptResult = new ElicitResult(ElicitAction::Accept, ['name' => 'John']);
$declineResult = new ElicitResult(ElicitAction::Decline);
$cancelResult = new ElicitResult(ElicitAction::Cancel);
$this->assertFalse($acceptResult->isDeclined());
$this->assertTrue($declineResult->isDeclined());
$this->assertFalse($cancelResult->isDeclined());
}
public function testIsCancelled(): void
{
$acceptResult = new ElicitResult(ElicitAction::Accept, ['name' => 'John']);
$declineResult = new ElicitResult(ElicitAction::Decline);
$cancelResult = new ElicitResult(ElicitAction::Cancel);
$this->assertFalse($acceptResult->isCancelled());
$this->assertFalse($declineResult->isCancelled());
$this->assertTrue($cancelResult->isCancelled());
}
public function testJsonSerializeWithAcceptAndContent(): void
{
$result = new ElicitResult(ElicitAction::Accept, ['name' => 'John', 'age' => 30]);
$this->assertSame([
'action' => 'accept',
'content' => ['name' => 'John', 'age' => 30],
], $result->jsonSerialize());
}
public function testJsonSerializeWithDecline(): void
{
$result = new ElicitResult(ElicitAction::Decline);
$this->assertSame([
'action' => 'decline',
], $result->jsonSerialize());
}
public function testJsonSerializeWithCancel(): void
{
$result = new ElicitResult(ElicitAction::Cancel);
$this->assertSame([
'action' => 'cancel',
], $result->jsonSerialize());
}
}

View file

@ -39,12 +39,15 @@ class InitializeHandlerTest extends TestCase
$handler = new InitializeHandler($configuration);
$session = $this->createMock(SessionInterface::class);
$session->expects($this->once())
$session->expects($this->exactly(2))
->method('set')
->with('client_info', [
'name' => 'client-app',
'version' => '1.0.0',
]);
->willReturnCallback(function (string $key, array $value): void {
match ($key) {
'client_info' => $this->assertSame(['name' => 'client-app', 'version' => '1.0.0'], $value),
'client_capabilities' => $this->assertSame([], $value),
default => $this->fail("Unexpected session key: {$key}"),
};
});
$request = InitializeRequest::fromArray([
'jsonrpc' => MessageInterface::JSONRPC_VERSION,