* refactor: Separate Registry concerns following SOLID principles [WIP] - Extract ReferenceProvider and ReferenceRegistryInterface interfaces - Create DefaultToolExecutor with ReferenceHandlerInterface - Remove execution responsibility from Registry class - Enable custom handler and executor implementations * refactor: implement SOLID principles with separated execution concerns * Create DefaultResourceReader, DefaultPromptGetter * Refactor JsonRpc Handler and RequestHandlers to use dedicated executors * Update ServerBuilder to support custom executors via dependency injection * chore: remove extra comments * feat: introduce DispatchableRegistry for enhanced tool and resource management * refactor: use proper interfaces * refactor: implement HandlerInterface for improved abstraction and flexibility * test: add unit tests for DispatchableRegistry and Registry classes * refactor: cover with unit tests Resource reader, Prompt getter and Tool executor * cs fix * phpstan fix * test: add unit tests for CallToolHandler, GetPromptHandler, PingHandler, and ReadResourceHandler * refactor: revert Handler * chore: remove style guide for tests * refactor: remove "Default" prefix from classes * refactor: remove DispatchableRegistry * refactor: use package specific exception classes * refactor: remove DispatchableRegistry * refactor: add logger support to PromptGetter and ResourceReader classes * Update src/Capability/Prompt/PromptGetter.php Co-authored-by: Christopher Hertel <mail@christopher-hertel.de> * refactor: Use FQN for Resource class to avoid cs-fixer misinterpreting it as `resource` * refactor: rename ToolExecutor to ToolCaller and related classes for clarity and consistency * refactor: add missed docblock * refactor: rename ToolExecutor references to ToolCaller * refactor: rename ToolCallerTest property and variable references * cs fix * ignore some phpstan errors --------- Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
354 lines
13 KiB
PHP
354 lines
13 KiB
PHP
<?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\Capability\Registry;
|
|
|
|
use Mcp\Capability\Prompt\Completion\EnumCompletionProvider;
|
|
use Mcp\Capability\Registry;
|
|
use Mcp\Schema\Prompt;
|
|
use Mcp\Schema\Resource;
|
|
use Mcp\Schema\ResourceTemplate;
|
|
use Mcp\Schema\ServerCapabilities;
|
|
use Mcp\Schema\Tool;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
class RegistryTest extends TestCase
|
|
{
|
|
private Registry $registry;
|
|
private LoggerInterface|MockObject $logger;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->logger = $this->createMock(LoggerInterface::class);
|
|
$this->registry = new Registry(null, $this->logger);
|
|
}
|
|
|
|
public function testConstructorWithDefaults(): void
|
|
{
|
|
$registry = new Registry();
|
|
$capabilities = $registry->getCapabilities();
|
|
|
|
$this->assertInstanceOf(ServerCapabilities::class, $capabilities);
|
|
$this->assertFalse($capabilities->toolsListChanged);
|
|
$this->assertFalse($capabilities->resourcesListChanged);
|
|
$this->assertFalse($capabilities->promptsListChanged);
|
|
}
|
|
|
|
public function testGetCapabilitiesWhenEmpty(): void
|
|
{
|
|
$this->logger
|
|
->expects($this->once())
|
|
->method('info')
|
|
->with('No capabilities registered on server.');
|
|
|
|
$capabilities = $this->registry->getCapabilities();
|
|
|
|
$this->assertFalse($capabilities->tools);
|
|
$this->assertFalse($capabilities->resources);
|
|
$this->assertFalse($capabilities->prompts);
|
|
}
|
|
|
|
public function testGetCapabilitiesWhenPopulated(): void
|
|
{
|
|
$tool = $this->createValidTool('test_tool');
|
|
$resource = $this->createValidResource('test://resource');
|
|
$prompt = $this->createValidPrompt('test_prompt');
|
|
$template = $this->createValidResourceTemplate('test://{id}');
|
|
|
|
$this->registry->registerTool($tool, fn () => 'result');
|
|
$this->registry->registerResource($resource, fn () => 'content');
|
|
$this->registry->registerPrompt($prompt, fn () => []);
|
|
$this->registry->registerResourceTemplate($template, fn () => 'template');
|
|
|
|
$capabilities = $this->registry->getCapabilities();
|
|
|
|
$this->assertTrue($capabilities->tools);
|
|
$this->assertTrue($capabilities->resources);
|
|
$this->assertTrue($capabilities->prompts);
|
|
$this->assertTrue($capabilities->completions);
|
|
$this->assertFalse($capabilities->resourcesSubscribe);
|
|
$this->assertFalse($capabilities->logging);
|
|
}
|
|
|
|
public function testRegisterToolWithManualFlag(): void
|
|
{
|
|
$tool = $this->createValidTool('test_tool');
|
|
$handler = fn () => 'result';
|
|
|
|
$this->registry->registerTool($tool, $handler, true);
|
|
|
|
$toolRef = $this->registry->getTool('test_tool');
|
|
$this->assertTrue($toolRef->isManual);
|
|
}
|
|
|
|
public function testRegisterToolIgnoresDiscoveredWhenManualExists(): void
|
|
{
|
|
$manualTool = $this->createValidTool('test_tool');
|
|
$discoveredTool = $this->createValidTool('test_tool');
|
|
|
|
$this->registry->registerTool($manualTool, fn () => 'manual', true);
|
|
|
|
$this->logger
|
|
->expects($this->once())
|
|
->method('debug')
|
|
->with("Ignoring discovered tool 'test_tool' as it conflicts with a manually registered one.");
|
|
|
|
$this->registry->registerTool($discoveredTool, fn () => 'discovered', false);
|
|
|
|
$toolRef = $this->registry->getTool('test_tool');
|
|
$this->assertTrue($toolRef->isManual);
|
|
}
|
|
|
|
public function testRegisterToolOverridesDiscoveredWithManual(): void
|
|
{
|
|
$discoveredTool = $this->createValidTool('test_tool');
|
|
$manualTool = $this->createValidTool('test_tool');
|
|
|
|
$this->registry->registerTool($discoveredTool, fn () => 'discovered', false);
|
|
$this->registry->registerTool($manualTool, fn () => 'manual', true);
|
|
|
|
$toolRef = $this->registry->getTool('test_tool');
|
|
$this->assertTrue($toolRef->isManual);
|
|
}
|
|
|
|
public function testRegisterResourceWithManualFlag(): void
|
|
{
|
|
$resource = $this->createValidResource('test://resource');
|
|
$handler = fn () => 'content';
|
|
|
|
$this->registry->registerResource($resource, $handler, true);
|
|
|
|
$resourceRef = $this->registry->getResource('test://resource');
|
|
$this->assertTrue($resourceRef->isManual);
|
|
}
|
|
|
|
public function testRegisterResourceIgnoresDiscoveredWhenManualExists(): void
|
|
{
|
|
$manualResource = $this->createValidResource('test://resource');
|
|
$discoveredResource = $this->createValidResource('test://resource');
|
|
|
|
$this->registry->registerResource($manualResource, fn () => 'manual', true);
|
|
|
|
$this->logger
|
|
->expects($this->once())
|
|
->method('debug')
|
|
->with("Ignoring discovered resource 'test://resource' as it conflicts with a manually registered one.");
|
|
|
|
$this->registry->registerResource($discoveredResource, fn () => 'discovered', false);
|
|
|
|
$resourceRef = $this->registry->getResource('test://resource');
|
|
$this->assertTrue($resourceRef->isManual);
|
|
}
|
|
|
|
public function testRegisterResourceTemplateWithCompletionProviders(): void
|
|
{
|
|
$template = $this->createValidResourceTemplate('test://{id}');
|
|
$completionProviders = ['id' => EnumCompletionProvider::class];
|
|
|
|
$this->registry->registerResourceTemplate($template, fn () => 'content', $completionProviders);
|
|
|
|
$templateRef = $this->registry->getResourceTemplate('test://{id}');
|
|
$this->assertEquals($completionProviders, $templateRef->completionProviders);
|
|
}
|
|
|
|
public function testRegisterResourceTemplateIgnoresDiscoveredWhenManualExists(): void
|
|
{
|
|
$manualTemplate = $this->createValidResourceTemplate('test://{id}');
|
|
$discoveredTemplate = $this->createValidResourceTemplate('test://{id}');
|
|
|
|
$this->registry->registerResourceTemplate($manualTemplate, fn () => 'manual', [], true);
|
|
|
|
$this->logger
|
|
->expects($this->once())
|
|
->method('debug')
|
|
->with("Ignoring discovered template 'test://{id}' as it conflicts with a manually registered one.");
|
|
|
|
$this->registry->registerResourceTemplate($discoveredTemplate, fn () => 'discovered', [], false);
|
|
|
|
$templateRef = $this->registry->getResourceTemplate('test://{id}');
|
|
$this->assertTrue($templateRef->isManual);
|
|
}
|
|
|
|
public function testRegisterPromptWithCompletionProviders(): void
|
|
{
|
|
$prompt = $this->createValidPrompt('test_prompt');
|
|
$completionProviders = ['param' => EnumCompletionProvider::class];
|
|
|
|
$this->registry->registerPrompt($prompt, fn () => [], $completionProviders);
|
|
|
|
$promptRef = $this->registry->getPrompt('test_prompt');
|
|
$this->assertEquals($completionProviders, $promptRef->completionProviders);
|
|
}
|
|
|
|
public function testRegisterPromptIgnoresDiscoveredWhenManualExists(): void
|
|
{
|
|
$manualPrompt = $this->createValidPrompt('test_prompt');
|
|
$discoveredPrompt = $this->createValidPrompt('test_prompt');
|
|
|
|
$this->registry->registerPrompt($manualPrompt, fn () => 'manual', [], true);
|
|
|
|
$this->logger
|
|
->expects($this->once())
|
|
->method('debug')
|
|
->with("Ignoring discovered prompt 'test_prompt' as it conflicts with a manually registered one.");
|
|
|
|
$this->registry->registerPrompt($discoveredPrompt, fn () => 'discovered', [], false);
|
|
|
|
$promptRef = $this->registry->getPrompt('test_prompt');
|
|
$this->assertTrue($promptRef->isManual);
|
|
}
|
|
|
|
public function testClearRemovesOnlyDiscoveredElements(): void
|
|
{
|
|
$manualTool = $this->createValidTool('manual_tool');
|
|
$discoveredTool = $this->createValidTool('discovered_tool');
|
|
$manualResource = $this->createValidResource('test://manual');
|
|
$discoveredResource = $this->createValidResource('test://discovered');
|
|
$manualPrompt = $this->createValidPrompt('manual_prompt');
|
|
$discoveredPrompt = $this->createValidPrompt('discovered_prompt');
|
|
$manualTemplate = $this->createValidResourceTemplate('manual://{id}');
|
|
$discoveredTemplate = $this->createValidResourceTemplate('discovered://{id}');
|
|
|
|
$this->registry->registerTool($manualTool, fn () => 'manual', true);
|
|
$this->registry->registerTool($discoveredTool, fn () => 'discovered', false);
|
|
$this->registry->registerResource($manualResource, fn () => 'manual', true);
|
|
$this->registry->registerResource($discoveredResource, fn () => 'discovered', false);
|
|
$this->registry->registerPrompt($manualPrompt, fn () => [], [], true);
|
|
$this->registry->registerPrompt($discoveredPrompt, fn () => [], [], false);
|
|
$this->registry->registerResourceTemplate($manualTemplate, fn () => 'manual', [], true);
|
|
$this->registry->registerResourceTemplate($discoveredTemplate, fn () => 'discovered', [], false);
|
|
|
|
$this->logger
|
|
->expects($this->once())
|
|
->method('debug')
|
|
->with('Removed 4 discovered elements from internal registry.');
|
|
|
|
$this->registry->clear();
|
|
|
|
$this->assertNotNull($this->registry->getTool('manual_tool'));
|
|
$this->assertNull($this->registry->getTool('discovered_tool'));
|
|
$this->assertNotNull($this->registry->getResource('test://manual'));
|
|
$this->assertNull(
|
|
$this->registry->getResource('test://discovered', false),
|
|
); // Don't include templates to avoid debug log
|
|
$this->assertNotNull($this->registry->getPrompt('manual_prompt'));
|
|
$this->assertNull($this->registry->getPrompt('discovered_prompt'));
|
|
$this->assertNotNull($this->registry->getResourceTemplate('manual://{id}'));
|
|
$this->assertNull($this->registry->getResourceTemplate('discovered://{id}'));
|
|
}
|
|
|
|
public function testClearLogsNothingWhenNoDiscoveredElements(): void
|
|
{
|
|
$manualTool = $this->createValidTool('manual_tool');
|
|
$this->registry->registerTool($manualTool, fn () => 'manual', true);
|
|
|
|
$this->logger
|
|
->expects($this->never())
|
|
->method('debug');
|
|
|
|
$this->registry->clear();
|
|
|
|
$this->assertNotNull($this->registry->getTool('manual_tool'));
|
|
}
|
|
|
|
public function testRegisterToolHandlesStringHandler(): void
|
|
{
|
|
$tool = $this->createValidTool('test_tool');
|
|
$handler = 'TestClass::testMethod';
|
|
|
|
$this->registry->registerTool($tool, $handler);
|
|
|
|
$toolRef = $this->registry->getTool('test_tool');
|
|
$this->assertEquals($handler, $toolRef->handler);
|
|
}
|
|
|
|
public function testRegisterToolHandlesArrayHandler(): void
|
|
{
|
|
$tool = $this->createValidTool('test_tool');
|
|
$handler = ['TestClass', 'testMethod'];
|
|
|
|
$this->registry->registerTool($tool, $handler);
|
|
|
|
$toolRef = $this->registry->getTool('test_tool');
|
|
$this->assertEquals($handler, $toolRef->handler);
|
|
}
|
|
|
|
public function testRegisterResourceHandlesCallableHandler(): void
|
|
{
|
|
$resource = $this->createValidResource('test://resource');
|
|
$handler = fn () => 'content';
|
|
|
|
$this->registry->registerResource($resource, $handler);
|
|
|
|
$resourceRef = $this->registry->getResource('test://resource');
|
|
$this->assertEquals($handler, $resourceRef->handler);
|
|
}
|
|
|
|
public function testMultipleRegistrationsOfSameElementWithSameType(): void
|
|
{
|
|
$tool1 = $this->createValidTool('test_tool');
|
|
$tool2 = $this->createValidTool('test_tool');
|
|
|
|
$this->registry->registerTool($tool1, fn () => 'first', false);
|
|
$this->registry->registerTool($tool2, fn () => 'second', false);
|
|
|
|
// Second registration should override the first
|
|
$toolRef = $this->registry->getTool('test_tool');
|
|
$this->assertEquals('second', ($toolRef->handler)());
|
|
}
|
|
|
|
private function createValidTool(string $name): Tool
|
|
{
|
|
return new Tool(
|
|
name: $name,
|
|
inputSchema: [
|
|
'type' => 'object',
|
|
'properties' => [
|
|
'param' => ['type' => 'string'],
|
|
],
|
|
'required' => null,
|
|
],
|
|
description: "Test tool: {$name}",
|
|
annotations: null,
|
|
);
|
|
}
|
|
|
|
private function createValidResource(string $uri): Resource
|
|
{
|
|
return new Resource(
|
|
uri: $uri,
|
|
name: 'test_resource',
|
|
description: 'Test resource',
|
|
mimeType: 'text/plain',
|
|
);
|
|
}
|
|
|
|
private function createValidResourceTemplate(string $uriTemplate): ResourceTemplate
|
|
{
|
|
return new ResourceTemplate(
|
|
uriTemplate: $uriTemplate,
|
|
name: 'test_template',
|
|
description: 'Test resource template',
|
|
mimeType: 'text/plain',
|
|
);
|
|
}
|
|
|
|
private function createValidPrompt(string $name): Prompt
|
|
{
|
|
return new Prompt(
|
|
name: $name,
|
|
description: "Test prompt: {$name}",
|
|
arguments: [],
|
|
);
|
|
}
|
|
}
|