* 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>
522 lines
20 KiB
PHP
522 lines
20 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\Resource;
|
|
|
|
use Mcp\Capability\Registry\ReferenceHandlerInterface;
|
|
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
|
use Mcp\Capability\Registry\ResourceReference;
|
|
use Mcp\Capability\Registry\ResourceTemplateReference;
|
|
use Mcp\Capability\Resource\ResourceReader;
|
|
use Mcp\Exception\RegistryException;
|
|
use Mcp\Exception\ResourceNotFoundException;
|
|
use Mcp\Exception\ResourceReadException;
|
|
use Mcp\Schema\Content\BlobResourceContents;
|
|
use Mcp\Schema\Content\TextResourceContents;
|
|
use Mcp\Schema\Request\ReadResourceRequest;
|
|
use Mcp\Schema\Resource;
|
|
use Mcp\Schema\ResourceTemplate;
|
|
use Mcp\Schema\Result\ReadResourceResult;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
class ResourceReaderTest extends TestCase
|
|
{
|
|
private ResourceReader $resourceReader;
|
|
private ReferenceProviderInterface|MockObject $referenceProvider;
|
|
private ReferenceHandlerInterface|MockObject $referenceHandler;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->referenceProvider = $this->createMock(ReferenceProviderInterface::class);
|
|
$this->referenceHandler = $this->createMock(ReferenceHandlerInterface::class);
|
|
|
|
$this->resourceReader = new ResourceReader(
|
|
$this->referenceProvider,
|
|
$this->referenceHandler,
|
|
);
|
|
}
|
|
|
|
public function testReadResourceSuccessfullyWithStringResult(): void
|
|
{
|
|
$request = new ReadResourceRequest('file://test.txt');
|
|
$resource = $this->createValidResource('file://test.txt', 'test', 'text/plain');
|
|
$resourceReference = new ResourceReference($resource, fn () => 'test content');
|
|
$handlerResult = 'test content';
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('file://test.txt')
|
|
->willReturn($resourceReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($resourceReference, ['uri' => 'file://test.txt'])
|
|
->willReturn($handlerResult);
|
|
|
|
$result = $this->resourceReader->read($request);
|
|
|
|
$this->assertInstanceOf(ReadResourceResult::class, $result);
|
|
$this->assertCount(1, $result->contents);
|
|
$this->assertInstanceOf(TextResourceContents::class, $result->contents[0]);
|
|
$this->assertEquals('test content', $result->contents[0]->text);
|
|
$this->assertEquals('file://test.txt', $result->contents[0]->uri);
|
|
$this->assertEquals('text/plain', $result->contents[0]->mimeType);
|
|
}
|
|
|
|
public function testReadResourceSuccessfullyWithArrayResult(): void
|
|
{
|
|
$request = new ReadResourceRequest('api://data');
|
|
$resource = $this->createValidResource('api://data', 'data', 'application/json');
|
|
$resourceReference = new ResourceReference($resource, fn () => ['key' => 'value', 'count' => 42]);
|
|
$handlerResult = ['key' => 'value', 'count' => 42];
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('api://data')
|
|
->willReturn($resourceReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($resourceReference, ['uri' => 'api://data'])
|
|
->willReturn($handlerResult);
|
|
|
|
$result = $this->resourceReader->read($request);
|
|
|
|
$this->assertInstanceOf(ReadResourceResult::class, $result);
|
|
$this->assertCount(1, $result->contents);
|
|
$this->assertInstanceOf(TextResourceContents::class, $result->contents[0]);
|
|
$this->assertJsonStringEqualsJsonString(
|
|
json_encode($handlerResult, \JSON_PRETTY_PRINT),
|
|
$result->contents[0]->text,
|
|
);
|
|
$this->assertEquals('api://data', $result->contents[0]->uri);
|
|
$this->assertEquals('application/json', $result->contents[0]->mimeType);
|
|
}
|
|
|
|
public function testReadResourceSuccessfullyWithBlobResult(): void
|
|
{
|
|
$request = new ReadResourceRequest('file://image.png');
|
|
$resource = $this->createValidResource('file://image.png', 'image', 'image/png');
|
|
|
|
$handlerResult = [
|
|
'blob' => base64_encode('binary data'),
|
|
'mimeType' => 'image/png',
|
|
];
|
|
|
|
$resourceReference = new ResourceReference($resource, fn () => $handlerResult);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('file://image.png')
|
|
->willReturn($resourceReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($resourceReference, ['uri' => 'file://image.png'])
|
|
->willReturn($handlerResult);
|
|
|
|
$result = $this->resourceReader->read($request);
|
|
|
|
$this->assertInstanceOf(ReadResourceResult::class, $result);
|
|
$this->assertCount(1, $result->contents);
|
|
$this->assertInstanceOf(BlobResourceContents::class, $result->contents[0]);
|
|
$this->assertEquals(base64_encode('binary data'), $result->contents[0]->blob);
|
|
$this->assertEquals('file://image.png', $result->contents[0]->uri);
|
|
$this->assertEquals('image/png', $result->contents[0]->mimeType);
|
|
}
|
|
|
|
public function testReadResourceSuccessfullyWithResourceContentResult(): void
|
|
{
|
|
$request = new ReadResourceRequest('custom://resource');
|
|
$resource = $this->createValidResource('custom://resource', 'resource', 'text/plain');
|
|
$textContent = new TextResourceContents('custom://resource', 'text/plain', 'direct content');
|
|
$resourceReference = new ResourceReference($resource, fn () => $textContent);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('custom://resource')
|
|
->willReturn($resourceReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($resourceReference, ['uri' => 'custom://resource'])
|
|
->willReturn($textContent);
|
|
|
|
$result = $this->resourceReader->read($request);
|
|
|
|
$this->assertInstanceOf(ReadResourceResult::class, $result);
|
|
$this->assertCount(1, $result->contents);
|
|
$this->assertSame($textContent, $result->contents[0]);
|
|
}
|
|
|
|
public function testReadResourceSuccessfullyWithMultipleContentResults(): void
|
|
{
|
|
$request = new ReadResourceRequest('multi://content');
|
|
$resource = $this->createValidResource('multi://content', 'content', 'application/json');
|
|
$content1 = new TextResourceContents('multi://content', 'text/plain', 'first content');
|
|
$content2 = new TextResourceContents('multi://content', 'text/plain', 'second content');
|
|
$handlerResult = [$content1, $content2];
|
|
$resourceReference = new ResourceReference($resource, fn () => $handlerResult);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('multi://content')
|
|
->willReturn($resourceReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($resourceReference, ['uri' => 'multi://content'])
|
|
->willReturn($handlerResult);
|
|
|
|
$result = $this->resourceReader->read($request);
|
|
|
|
$this->assertInstanceOf(ReadResourceResult::class, $result);
|
|
$this->assertCount(2, $result->contents);
|
|
$this->assertSame($content1, $result->contents[0]);
|
|
$this->assertSame($content2, $result->contents[1]);
|
|
}
|
|
|
|
public function testReadResourceTemplate(): void
|
|
{
|
|
$request = new ReadResourceRequest('users://123');
|
|
$resourceTemplate = $this->createValidResourceTemplate('users://{id}', 'user_template');
|
|
$templateReference = new ResourceTemplateReference(
|
|
$resourceTemplate,
|
|
fn () => ['id' => 123, 'name' => 'Test User'],
|
|
);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('users://123')
|
|
->willReturn($templateReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($templateReference, ['uri' => 'users://123'])
|
|
->willReturn(['id' => 123, 'name' => 'Test User']);
|
|
|
|
$result = $this->resourceReader->read($request);
|
|
|
|
$this->assertInstanceOf(ReadResourceResult::class, $result);
|
|
$this->assertCount(1, $result->contents);
|
|
$this->assertInstanceOf(TextResourceContents::class, $result->contents[0]);
|
|
$this->assertJsonStringEqualsJsonString(
|
|
json_encode(['id' => 123, 'name' => 'Test User'], \JSON_PRETTY_PRINT),
|
|
$result->contents[0]->text,
|
|
);
|
|
}
|
|
|
|
public function testReadResourceThrowsExceptionWhenResourceNotFound(): void
|
|
{
|
|
$request = new ReadResourceRequest('nonexistent://resource');
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('nonexistent://resource')
|
|
->willReturn(null);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->never())
|
|
->method('handle');
|
|
|
|
$this->expectException(ResourceNotFoundException::class);
|
|
$this->expectExceptionMessage('Resource not found for uri: "nonexistent://resource".');
|
|
|
|
$this->resourceReader->read($request);
|
|
}
|
|
|
|
public function testReadResourceThrowsRegistryExceptionWhenHandlerFails(): void
|
|
{
|
|
$request = new ReadResourceRequest('failing://resource');
|
|
$resource = $this->createValidResource('failing://resource', 'failing', 'text/plain');
|
|
$resourceReference = new ResourceReference($resource, fn () => throw new \RuntimeException('Handler failed'));
|
|
$handlerException = new RegistryException('Handler execution failed');
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('failing://resource')
|
|
->willReturn($resourceReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($resourceReference, ['uri' => 'failing://resource'])
|
|
->willThrowException($handlerException);
|
|
|
|
$this->expectException(ResourceReadException::class);
|
|
$this->expectExceptionMessage('Handler execution failed');
|
|
|
|
$this->resourceReader->read($request);
|
|
}
|
|
|
|
public function testReadResourcePassesCorrectArgumentsToHandler(): void
|
|
{
|
|
$request = new ReadResourceRequest('test://resource');
|
|
$resource = $this->createValidResource('test://resource', 'test', 'text/plain');
|
|
$resourceReference = new ResourceReference($resource, fn () => 'test');
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('test://resource')
|
|
->willReturn($resourceReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with(
|
|
$this->identicalTo($resourceReference),
|
|
$this->equalTo(['uri' => 'test://resource']),
|
|
)
|
|
->willReturn('test');
|
|
|
|
$result = $this->resourceReader->read($request);
|
|
|
|
$this->assertInstanceOf(ReadResourceResult::class, $result);
|
|
}
|
|
|
|
public function testReadResourceWithEmptyStringResult(): void
|
|
{
|
|
$request = new ReadResourceRequest('empty://resource');
|
|
$resource = $this->createValidResource('empty://resource', 'empty', 'text/plain');
|
|
$resourceReference = new ResourceReference($resource, fn () => '');
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('empty://resource')
|
|
->willReturn($resourceReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($resourceReference, ['uri' => 'empty://resource'])
|
|
->willReturn('');
|
|
|
|
$result = $this->resourceReader->read($request);
|
|
|
|
$this->assertInstanceOf(ReadResourceResult::class, $result);
|
|
$this->assertCount(1, $result->contents);
|
|
$this->assertInstanceOf(TextResourceContents::class, $result->contents[0]);
|
|
$this->assertEquals('', $result->contents[0]->text);
|
|
}
|
|
|
|
public function testReadResourceWithEmptyArrayResult(): void
|
|
{
|
|
$request = new ReadResourceRequest('empty://array');
|
|
$resource = $this->createValidResource('empty://array', 'array', 'application/json');
|
|
$resourceReference = new ResourceReference($resource, fn () => []);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('empty://array')
|
|
->willReturn($resourceReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($resourceReference, ['uri' => 'empty://array'])
|
|
->willReturn([]);
|
|
|
|
$result = $this->resourceReader->read($request);
|
|
|
|
$this->assertInstanceOf(ReadResourceResult::class, $result);
|
|
$this->assertCount(1, $result->contents);
|
|
$this->assertInstanceOf(TextResourceContents::class, $result->contents[0]);
|
|
$this->assertEquals('[]', $result->contents[0]->text);
|
|
$this->assertEquals('application/json', $result->contents[0]->mimeType);
|
|
}
|
|
|
|
public function testReadResourceWithNullResult(): void
|
|
{
|
|
$request = new ReadResourceRequest('null://resource');
|
|
$resource = $this->createValidResource('null://resource', 'null', 'text/plain');
|
|
$resourceReference = new ResourceReference($resource, fn () => null);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('null://resource')
|
|
->willReturn($resourceReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($resourceReference, ['uri' => 'null://resource'])
|
|
->willReturn(null);
|
|
|
|
// The formatResult method in ResourceReference should handle null values
|
|
$this->expectException(\RuntimeException::class);
|
|
|
|
$this->resourceReader->read($request);
|
|
}
|
|
|
|
public function testReadResourceWithDifferentMimeTypes(): void
|
|
{
|
|
$request = new ReadResourceRequest('xml://data');
|
|
$resource = $this->createValidResource('xml://data', 'data', 'application/xml');
|
|
$resourceReference = new ResourceReference($resource, fn () => '<xml><data>value</data></xml>');
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('xml://data')
|
|
->willReturn($resourceReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($resourceReference, ['uri' => 'xml://data'])
|
|
->willReturn('<xml><data>value</data></xml>');
|
|
|
|
$result = $this->resourceReader->read($request);
|
|
|
|
$this->assertInstanceOf(ReadResourceResult::class, $result);
|
|
$this->assertCount(1, $result->contents);
|
|
$this->assertInstanceOf(TextResourceContents::class, $result->contents[0]);
|
|
// The MIME type should be guessed from content since formatResult handles the conversion
|
|
$this->assertEquals('<xml><data>value</data></xml>', $result->contents[0]->text);
|
|
}
|
|
|
|
public function testReadResourceWithJsonMimeTypeAndArrayResult(): void
|
|
{
|
|
$request = new ReadResourceRequest('api://json');
|
|
$resource = $this->createValidResource('api://json', 'json', 'application/json');
|
|
$resourceReference = new ResourceReference($resource, fn () => ['formatted' => true, 'data' => [1, 2, 3]]);
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('api://json')
|
|
->willReturn($resourceReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($resourceReference, ['uri' => 'api://json'])
|
|
->willReturn(['formatted' => true, 'data' => [1, 2, 3]]);
|
|
|
|
$result = $this->resourceReader->read($request);
|
|
|
|
$this->assertInstanceOf(ReadResourceResult::class, $result);
|
|
$this->assertCount(1, $result->contents);
|
|
$this->assertInstanceOf(TextResourceContents::class, $result->contents[0]);
|
|
$this->assertEquals('application/json', $result->contents[0]->mimeType);
|
|
$this->assertJsonStringEqualsJsonString(
|
|
json_encode(['formatted' => true, 'data' => [1, 2, 3]], \JSON_PRETTY_PRINT),
|
|
$result->contents[0]->text,
|
|
);
|
|
}
|
|
|
|
public function testReadResourceCallsFormatResultOnReference(): void
|
|
{
|
|
$request = new ReadResourceRequest('format://test');
|
|
$resource = $this->createValidResource('format://test', 'format', 'text/plain');
|
|
|
|
// Create a mock ResourceReference to verify formatResult is called
|
|
$resourceReference = $this
|
|
->getMockBuilder(ResourceReference::class)
|
|
->setConstructorArgs([$resource, fn () => 'test', false])
|
|
->onlyMethods(['formatResult'])
|
|
->getMock();
|
|
|
|
$handlerResult = 'test result';
|
|
$formattedResult = [new TextResourceContents('format://test', 'text/plain', 'formatted content')];
|
|
|
|
$this->referenceProvider
|
|
->expects($this->once())
|
|
->method('getResource')
|
|
->with('format://test')
|
|
->willReturn($resourceReference);
|
|
|
|
$this->referenceHandler
|
|
->expects($this->once())
|
|
->method('handle')
|
|
->with($resourceReference, ['uri' => 'format://test'])
|
|
->willReturn($handlerResult);
|
|
|
|
$resourceReference
|
|
->expects($this->once())
|
|
->method('formatResult')
|
|
->with($handlerResult, 'format://test')
|
|
->willReturn($formattedResult);
|
|
|
|
$result = $this->resourceReader->read($request);
|
|
|
|
$this->assertInstanceOf(ReadResourceResult::class, $result);
|
|
$this->assertSame($formattedResult, $result->contents);
|
|
}
|
|
|
|
public function testConstructorWithDefaultLogger(): void
|
|
{
|
|
$resourceReader = new ResourceReader(
|
|
$this->referenceProvider,
|
|
$this->referenceHandler,
|
|
);
|
|
|
|
$this->assertInstanceOf(ResourceReader::class, $resourceReader);
|
|
}
|
|
|
|
public function testConstructorWithCustomLogger(): void
|
|
{
|
|
$logger = $this->createMock(LoggerInterface::class);
|
|
$resourceReader = new ResourceReader(
|
|
$this->referenceProvider,
|
|
$this->referenceHandler,
|
|
$logger,
|
|
);
|
|
|
|
$this->assertInstanceOf(ResourceReader::class, $resourceReader);
|
|
}
|
|
|
|
private function createValidResource(string $uri, string $name, ?string $mimeType = null): Resource
|
|
{
|
|
return new Resource(
|
|
uri: $uri,
|
|
name: $name,
|
|
description: "Test resource: {$name}",
|
|
mimeType: $mimeType,
|
|
size: null,
|
|
annotations: null,
|
|
);
|
|
}
|
|
|
|
private function createValidResourceTemplate(
|
|
string $uriTemplate,
|
|
string $name,
|
|
?string $mimeType = null,
|
|
): ResourceTemplate {
|
|
return new ResourceTemplate(
|
|
uriTemplate: $uriTemplate,
|
|
name: $name,
|
|
description: "Test resource template: {$name}",
|
|
mimeType: $mimeType,
|
|
annotations: null,
|
|
);
|
|
}
|
|
}
|