Registry Architecture Refactoring - Enhanced Separation of Concerns (#46)
* 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>
This commit is contained in:
parent
bfc3b82d19
commit
99bf56eef8
1 changed files with 4656 additions and 334 deletions
354
tests/Capability/Registry/RegistryTest.php
Normal file
354
tests/Capability/Registry/RegistryTest.php
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
<?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: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue