Split testsuite for unit and inspector based tests (#82)

This commit is contained in:
Christopher Hertel 2025-09-25 23:27:41 +02:00 • committed by GitHub
commit 3ec4cf612a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 188 additions and 85 deletions

View file

@ -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\Tests\Inspector;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\Process;
abstract class InspectorSnapshotTestCase extends TestCase
{
#[DataProvider('provideMethods')]
public function testResourcesListOutputMatchesSnapshot(string $method): void
{
$process = Process::fromShellCommandline(
\sprintf('npx @modelcontextprotocol/inspector --cli php %s --method %s', $this->getServerScript(), $method)
)->mustRun();
$output = $process->getOutput();
$snapshotFile = $this->getSnapshotFilePath($method);
if (!file_exists($snapshotFile)) {
file_put_contents($snapshotFile, $output.\PHP_EOL);
$this->markTestIncomplete("Snapshot created at $snapshotFile, please re-run tests.");
}
$expected = file_get_contents($snapshotFile);
$this->assertJsonStringEqualsJsonString($expected, $output);
}
/**
* List of methods to test.
*
* @return array<string, array{method: string}>
*/
abstract public static function provideMethods(): array;
abstract protected function getServerScript(): string;
/**
* @return array<string, array{method: string}>
*/
protected static function provideListMethods(): array
{
return [
'Prompt Listing' => ['method' => 'prompts/list'],
'Resource Listing' => ['method' => 'resources/list'],
// 'Resource Template Listing' => ['method' => 'resources/templates/list'],
'Tool Listing' => ['method' => 'tools/list'],
];
}
private function getSnapshotFilePath(string $method): string
{
$className = substr(static::class, strrpos(static::class, '\\') + 1);
return __DIR__.'/snapshots/'.$className.'-'.str_replace('/', '_', $method).'.json';
}
}

View file

@ -0,0 +1,29 @@
<?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 Inspector;
use Mcp\Tests\Inspector\InspectorSnapshotTestCase;
final class ManualStdioExampleTest extends InspectorSnapshotTestCase
{
public static function provideMethods(): array
{
return [
...parent::provideListMethods(),
];
}
protected function getServerScript(): string
{
return \dirname(__DIR__, 2).'/examples/03-manual-registration-stdio/server.php';
}
}

View file

@ -0,0 +1,27 @@
<?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\Inspector;
final class StdioCalculatorExampleTest extends InspectorSnapshotTestCase
{
public static function provideMethods(): array
{
return [
...parent::provideListMethods(),
];
}
protected function getServerScript(): string
{
return \dirname(__DIR__, 2).'/examples/01-discovery-stdio-calculator/server.php';
}
}

View file

@ -0,0 +1,15 @@
{
"prompts": [
{
"name": "personalized_greeting",
"description": "A manually registered prompt template.",
"arguments": [
{
"name": "userName",
"description": "the name of the user",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,10 @@
{
"resources": [
{
"name": "application_version",
"uri": "app://version",
"description": "A manually registered resource providing app version.",
"mimeType": "text/plain"
}
]
}

View file

@ -0,0 +1,20 @@
{
"tools": [
{
"name": "echo_text",
"description": "A manually registered tool to echo input.",
"inputSchema": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "the text to echo"
}
},
"required": [
"text"
]
}
}
]
}

View file

@ -0,0 +1,4 @@
{
"prompts": []
}

View file

@ -0,0 +1,11 @@
{
"resources": [
{
"name": "calculator_config",
"uri": "config://calculator/settings",
"description": "Current settings for the calculator tool (precision, allow_negative).",
"mimeType": "application/json"
}
]
}

View file

@ -0,0 +1,51 @@
{
"tools": [
{
"name": "calculate",
"description": "Performs a calculation based on the operation.",
"inputSchema": {
"type": "object",
"properties": {
"a": {
"type": "number",
"description": "the first operand"
},
"b": {
"type": "number",
"description": "the second operand"
},
"operation": {
"type": "string",
"description": "the operation ('add', 'subtract', 'multiply', 'divide')"
}
},
"required": [
"a",
"b",
"operation"
]
}
},
{
"name": "update_setting",
"description": "Updates a specific configuration setting.",
"inputSchema": {
"type": "object",
"properties": {
"setting": {
"type": "string",
"description": "the setting key ('precision' or 'allow_negative')"
},
"value": {
"description": "the new value (int for precision, bool for allow_negative)"
}
},
"required": [
"setting",
"value"
]
}
}
]
}

View file

@ -0,0 +1,30 @@
<?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\Capability\Attribute;
use Mcp\Capability\Prompt\Completion\ProviderInterface;
class CompletionProviderFixture implements ProviderInterface
{
/**
* @var string[]
*/
public static array $completions = ['alpha', 'beta', 'gamma'];
public static string $lastCurrentValue = '';
public function getCompletions(string $currentValue): array
{
self::$lastCurrentValue = $currentValue;
return array_filter(self::$completions, fn ($item) => str_starts_with($item, $currentValue));
}
}

View file

@ -0,0 +1,85 @@
<?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\Capability\Attribute;
use Mcp\Capability\Attribute\CompletionProvider;
use Mcp\Tests\Unit\Fixtures\Enum\StatusEnum;
use PHPUnit\Framework\TestCase;
class CompletionProviderTest extends TestCase
{
public function testCanBeConstructedWithProviderClass(): void
{
$attribute = new CompletionProvider(provider: CompletionProviderFixture::class);
$this->assertSame(CompletionProviderFixture::class, $attribute->provider);
$this->assertNull($attribute->values);
$this->assertNull($attribute->enum);
}
public function testCanBeConstructedWithProviderInstance(): void
{
$instance = new CompletionProviderFixture();
$attribute = new CompletionProvider(provider: $instance);
$this->assertSame($instance, $attribute->provider);
$this->assertNull($attribute->values);
$this->assertNull($attribute->enum);
}
public function testCanBeConstructedWithValuesArray(): void
{
$values = ['draft', 'published', 'archived'];
$attribute = new CompletionProvider(values: $values);
$this->assertNull($attribute->provider);
$this->assertSame($values, $attribute->values);
$this->assertNull($attribute->enum);
}
public function testCanBeConstructedWithEnumClass(): void
{
$attribute = new CompletionProvider(enum: StatusEnum::class);
$this->assertNull($attribute->provider);
$this->assertNull($attribute->values);
$this->assertSame(StatusEnum::class, $attribute->enum);
}
public function testThrowsExceptionWhenNoParametersProvided(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only one of provider, values, or enum can be set');
new CompletionProvider();
}
public function testThrowsExceptionWhenMultipleParametersProvided(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only one of provider, values, or enum can be set');
new CompletionProvider(
provider: CompletionProviderFixture::class,
values: ['test']
);
}
public function testThrowsExceptionWhenAllParametersProvided(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only one of provider, values, or enum can be set');
new CompletionProvider(
provider: CompletionProviderFixture::class,
values: ['test'],
enum: StatusEnum::class
);
}
}

View file

@ -0,0 +1,52 @@
<?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\Capability\Attribute;
use Mcp\Capability\Attribute\McpPrompt;
use PHPUnit\Framework\TestCase;
class McpPromptTest extends TestCase
{
public function testInstantiatesWithNameAndDescription(): void
{
// Arrange
$name = 'test-prompt-name';
$description = 'This is a test prompt description.';
// Act
$attribute = new McpPrompt(name: $name, description: $description);
// Assert
$this->assertSame($name, $attribute->name);
$this->assertSame($description, $attribute->description);
}
public function testInstantiatesWithNullValuesForNameAndDescription(): void
{
// Arrange & Act
$attribute = new McpPrompt(name: null, description: null);
// Assert
$this->assertNull($attribute->name);
$this->assertNull($attribute->description);
}
public function testInstantiatesWithMissingOptionalArguments(): void
{
// Arrange & Act
$attribute = new McpPrompt(); // Use default constructor values
// Assert
$this->assertNull($attribute->name);
$this->assertNull($attribute->description);
}
}

View file

@ -0,0 +1,71 @@
<?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\Capability\Attribute;
use Mcp\Capability\Attribute\McpResourceTemplate;
use PHPUnit\Framework\TestCase;
class McpResourceTemplateTest extends TestCase
{
public function testInstantiatesWithCorrectProperties(): void
{
// Arrange
$uriTemplate = 'file:///{path}/data';
$name = 'test-template-name';
$description = 'This is a test template description.';
$mimeType = 'application/json';
// Act
$attribute = new McpResourceTemplate(
uriTemplate: $uriTemplate,
name: $name,
description: $description,
mimeType: $mimeType,
);
// Assert
$this->assertSame($uriTemplate, $attribute->uriTemplate);
$this->assertSame($name, $attribute->name);
$this->assertSame($description, $attribute->description);
$this->assertSame($mimeType, $attribute->mimeType);
}
public function testInstantiatesWithNullValuesForNameAndDescription(): void
{
// Arrange & Act
$attribute = new McpResourceTemplate(
uriTemplate: 'test://{id}', // uriTemplate is required
name: null,
description: null,
mimeType: null,
);
// Assert
$this->assertSame('test://{id}', $attribute->uriTemplate);
$this->assertNull($attribute->name);
$this->assertNull($attribute->description);
$this->assertNull($attribute->mimeType);
}
public function testInstantiatesWithMissingOptionalArguments(): void
{
// Arrange & Act
$uriTemplate = 'tmpl://{key}';
$attribute = new McpResourceTemplate(uriTemplate: $uriTemplate);
// Assert
$this->assertSame($uriTemplate, $attribute->uriTemplate);
$this->assertNull($attribute->name);
$this->assertNull($attribute->description);
$this->assertNull($attribute->mimeType);
}
}

View file

@ -0,0 +1,77 @@
<?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\Capability\Attribute;
use Mcp\Capability\Attribute\McpResource;
use PHPUnit\Framework\TestCase;
class McpResourceTest extends TestCase
{
public function testInstantiatesWithCorrectProperties(): void
{
// Arrange
$uri = 'file:///test/resource';
$name = 'test-resource-name';
$description = 'This is a test resource description.';
$mimeType = 'text/plain';
$size = 1024;
// Act
$attribute = new McpResource(
uri: $uri,
name: $name,
description: $description,
mimeType: $mimeType,
size: $size,
);
// Assert
$this->assertSame($uri, $attribute->uri);
$this->assertSame($name, $attribute->name);
$this->assertSame($description, $attribute->description);
$this->assertSame($mimeType, $attribute->mimeType);
$this->assertSame($size, $attribute->size);
}
public function testInstantiatesWithNullValuesForNameAndDescription(): void
{
// Arrange & Act
$attribute = new McpResource(
uri: 'file:///test', // URI is required
name: null,
description: null,
mimeType: null,
size: null,
);
// Assert
$this->assertSame('file:///test', $attribute->uri);
$this->assertNull($attribute->name);
$this->assertNull($attribute->description);
$this->assertNull($attribute->mimeType);
$this->assertNull($attribute->size);
}
public function testInstantiatesWithMissingOptionalArguments(): void
{
// Arrange & Act
$uri = 'file:///only-uri';
$attribute = new McpResource(uri: $uri);
// Assert
$this->assertSame($uri, $attribute->uri);
$this->assertNull($attribute->name);
$this->assertNull($attribute->description);
$this->assertNull($attribute->mimeType);
$this->assertNull($attribute->size);
}
}

View file

@ -0,0 +1,52 @@
<?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\Capability\Attribute;
use Mcp\Capability\Attribute\McpTool;
use PHPUnit\Framework\TestCase;
class McpToolTest extends TestCase
{
public function testInstantiatesWithCorrectProperties(): void
{
// Arrange
$name = 'test-tool-name';
$description = 'This is a test description.';
// Act
$attribute = new McpTool(name: $name, description: $description);
// Assert
$this->assertSame($name, $attribute->name);
$this->assertSame($description, $attribute->description);
}
public function testInstantiatesWithNullValuesForNameAndDescription(): void
{
// Arrange & Act
$attribute = new McpTool(name: null, description: null);
// Assert
$this->assertNull($attribute->name);
$this->assertNull($attribute->description);
}
public function testInstantiatesWithMissingOptionalArguments(): void
{
// Arrange & Act
$attribute = new McpTool(); // Use default constructor values
// Assert
$this->assertNull($attribute->name);
$this->assertNull($attribute->description);
}
}

View file

@ -0,0 +1,98 @@
<?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\Capability\Discovery;
use Mcp\Capability\Discovery\CachedDiscoverer;
use Mcp\Capability\Discovery\Discoverer;
use Mcp\Capability\Discovery\DiscoveryState;
use Mcp\Capability\Registry;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Psr\SimpleCache\CacheInterface;
class CachedDiscovererTest extends TestCase
{
public function testCachedDiscovererUsesCacheOnSecondCall(): void
{
$registry = new Registry(null, new NullLogger());
$discoverer = new Discoverer($registry, new NullLogger());
$cache = $this->createMock(CacheInterface::class);
$cache->expects($this->once())
->method('get')
->willReturn(null);
$cache->expects($this->once())
->method('set')
->willReturn(true);
$cachedDiscoverer = new CachedDiscoverer(
$discoverer,
$cache,
new NullLogger()
);
$result = $cachedDiscoverer->discover('/test/path', ['.'], []);
$this->assertInstanceOf(DiscoveryState::class, $result);
}
public function testCachedDiscovererReturnsCachedResults(): void
{
$registry = new Registry(null, new NullLogger());
$discoverer = new Discoverer($registry, new NullLogger());
$cache = $this->createMock(CacheInterface::class);
$cachedState = new DiscoveryState();
$cache->expects($this->once())
->method('get')
->willReturn($cachedState);
$cache->expects($this->never())
->method('set');
$cachedDiscoverer = new CachedDiscoverer(
$discoverer,
$cache,
new NullLogger()
);
$result = $cachedDiscoverer->discover('/test/path', ['.'], []);
$this->assertInstanceOf(DiscoveryState::class, $result);
}
public function testCacheKeyGeneration(): void
{
$registry = new Registry(null, new NullLogger());
$discoverer = new Discoverer($registry, new NullLogger());
$cache = $this->createMock(CacheInterface::class);
$cache->expects($this->exactly(2))
->method('get')
->willReturn(null);
$cache->expects($this->exactly(2))
->method('set')
->willReturn(true);
$cachedDiscoverer = new CachedDiscoverer(
$discoverer,
$cache,
new NullLogger()
);
$result1 = $cachedDiscoverer->discover('/path1', ['.'], []);
$result2 = $cachedDiscoverer->discover('/path2', ['.'], []);
$this->assertInstanceOf(DiscoveryState::class, $result1);
$this->assertInstanceOf(DiscoveryState::class, $result2);
}
}

View file

@ -0,0 +1,183 @@
<?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\Capability\Discovery;
use Mcp\Capability\Discovery\Discoverer;
use Mcp\Capability\Prompt\Completion\EnumCompletionProvider;
use Mcp\Capability\Prompt\Completion\ListCompletionProvider;
use Mcp\Capability\Registry;
use Mcp\Capability\Registry\PromptReference;
use Mcp\Capability\Registry\ResourceReference;
use Mcp\Capability\Registry\ResourceTemplateReference;
use Mcp\Capability\Registry\ToolReference;
use Mcp\Tests\Unit\Capability\Attribute\CompletionProviderFixture;
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\DiscoverableToolHandler;
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\InvocablePromptFixture;
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\InvocableResourceFixture;
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\InvocableResourceTemplateFixture;
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\InvocableToolFixture;
use PHPUnit\Framework\TestCase;
class DiscoveryTest extends TestCase
{
private Registry $registry;
private Discoverer $discoverer;
protected function setUp(): void
{
$this->registry = new Registry();
$this->discoverer = new Discoverer($this->registry);
}
public function testDiscoversAllElementTypesCorrectlyFromFixtureFiles()
{
$this->discoverer->discover(__DIR__, ['Fixtures']);
$tools = $this->registry->getTools();
$this->assertCount(4, $tools);
$greetUserTool = $this->registry->getTool('greet_user');
$this->assertInstanceOf(ToolReference::class, $greetUserTool);
$this->assertFalse($greetUserTool->isManual);
$this->assertEquals('greet_user', $greetUserTool->tool->name);
$this->assertEquals('Greets a user by name.', $greetUserTool->tool->description);
$this->assertEquals([DiscoverableToolHandler::class, 'greet'], $greetUserTool->handler);
$this->assertArrayHasKey('name', $greetUserTool->tool->inputSchema['properties'] ?? []);
$repeatActionTool = $this->registry->getTool('repeatAction');
$this->assertInstanceOf(ToolReference::class, $repeatActionTool);
$this->assertEquals('A tool with more complex parameters and inferred name/description.', $repeatActionTool->tool->description);
$this->assertTrue($repeatActionTool->tool->annotations->readOnlyHint);
$this->assertEquals(['count', 'loudly', 'mode'], array_keys($repeatActionTool->tool->inputSchema['properties'] ?? []));
$invokableCalcTool = $this->registry->getTool('InvokableCalculator');
$this->assertInstanceOf(ToolReference::class, $invokableCalcTool);
$this->assertFalse($invokableCalcTool->isManual);
$this->assertEquals([InvocableToolFixture::class, '__invoke'], $invokableCalcTool->handler);
$this->assertNull($this->registry->getTool('private_tool_should_be_ignored'));
$this->assertNull($this->registry->getTool('protected_tool_should_be_ignored'));
$this->assertNull($this->registry->getTool('static_tool_should_be_ignored'));
$resources = $this->registry->getResources();
$this->assertCount(3, $resources);
$appVersionRes = $this->registry->getResource('app://info/version');
$this->assertInstanceOf(ResourceReference::class, $appVersionRes);
$this->assertFalse($appVersionRes->isManual);
$this->assertEquals('app_version', $appVersionRes->schema->name);
$this->assertEquals('text/plain', $appVersionRes->schema->mimeType);
$invokableStatusRes = $this->registry->getResource('invokable://config/status');
$this->assertInstanceOf(ResourceReference::class, $invokableStatusRes);
$this->assertFalse($invokableStatusRes->isManual);
$this->assertEquals([InvocableResourceFixture::class, '__invoke'], $invokableStatusRes->handler);
$prompts = $this->registry->getPrompts();
$this->assertCount(4, $prompts);
$storyPrompt = $this->registry->getPrompt('creative_story_prompt');
$this->assertInstanceOf(PromptReference::class, $storyPrompt);
$this->assertFalse($storyPrompt->isManual);
$this->assertCount(2, $storyPrompt->prompt->arguments);
$this->assertEquals(CompletionProviderFixture::class, $storyPrompt->completionProviders['genre']);
$simplePrompt = $this->registry->getPrompt('simpleQuestionPrompt');
$this->assertInstanceOf(PromptReference::class, $simplePrompt);
$this->assertFalse($simplePrompt->isManual);
$invokableGreeter = $this->registry->getPrompt('InvokableGreeterPrompt');
$this->assertInstanceOf(PromptReference::class, $invokableGreeter);
$this->assertFalse($invokableGreeter->isManual);
$this->assertEquals([InvocablePromptFixture::class, '__invoke'], $invokableGreeter->handler);
$contentCreatorPrompt = $this->registry->getPrompt('content_creator');
$this->assertInstanceOf(PromptReference::class, $contentCreatorPrompt);
$this->assertFalse($contentCreatorPrompt->isManual);
$this->assertCount(3, $contentCreatorPrompt->completionProviders);
$templates = $this->registry->getResourceTemplates();
$this->assertCount(4, $templates);
$productTemplate = $this->registry->getResourceTemplate('product://{region}/details/{productId}');
$this->assertInstanceOf(ResourceTemplateReference::class, $productTemplate);
$this->assertFalse($productTemplate->isManual);
$this->assertEquals('product_details_template', $productTemplate->resourceTemplate->name);
$this->assertEquals(CompletionProviderFixture::class, $productTemplate->completionProviders['region']);
$this->assertEqualsCanonicalizing(['region', 'productId'], $productTemplate->getVariableNames());
$invokableUserTemplate = $this->registry->getResourceTemplate('invokable://user-profile/{userId}');
$this->assertInstanceOf(ResourceTemplateReference::class, $invokableUserTemplate);
$this->assertFalse($invokableUserTemplate->isManual);
$this->assertEquals([InvocableResourceTemplateFixture::class, '__invoke'], $invokableUserTemplate->handler);
}
public function testDoesNotDiscoverElementsFromExcludedDirectories()
{
$this->discoverer->discover(__DIR__, ['Fixtures']);
$this->assertInstanceOf(ToolReference::class, $this->registry->getTool('hidden_subdir_tool'));
$this->registry->clear();
$this->discoverer->discover(__DIR__, ['Fixtures'], ['SubDir']);
$this->assertNull($this->registry->getTool('hidden_subdir_tool'));
}
public function testHandlesEmptyDirectoriesOrDirectoriesWithNoPhpFiles()
{
$this->discoverer->discover(__DIR__, ['EmptyDir']);
$tools = $this->registry->getTools();
$this->assertEmpty($tools->references);
}
public function testCorrectlyInfersNamesAndDescriptionsFromMethodsOrClassesIfNotSetInAttribute()
{
$this->discoverer->discover(__DIR__, ['Fixtures']);
$repeatActionTool = $this->registry->getTool('repeatAction');
$this->assertEquals('repeatAction', $repeatActionTool->tool->name);
$this->assertEquals('A tool with more complex parameters and inferred name/description.', $repeatActionTool->tool->description);
$simplePrompt = $this->registry->getPrompt('simpleQuestionPrompt');
$this->assertEquals('simpleQuestionPrompt', $simplePrompt->prompt->name);
$this->assertNull($simplePrompt->prompt->description);
$invokableCalc = $this->registry->getTool('InvokableCalculator');
$this->assertEquals('InvokableCalculator', $invokableCalc->tool->name);
$this->assertEquals('An invokable calculator tool.', $invokableCalc->tool->description);
}
public function testDiscoversEnhancedCompletionProvidersWithValuesAndEnumAttributes()
{
$this->discoverer->discover(__DIR__, ['Fixtures']);
$contentPrompt = $this->registry->getPrompt('content_creator');
$this->assertInstanceOf(PromptReference::class, $contentPrompt);
$this->assertCount(3, $contentPrompt->completionProviders);
$typeProvider = $contentPrompt->completionProviders['type'];
$this->assertInstanceOf(ListCompletionProvider::class, $typeProvider);
$statusProvider = $contentPrompt->completionProviders['status'];
$this->assertInstanceOf(EnumCompletionProvider::class, $statusProvider);
$priorityProvider = $contentPrompt->completionProviders['priority'];
$this->assertInstanceOf(EnumCompletionProvider::class, $priorityProvider);
$contentTemplate = $this->registry->getResourceTemplate('content://{category}/{slug}');
$this->assertInstanceOf(ResourceTemplateReference::class, $contentTemplate);
$this->assertCount(1, $contentTemplate->completionProviders);
$categoryProvider = $contentTemplate->completionProviders['category'];
$this->assertInstanceOf(ListCompletionProvider::class, $categoryProvider);
}
}

View file

@ -0,0 +1,143 @@
<?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\Capability\Discovery;
use Mcp\Capability\Discovery\DocBlockParser;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\DocBlock\Tags\See;
use phpDocumentor\Reflection\DocBlock\Tags\Throws;
use PHPUnit\Framework\TestCase;
class DocBlockParserTest extends TestCase
{
private DocBlockParser $parser;
protected function setUp(): void
{
$this->parser = new DocBlockParser();
}
public function testGetSummaryReturnsCorrectSummary()
{
$method = new \ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryOnly');
$docComment = $method->getDocComment() ?: null;
$docBlock = $this->parser->parseDocBlock($docComment);
$this->assertEquals('Simple summary line.', $this->parser->getSummary($docBlock));
$method2 = new \ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryAndDescription');
$docComment2 = $method2->getDocComment() ?: null;
$docBlock2 = $this->parser->parseDocBlock($docComment2);
$this->assertEquals('Summary line here.', $this->parser->getSummary($docBlock2));
}
public function testGetDescriptionReturnsCorrectDescription()
{
$method = new \ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryAndDescription');
$docComment = $method->getDocComment() ?: null;
$docBlock = $this->parser->parseDocBlock($docComment);
$expectedDesc = "Summary line here.\n\nThis is a longer description spanning\nmultiple lines.\nIt might contain *markdown* or `code`.";
$this->assertEquals($expectedDesc, $this->parser->getDescription($docBlock));
$method2 = new \ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryOnly');
$docComment2 = $method2->getDocComment() ?: null;
$docBlock2 = $this->parser->parseDocBlock($docComment2);
$this->assertEquals('Simple summary line.', $this->parser->getDescription($docBlock2));
}
public function testGetParamTagsReturnsStructuredParamInfo()
{
$method = new \ReflectionMethod(DocBlockTestFixture::class, 'methodWithParams');
$docComment = $method->getDocComment() ?: null;
$docBlock = $this->parser->parseDocBlock($docComment);
$params = $this->parser->getParamTags($docBlock);
$this->assertCount(6, $params);
$this->assertArrayHasKey('$param1', $params);
$this->assertArrayHasKey('$param2', $params);
$this->assertArrayHasKey('$param3', $params);
$this->assertArrayHasKey('$param4', $params);
$this->assertArrayHasKey('$param5', $params);
$this->assertArrayHasKey('$param6', $params);
$this->assertInstanceOf(Param::class, $params['$param1']);
$this->assertEquals('param1', $params['$param1']->getVariableName());
$this->assertEquals('string', $this->parser->getParamTypeString($params['$param1']));
$this->assertEquals('description for string param', $this->parser->getParamDescription($params['$param1']));
$this->assertInstanceOf(Param::class, $params['$param2']);
$this->assertEquals('param2', $params['$param2']->getVariableName());
$this->assertEquals('int|null', $this->parser->getParamTypeString($params['$param2']));
$this->assertEquals('description for nullable int param', $this->parser->getParamDescription($params['$param2']));
$this->assertInstanceOf(Param::class, $params['$param3']);
$this->assertEquals('param3', $params['$param3']->getVariableName());
$this->assertEquals('bool', $this->parser->getParamTypeString($params['$param3']));
$this->assertEquals('nothing to say', $this->parser->getParamDescription($params['$param3']));
$this->assertInstanceOf(Param::class, $params['$param4']);
$this->assertEquals('param4', $params['$param4']->getVariableName());
$this->assertEquals('mixed', $this->parser->getParamTypeString($params['$param4']));
$this->assertEquals('Missing type', $this->parser->getParamDescription($params['$param4']));
$this->assertInstanceOf(Param::class, $params['$param5']);
$this->assertEquals('param5', $params['$param5']->getVariableName());
$this->assertEquals('array<string,mixed>', $this->parser->getParamTypeString($params['$param5']));
$this->assertEquals('array description', $this->parser->getParamDescription($params['$param5']));
$this->assertInstanceOf(Param::class, $params['$param6']);
$this->assertEquals('param6', $params['$param6']->getVariableName());
$this->assertEquals('stdClass', $this->parser->getParamTypeString($params['$param6']));
$this->assertEquals('object param', $this->parser->getParamDescription($params['$param6']));
}
public function testGetTagsByNameReturnsSpecificTags()
{
$method = new \ReflectionMethod(DocBlockTestFixture::class, 'methodWithMultipleTags');
$docComment = $method->getDocComment() ?: null;
$docBlock = $this->parser->parseDocBlock($docComment);
$this->assertInstanceOf(DocBlock::class, $docBlock);
$throwsTags = $docBlock->getTagsByName('throws');
$this->assertCount(1, $throwsTags);
$this->assertInstanceOf(Throws::class, $throwsTags[0]);
$this->assertEquals('\\RuntimeException', (string) $throwsTags[0]->getType());
$this->assertEquals('if processing fails', $throwsTags[0]->getDescription()->render());
$deprecatedTags = $docBlock->getTagsByName('deprecated');
$this->assertCount(1, $deprecatedTags);
$this->assertInstanceOf(Deprecated::class, $deprecatedTags[0]);
$this->assertEquals('use newMethod() instead', $deprecatedTags[0]->getDescription()->render());
$seeTags = $docBlock->getTagsByName('see');
$this->assertCount(1, $seeTags);
$this->assertInstanceOf(See::class, $seeTags[0]);
$this->assertStringContainsString('DocBlockTestFixture::newMethod()', (string) $seeTags[0]->getReference());
$nonExistentTags = $docBlock->getTagsByName('nosuchtag');
$this->assertEmpty($nonExistentTags);
}
public function testHandlesMethodWithNoDocblockGracefully()
{
$method = new \ReflectionMethod(DocBlockTestFixture::class, 'methodWithNoDocBlock');
$docComment = $method->getDocComment() ?: null;
$docBlock = $this->parser->parseDocBlock($docComment);
$this->assertNull($docBlock);
$this->assertNull($this->parser->getSummary($docBlock));
$this->assertNull($this->parser->getDescription($docBlock));
$this->assertEmpty($this->parser->getParamTags($docBlock));
}
}

View file

@ -0,0 +1,98 @@
<?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\Capability\Discovery;
use Mcp\Tests\Capability\Discovery\Missing;
/**
* A stub class for testing DocBlock parsing.
*/
class DocBlockTestFixture
{
/**
* Simple summary line.
*/
public function methodWithSummaryOnly(): void
{
}
/**
* Summary line here.
*
* This is a longer description spanning
* multiple lines.
* It might contain *markdown* or `code`.
*
* @since 1.0
*/
public function methodWithSummaryAndDescription(): void
{
}
/**
* Method with various parameter tags.
*
* @param string $param1 description for string param
* @param int|null $param2 description for nullable int param
* @param bool $param3 nothing to say
* @param $param4 Missing type
* @param array<string, mixed> $param5 array description
* @param \stdClass $param6 object param
*/
/* @phpstan-ignore-next-line missingType.parameter */
public function methodWithParams(string $param1, ?int $param2, bool $param3, $param4, array $param5, \stdClass $param6): void
{
}
/**
* Method with return tag.
*
* @return string the result of the operation
*/
public function methodWithReturn(): string
{
return '';
}
/**
* Method with multiple tags.
*
* @param float $value the value to process
*
* @return bool status of the operation
*
* @throws \RuntimeException if processing fails
*
* @deprecated use newMethod() instead
* @see DocBlockTestFixture::newMethod()
*/
public function methodWithMultipleTags(float $value): bool /* @phpstan-ignore throws.unusedType */
{
return true;
}
/**
* Malformed docblock - missing closing.
*/
public function methodWithMalformedDocBlock(): void
{
}
public function methodWithNoDocBlock(): void
{
}
// Some other method needed for a @see tag perhaps
public function newMethod(): void
{
}
}

View file

@ -0,0 +1,47 @@
<?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\Capability\Discovery\Fixtures;
use Mcp\Capability\Attribute\CompletionProvider;
use Mcp\Capability\Attribute\McpPrompt;
use Mcp\Tests\Unit\Capability\Attribute\CompletionProviderFixture;
class DiscoverablePromptHandler
{
/**
* Generates a creative story prompt.
*
* @param string $genre the genre of the story
* @param int $lengthWords approximate length in words
*
* @return array the prompt messages
*/
#[McpPrompt(name: 'creative_story_prompt')]
public function generateStoryPrompt(
#[CompletionProvider(provider: CompletionProviderFixture::class)]
string $genre,
int $lengthWords = 200,
): array {
return [
['role' => 'user', 'content' => "Write a {$genre} story about a lost robot, approximately {$lengthWords} words long."],
];
}
#[McpPrompt]
public function simpleQuestionPrompt(string $question): array
{
return [
['role' => 'user', 'content' => $question],
['role' => 'assistant', 'content' => 'I will try to answer that.'],
];
}
}

Some files were not shown because too many files have changed in this diff Show more