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
11 changed files with 4656 additions and 334 deletions
638
tests/Capability/Prompt/PromptGetterTest.php
Normal file
638
tests/Capability/Prompt/PromptGetterTest.php
Normal file
|
|
@ -0,0 +1,638 @@
|
|||
<?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\Prompt;
|
||||
|
||||
use Mcp\Capability\Prompt\Completion\EnumCompletionProvider;
|
||||
use Mcp\Capability\Prompt\PromptGetter;
|
||||
use Mcp\Capability\Registry\PromptReference;
|
||||
use Mcp\Capability\Registry\ReferenceHandlerInterface;
|
||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
||||
use Mcp\Exception\PromptGetException;
|
||||
use Mcp\Exception\PromptNotFoundException;
|
||||
use Mcp\Exception\RegistryException;
|
||||
use Mcp\Schema\Content\PromptMessage;
|
||||
use Mcp\Schema\Content\TextContent;
|
||||
use Mcp\Schema\Enum\Role;
|
||||
use Mcp\Schema\Prompt;
|
||||
use Mcp\Schema\Request\GetPromptRequest;
|
||||
use Mcp\Schema\Result\GetPromptResult;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class PromptGetterTest extends TestCase
|
||||
{
|
||||
private PromptGetter $promptGetter;
|
||||
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->promptGetter = new PromptGetter(
|
||||
$this->referenceProvider,
|
||||
$this->referenceHandler,
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetExecutesPromptSuccessfully(): void
|
||||
{
|
||||
$request = new GetPromptRequest('test_prompt', ['param' => 'value']);
|
||||
$prompt = $this->createValidPrompt('test_prompt');
|
||||
$promptReference = new PromptReference($prompt, fn () => 'test result');
|
||||
$handlerResult = [
|
||||
'role' => 'user',
|
||||
'content' => 'Generated prompt content',
|
||||
];
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('test_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, ['param' => 'value'])
|
||||
->willReturn($handlerResult);
|
||||
|
||||
$result = $this->promptGetter->get($request);
|
||||
|
||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
||||
$this->assertCount(1, $result->messages);
|
||||
$this->assertInstanceOf(PromptMessage::class, $result->messages[0]);
|
||||
$this->assertEquals(Role::User, $result->messages[0]->role);
|
||||
$this->assertInstanceOf(TextContent::class, $result->messages[0]->content);
|
||||
$this->assertEquals('Generated prompt content', $result->messages[0]->content->text);
|
||||
}
|
||||
|
||||
public function testGetWithEmptyArguments(): void
|
||||
{
|
||||
$request = new GetPromptRequest('empty_args_prompt', []);
|
||||
$prompt = $this->createValidPrompt('empty_args_prompt');
|
||||
$promptReference = new PromptReference($prompt, fn () => 'Empty args content');
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('empty_args_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, [])
|
||||
->willReturn([
|
||||
'role' => 'user',
|
||||
'content' => 'Empty args content',
|
||||
]);
|
||||
|
||||
$result = $this->promptGetter->get($request);
|
||||
|
||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
||||
$this->assertCount(1, $result->messages);
|
||||
}
|
||||
|
||||
public function testGetWithComplexArguments(): void
|
||||
{
|
||||
$arguments = [
|
||||
'string_param' => 'value',
|
||||
'int_param' => 42,
|
||||
'bool_param' => true,
|
||||
'array_param' => ['nested' => 'data'],
|
||||
'null_param' => null,
|
||||
];
|
||||
$request = new GetPromptRequest('complex_prompt', $arguments);
|
||||
$prompt = $this->createValidPrompt('complex_prompt');
|
||||
$promptReference = new PromptReference($prompt, fn () => 'Complex content');
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('complex_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, $arguments)
|
||||
->willReturn([
|
||||
'role' => 'assistant',
|
||||
'content' => 'Complex response',
|
||||
]);
|
||||
|
||||
$result = $this->promptGetter->get($request);
|
||||
|
||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
||||
$this->assertCount(1, $result->messages);
|
||||
}
|
||||
|
||||
public function testGetThrowsInvalidArgumentExceptionWhenPromptNotFound(): void
|
||||
{
|
||||
$request = new GetPromptRequest('nonexistent_prompt', ['param' => 'value']);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('nonexistent_prompt')
|
||||
->willReturn(null);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->never())
|
||||
->method('handle');
|
||||
|
||||
$this->expectException(PromptNotFoundException::class);
|
||||
$this->expectExceptionMessage('Prompt not found for name: "nonexistent_prompt".');
|
||||
|
||||
$this->promptGetter->get($request);
|
||||
}
|
||||
|
||||
public function testGetThrowsRegistryExceptionWhenHandlerFails(): void
|
||||
{
|
||||
$request = new GetPromptRequest('failing_prompt', ['param' => 'value']);
|
||||
$prompt = $this->createValidPrompt('failing_prompt');
|
||||
$promptReference = new PromptReference($prompt, fn () => throw new \RuntimeException('Handler failed'));
|
||||
$handlerException = RegistryException::internalError('Handler failed');
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('failing_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, ['param' => 'value'])
|
||||
->willThrowException($handlerException);
|
||||
|
||||
$this->expectException(PromptGetException::class);
|
||||
|
||||
$this->promptGetter->get($request);
|
||||
}
|
||||
|
||||
public function testGetHandlesJsonExceptionDuringFormatting(): void
|
||||
{
|
||||
$request = new GetPromptRequest('json_error_prompt', []);
|
||||
$prompt = $this->createValidPrompt('json_error_prompt');
|
||||
|
||||
// Create a mock PromptReference that will throw JsonException during formatResult
|
||||
$promptReference = $this->createMock(PromptReference::class);
|
||||
$promptReference->expects($this->once())
|
||||
->method('formatResult')
|
||||
->willThrowException(new \JsonException('JSON encoding failed'));
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('json_error_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, [])
|
||||
->willReturn('some result');
|
||||
|
||||
$this->expectException(PromptGetException::class);
|
||||
$this->expectExceptionMessage('JSON encoding failed');
|
||||
|
||||
$this->promptGetter->get($request);
|
||||
}
|
||||
|
||||
public function testGetHandlesArrayOfMessages(): void
|
||||
{
|
||||
$request = new GetPromptRequest('multi_message_prompt', ['context' => 'test']);
|
||||
$prompt = $this->createValidPrompt('multi_message_prompt');
|
||||
$promptReference = new PromptReference($prompt, fn () => 'Multiple messages');
|
||||
$handlerResult = [
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => 'First message',
|
||||
],
|
||||
[
|
||||
'role' => 'assistant',
|
||||
'content' => 'Second message',
|
||||
],
|
||||
];
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('multi_message_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, ['context' => 'test'])
|
||||
->willReturn($handlerResult);
|
||||
|
||||
$result = $this->promptGetter->get($request);
|
||||
|
||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
||||
$this->assertCount(2, $result->messages);
|
||||
$this->assertEquals(Role::User, $result->messages[0]->role);
|
||||
$this->assertEquals('First message', $result->messages[0]->content->text);
|
||||
$this->assertEquals(Role::Assistant, $result->messages[1]->role);
|
||||
$this->assertEquals('Second message', $result->messages[1]->content->text);
|
||||
}
|
||||
|
||||
public function testGetHandlesPromptMessageObjects(): void
|
||||
{
|
||||
$request = new GetPromptRequest('prompt_message_prompt', []);
|
||||
$prompt = $this->createValidPrompt('prompt_message_prompt');
|
||||
$promptMessage = new PromptMessage(
|
||||
Role::User,
|
||||
new TextContent('Direct prompt message')
|
||||
);
|
||||
$promptReference = new PromptReference($prompt, fn () => $promptMessage);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('prompt_message_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, [])
|
||||
->willReturn($promptMessage);
|
||||
|
||||
$result = $this->promptGetter->get($request);
|
||||
|
||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
||||
$this->assertCount(1, $result->messages);
|
||||
$this->assertSame($promptMessage, $result->messages[0]);
|
||||
}
|
||||
|
||||
public function testGetHandlesUserAssistantStructure(): void
|
||||
{
|
||||
$request = new GetPromptRequest('user_assistant_prompt', []);
|
||||
$prompt = $this->createValidPrompt('user_assistant_prompt');
|
||||
$promptReference = new PromptReference($prompt, fn () => 'Conversation content');
|
||||
$handlerResult = [
|
||||
'user' => 'What is the weather?',
|
||||
'assistant' => 'I can help you check the weather.',
|
||||
];
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('user_assistant_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, [])
|
||||
->willReturn($handlerResult);
|
||||
|
||||
$result = $this->promptGetter->get($request);
|
||||
|
||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
||||
$this->assertCount(2, $result->messages);
|
||||
$this->assertEquals(Role::User, $result->messages[0]->role);
|
||||
$this->assertEquals('What is the weather?', $result->messages[0]->content->text);
|
||||
$this->assertEquals(Role::Assistant, $result->messages[1]->role);
|
||||
$this->assertEquals('I can help you check the weather.', $result->messages[1]->content->text);
|
||||
}
|
||||
|
||||
public function testGetHandlesEmptyArrayResult(): void
|
||||
{
|
||||
$request = new GetPromptRequest('empty_array_prompt', []);
|
||||
$prompt = $this->createValidPrompt('empty_array_prompt');
|
||||
$promptReference = new PromptReference($prompt, fn () => []);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('empty_array_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, [])
|
||||
->willReturn([]);
|
||||
|
||||
$result = $this->promptGetter->get($request);
|
||||
|
||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
||||
$this->assertCount(0, $result->messages);
|
||||
}
|
||||
|
||||
public function testGetWithTypedContentStructure(): void
|
||||
{
|
||||
$request = new GetPromptRequest('typed_content_prompt', []);
|
||||
$prompt = $this->createValidPrompt('typed_content_prompt');
|
||||
$promptReference = new PromptReference($prompt, fn () => 'Typed content');
|
||||
$handlerResult = [
|
||||
'role' => 'user',
|
||||
'content' => [
|
||||
'type' => 'text',
|
||||
'text' => 'Typed text content',
|
||||
],
|
||||
];
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('typed_content_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, [])
|
||||
->willReturn($handlerResult);
|
||||
|
||||
$result = $this->promptGetter->get($request);
|
||||
|
||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
||||
$this->assertCount(1, $result->messages);
|
||||
$this->assertEquals(Role::User, $result->messages[0]->role);
|
||||
$this->assertEquals('Typed text content', $result->messages[0]->content->text);
|
||||
}
|
||||
|
||||
public function testGetWithPromptReferenceHavingCompletionProviders(): void
|
||||
{
|
||||
$request = new GetPromptRequest('completion_prompt', ['param' => 'value']);
|
||||
$prompt = $this->createValidPrompt('completion_prompt');
|
||||
$completionProviders = ['param' => EnumCompletionProvider::class];
|
||||
$promptReference = new PromptReference(
|
||||
$prompt,
|
||||
fn () => 'Completion content',
|
||||
false,
|
||||
$completionProviders
|
||||
);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('completion_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, ['param' => 'value'])
|
||||
->willReturn([
|
||||
'role' => 'user',
|
||||
'content' => 'Completion content',
|
||||
]);
|
||||
|
||||
$result = $this->promptGetter->get($request);
|
||||
|
||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
||||
$this->assertCount(1, $result->messages);
|
||||
}
|
||||
|
||||
public function testGetHandlesMixedMessageArray(): void
|
||||
{
|
||||
$request = new GetPromptRequest('mixed_prompt', []);
|
||||
$prompt = $this->createValidPrompt('mixed_prompt');
|
||||
$promptMessage = new PromptMessage(Role::Assistant, new TextContent('Direct message'));
|
||||
$promptReference = new PromptReference($prompt, fn () => 'Mixed content');
|
||||
$handlerResult = [
|
||||
$promptMessage,
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => 'Regular message',
|
||||
],
|
||||
];
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('mixed_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, [])
|
||||
->willReturn($handlerResult);
|
||||
|
||||
$result = $this->promptGetter->get($request);
|
||||
|
||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
||||
$this->assertCount(2, $result->messages);
|
||||
$this->assertSame($promptMessage, $result->messages[0]);
|
||||
$this->assertEquals(Role::User, $result->messages[1]->role);
|
||||
$this->assertEquals('Regular message', $result->messages[1]->content->text);
|
||||
}
|
||||
|
||||
public function testGetReflectsFormattedMessagesCorrectly(): void
|
||||
{
|
||||
$request = new GetPromptRequest('format_test_prompt', []);
|
||||
$prompt = $this->createValidPrompt('format_test_prompt');
|
||||
$promptReference = new PromptReference($prompt, fn () => 'Format test');
|
||||
|
||||
// Test that the formatted result from PromptReference.formatResult is properly returned
|
||||
$handlerResult = [
|
||||
'role' => 'user',
|
||||
'content' => 'Test formatting',
|
||||
];
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('format_test_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, [])
|
||||
->willReturn($handlerResult);
|
||||
|
||||
$result = $this->promptGetter->get($request);
|
||||
|
||||
$this->assertInstanceOf(GetPromptResult::class, $result);
|
||||
$this->assertCount(1, $result->messages);
|
||||
$this->assertEquals('Test formatting', $result->messages[0]->content->text);
|
||||
$this->assertEquals(Role::User, $result->messages[0]->role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that invalid handler results throw RuntimeException from PromptReference.formatResult().
|
||||
*/
|
||||
public function testGetThrowsRuntimeExceptionForInvalidHandlerResult(): void
|
||||
{
|
||||
$request = new GetPromptRequest('invalid_prompt', []);
|
||||
$prompt = $this->createValidPrompt('invalid_prompt');
|
||||
$promptReference = new PromptReference($prompt, fn () => 'Invalid content');
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('invalid_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, [])
|
||||
->willReturn('This is not a valid prompt format');
|
||||
|
||||
$this->expectException(PromptGetException::class);
|
||||
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
||||
|
||||
$this->promptGetter->get($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that null result from handler throws RuntimeException.
|
||||
*/
|
||||
public function testGetThrowsRuntimeExceptionForNullHandlerResult(): void
|
||||
{
|
||||
$request = new GetPromptRequest('null_prompt', []);
|
||||
$prompt = $this->createValidPrompt('null_prompt');
|
||||
$promptReference = new PromptReference($prompt, fn () => null);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('null_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, [])
|
||||
->willReturn(null);
|
||||
|
||||
$this->expectException(PromptGetException::class);
|
||||
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
||||
|
||||
$this->promptGetter->get($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that scalar result from handler throws RuntimeException.
|
||||
*/
|
||||
public function testGetThrowsRuntimeExceptionForScalarHandlerResult(): void
|
||||
{
|
||||
$request = new GetPromptRequest('scalar_prompt', []);
|
||||
$prompt = $this->createValidPrompt('scalar_prompt');
|
||||
$promptReference = new PromptReference($prompt, fn () => 42);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('scalar_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, [])
|
||||
->willReturn(42);
|
||||
|
||||
$this->expectException(PromptGetException::class);
|
||||
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
||||
|
||||
$this->promptGetter->get($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that boolean result from handler throws RuntimeException.
|
||||
*/
|
||||
public function testGetThrowsRuntimeExceptionForBooleanHandlerResult(): void
|
||||
{
|
||||
$request = new GetPromptRequest('boolean_prompt', []);
|
||||
$prompt = $this->createValidPrompt('boolean_prompt');
|
||||
$promptReference = new PromptReference($prompt, fn () => true);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('boolean_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, [])
|
||||
->willReturn(true);
|
||||
|
||||
$this->expectException(PromptGetException::class);
|
||||
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
||||
|
||||
$this->promptGetter->get($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that object result from handler throws RuntimeException.
|
||||
*/
|
||||
public function testGetThrowsRuntimeExceptionForObjectHandlerResult(): void
|
||||
{
|
||||
$request = new GetPromptRequest('object_prompt', []);
|
||||
$prompt = $this->createValidPrompt('object_prompt');
|
||||
$objectResult = new \stdClass();
|
||||
$objectResult->property = 'value';
|
||||
$promptReference = new PromptReference($prompt, fn () => $objectResult);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getPrompt')
|
||||
->with('object_prompt')
|
||||
->willReturn($promptReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($promptReference, [])
|
||||
->willReturn($objectResult);
|
||||
|
||||
$this->expectException(PromptGetException::class);
|
||||
$this->expectExceptionMessage('Prompt generator method must return an array of messages.');
|
||||
|
||||
$this->promptGetter->get($request);
|
||||
}
|
||||
|
||||
public function testConstructorWithDefaultLogger(): void
|
||||
{
|
||||
$promptGetter = new PromptGetter(
|
||||
$this->referenceProvider,
|
||||
$this->referenceHandler,
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(PromptGetter::class, $promptGetter);
|
||||
}
|
||||
|
||||
public function testConstructorWithCustomLogger(): void
|
||||
{
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$promptGetter = new PromptGetter(
|
||||
$this->referenceProvider,
|
||||
$this->referenceHandler,
|
||||
$logger,
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(PromptGetter::class, $promptGetter);
|
||||
}
|
||||
|
||||
private function createValidPrompt(string $name): Prompt
|
||||
{
|
||||
return new Prompt(
|
||||
name: $name,
|
||||
description: "Test prompt: {$name}",
|
||||
arguments: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
313
tests/Capability/Registry/RegistryProviderTest.php
Normal file
313
tests/Capability/Registry/RegistryProviderTest.php
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
<?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\Registry;
|
||||
use Mcp\Capability\Registry\PromptReference;
|
||||
use Mcp\Capability\Registry\ResourceReference;
|
||||
use Mcp\Capability\Registry\ResourceTemplateReference;
|
||||
use Mcp\Capability\Registry\ToolReference;
|
||||
use Mcp\Schema\Prompt;
|
||||
use Mcp\Schema\Resource;
|
||||
use Mcp\Schema\ResourceTemplate;
|
||||
use Mcp\Schema\Tool;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class RegistryProviderTest extends TestCase
|
||||
{
|
||||
private Registry $registry;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->registry = new Registry();
|
||||
}
|
||||
|
||||
public function testGetToolReturnsRegisteredTool(): void
|
||||
{
|
||||
$tool = $this->createValidTool('test_tool');
|
||||
$handler = fn () => 'result';
|
||||
|
||||
$this->registry->registerTool($tool, $handler);
|
||||
|
||||
$toolRef = $this->registry->getTool('test_tool');
|
||||
$this->assertInstanceOf(ToolReference::class, $toolRef);
|
||||
$this->assertEquals($tool->name, $toolRef->tool->name);
|
||||
$this->assertEquals($handler, $toolRef->handler);
|
||||
$this->assertFalse($toolRef->isManual);
|
||||
}
|
||||
|
||||
public function testGetToolReturnsNullForUnregisteredTool(): void
|
||||
{
|
||||
$toolRef = $this->registry->getTool('non_existent_tool');
|
||||
$this->assertNull($toolRef);
|
||||
}
|
||||
|
||||
public function testGetResourceReturnsRegisteredResource(): void
|
||||
{
|
||||
$resource = $this->createValidResource('test://resource');
|
||||
$handler = fn () => 'content';
|
||||
|
||||
$this->registry->registerResource($resource, $handler);
|
||||
|
||||
$resourceRef = $this->registry->getResource('test://resource');
|
||||
$this->assertInstanceOf(ResourceReference::class, $resourceRef);
|
||||
$this->assertEquals($resource->uri, $resourceRef->schema->uri);
|
||||
$this->assertEquals($handler, $resourceRef->handler);
|
||||
$this->assertFalse($resourceRef->isManual);
|
||||
}
|
||||
|
||||
public function testGetResourceReturnsNullForUnregisteredResource(): void
|
||||
{
|
||||
$resourceRef = $this->registry->getResource('test://non_existent');
|
||||
$this->assertNull($resourceRef);
|
||||
}
|
||||
|
||||
public function testGetResourceMatchesResourceTemplate(): void
|
||||
{
|
||||
$template = $this->createValidResourceTemplate('test://{id}');
|
||||
$handler = fn (string $id) => "content for {$id}";
|
||||
|
||||
$this->registry->registerResourceTemplate($template, $handler);
|
||||
|
||||
$resourceRef = $this->registry->getResource('test://123');
|
||||
$this->assertInstanceOf(ResourceTemplateReference::class, $resourceRef);
|
||||
$this->assertEquals($template->uriTemplate, $resourceRef->resourceTemplate->uriTemplate);
|
||||
$this->assertEquals($handler, $resourceRef->handler);
|
||||
}
|
||||
|
||||
public function testGetResourceWithIncludeTemplatesFalse(): void
|
||||
{
|
||||
$template = $this->createValidResourceTemplate('test://{id}');
|
||||
$handler = fn (string $id) => "content for {$id}";
|
||||
|
||||
$this->registry->registerResourceTemplate($template, $handler);
|
||||
|
||||
$resourceRef = $this->registry->getResource('test://123', false);
|
||||
$this->assertNull($resourceRef);
|
||||
}
|
||||
|
||||
public function testGetResourcePrefersDirectResourceOverTemplate(): void
|
||||
{
|
||||
$resource = $this->createValidResource('test://123');
|
||||
$resourceHandler = fn () => 'direct resource';
|
||||
|
||||
$template = $this->createValidResourceTemplate('test://{id}');
|
||||
$templateHandler = fn (string $id) => "template for {$id}";
|
||||
|
||||
$this->registry->registerResource($resource, $resourceHandler);
|
||||
$this->registry->registerResourceTemplate($template, $templateHandler);
|
||||
|
||||
$resourceRef = $this->registry->getResource('test://123');
|
||||
$this->assertInstanceOf(ResourceReference::class, $resourceRef);
|
||||
$this->assertEquals($resource->uri, $resourceRef->schema->uri);
|
||||
}
|
||||
|
||||
public function testGetResourceTemplateReturnsRegisteredTemplate(): void
|
||||
{
|
||||
$template = $this->createValidResourceTemplate('test://{id}');
|
||||
$handler = fn (string $id) => "content for {$id}";
|
||||
|
||||
$this->registry->registerResourceTemplate($template, $handler);
|
||||
|
||||
$templateRef = $this->registry->getResourceTemplate('test://{id}');
|
||||
$this->assertInstanceOf(ResourceTemplateReference::class, $templateRef);
|
||||
$this->assertEquals($template->uriTemplate, $templateRef->resourceTemplate->uriTemplate);
|
||||
$this->assertEquals($handler, $templateRef->handler);
|
||||
$this->assertFalse($templateRef->isManual);
|
||||
}
|
||||
|
||||
public function testGetResourceTemplateReturnsNullForUnregisteredTemplate(): void
|
||||
{
|
||||
$templateRef = $this->registry->getResourceTemplate('test://{non_existent}');
|
||||
$this->assertNull($templateRef);
|
||||
}
|
||||
|
||||
public function testGetPromptReturnsRegisteredPrompt(): void
|
||||
{
|
||||
$prompt = $this->createValidPrompt('test_prompt');
|
||||
$handler = fn () => ['role' => 'user', 'content' => 'test message'];
|
||||
|
||||
$this->registry->registerPrompt($prompt, $handler);
|
||||
|
||||
$promptRef = $this->registry->getPrompt('test_prompt');
|
||||
$this->assertInstanceOf(PromptReference::class, $promptRef);
|
||||
$this->assertEquals($prompt->name, $promptRef->prompt->name);
|
||||
$this->assertEquals($handler, $promptRef->handler);
|
||||
$this->assertFalse($promptRef->isManual);
|
||||
}
|
||||
|
||||
public function testGetPromptReturnsNullForUnregisteredPrompt(): void
|
||||
{
|
||||
$promptRef = $this->registry->getPrompt('non_existent_prompt');
|
||||
$this->assertNull($promptRef);
|
||||
}
|
||||
|
||||
public function testGetToolsReturnsAllRegisteredTools(): void
|
||||
{
|
||||
$tool1 = $this->createValidTool('tool1');
|
||||
$tool2 = $this->createValidTool('tool2');
|
||||
|
||||
$this->registry->registerTool($tool1, fn () => 'result1');
|
||||
$this->registry->registerTool($tool2, fn () => 'result2');
|
||||
|
||||
$tools = $this->registry->getTools();
|
||||
$this->assertCount(2, $tools);
|
||||
$this->assertArrayHasKey('tool1', $tools);
|
||||
$this->assertArrayHasKey('tool2', $tools);
|
||||
$this->assertInstanceOf(Tool::class, $tools['tool1']);
|
||||
$this->assertInstanceOf(Tool::class, $tools['tool2']);
|
||||
}
|
||||
|
||||
public function testGetResourcesReturnsAllRegisteredResources(): void
|
||||
{
|
||||
$resource1 = $this->createValidResource('test://resource1');
|
||||
$resource2 = $this->createValidResource('test://resource2');
|
||||
|
||||
$this->registry->registerResource($resource1, fn () => 'content1');
|
||||
$this->registry->registerResource($resource2, fn () => 'content2');
|
||||
|
||||
$resources = $this->registry->getResources();
|
||||
$this->assertCount(2, $resources);
|
||||
$this->assertArrayHasKey('test://resource1', $resources);
|
||||
$this->assertArrayHasKey('test://resource2', $resources);
|
||||
$this->assertInstanceOf(Resource::class, $resources['test://resource1']);
|
||||
$this->assertInstanceOf(Resource::class, $resources['test://resource2']);
|
||||
}
|
||||
|
||||
public function testGetPromptsReturnsAllRegisteredPrompts(): void
|
||||
{
|
||||
$prompt1 = $this->createValidPrompt('prompt1');
|
||||
$prompt2 = $this->createValidPrompt('prompt2');
|
||||
|
||||
$this->registry->registerPrompt($prompt1, fn () => []);
|
||||
$this->registry->registerPrompt($prompt2, fn () => []);
|
||||
|
||||
$prompts = $this->registry->getPrompts();
|
||||
$this->assertCount(2, $prompts);
|
||||
$this->assertArrayHasKey('prompt1', $prompts);
|
||||
$this->assertArrayHasKey('prompt2', $prompts);
|
||||
$this->assertInstanceOf(Prompt::class, $prompts['prompt1']);
|
||||
$this->assertInstanceOf(Prompt::class, $prompts['prompt2']);
|
||||
}
|
||||
|
||||
public function testGetResourceTemplatesReturnsAllRegisteredTemplates(): void
|
||||
{
|
||||
$template1 = $this->createValidResourceTemplate('test1://{id}');
|
||||
$template2 = $this->createValidResourceTemplate('test2://{category}');
|
||||
|
||||
$this->registry->registerResourceTemplate($template1, fn () => 'content1');
|
||||
$this->registry->registerResourceTemplate($template2, fn () => 'content2');
|
||||
|
||||
$templates = $this->registry->getResourceTemplates();
|
||||
$this->assertCount(2, $templates);
|
||||
$this->assertArrayHasKey('test1://{id}', $templates);
|
||||
$this->assertArrayHasKey('test2://{category}', $templates);
|
||||
$this->assertInstanceOf(ResourceTemplate::class, $templates['test1://{id}']);
|
||||
$this->assertInstanceOf(ResourceTemplate::class, $templates['test2://{category}']);
|
||||
}
|
||||
|
||||
public function testHasElementsReturnsFalseForEmptyRegistry(): void
|
||||
{
|
||||
$this->assertFalse($this->registry->hasElements());
|
||||
}
|
||||
|
||||
public function testHasElementsReturnsTrueWhenToolIsRegistered(): void
|
||||
{
|
||||
$tool = $this->createValidTool('test_tool');
|
||||
$this->registry->registerTool($tool, fn () => 'result');
|
||||
|
||||
$this->assertTrue($this->registry->hasElements());
|
||||
}
|
||||
|
||||
public function testHasElementsReturnsTrueWhenResourceIsRegistered(): void
|
||||
{
|
||||
$resource = $this->createValidResource('test://resource');
|
||||
$this->registry->registerResource($resource, fn () => 'content');
|
||||
|
||||
$this->assertTrue($this->registry->hasElements());
|
||||
}
|
||||
|
||||
public function testHasElementsReturnsTrueWhenPromptIsRegistered(): void
|
||||
{
|
||||
$prompt = $this->createValidPrompt('test_prompt');
|
||||
$this->registry->registerPrompt($prompt, fn () => []);
|
||||
|
||||
$this->assertTrue($this->registry->hasElements());
|
||||
}
|
||||
|
||||
public function testHasElementsReturnsTrueWhenResourceTemplateIsRegistered(): void
|
||||
{
|
||||
$template = $this->createValidResourceTemplate('test://{id}');
|
||||
$this->registry->registerResourceTemplate($template, fn () => 'content');
|
||||
|
||||
$this->assertTrue($this->registry->hasElements());
|
||||
}
|
||||
|
||||
public function testResourceTemplateMatchingPrefersMoreSpecificMatches(): void
|
||||
{
|
||||
$specificTemplate = $this->createValidResourceTemplate('test://users/{userId}/profile');
|
||||
$genericTemplate = $this->createValidResourceTemplate('test://users/{userId}');
|
||||
|
||||
$this->registry->registerResourceTemplate($genericTemplate, fn () => 'generic');
|
||||
$this->registry->registerResourceTemplate($specificTemplate, fn () => 'specific');
|
||||
|
||||
// Should match the more specific template first
|
||||
$resourceRef = $this->registry->getResource('test://users/123/profile');
|
||||
$this->assertInstanceOf(ResourceTemplateReference::class, $resourceRef);
|
||||
$this->assertEquals('test://users/{userId}/profile', $resourceRef->resourceTemplate->uriTemplate);
|
||||
}
|
||||
|
||||
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: []
|
||||
);
|
||||
}
|
||||
}
|
||||
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: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
522
tests/Capability/Resource/ResourceReaderTest.php
Normal file
522
tests/Capability/Resource/ResourceReaderTest.php
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
<?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,
|
||||
);
|
||||
}
|
||||
}
|
||||
628
tests/Capability/Tool/ToolCallerTest.php
Normal file
628
tests/Capability/Tool/ToolCallerTest.php
Normal file
|
|
@ -0,0 +1,628 @@
|
|||
<?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\Tool;
|
||||
|
||||
use Mcp\Capability\Registry\ReferenceHandlerInterface;
|
||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
||||
use Mcp\Capability\Registry\ToolReference;
|
||||
use Mcp\Capability\Tool\ToolCaller;
|
||||
use Mcp\Exception\ToolCallException;
|
||||
use Mcp\Exception\ToolNotFoundException;
|
||||
use Mcp\Schema\Content\TextContent;
|
||||
use Mcp\Schema\Request\CallToolRequest;
|
||||
use Mcp\Schema\Result\CallToolResult;
|
||||
use Mcp\Schema\Tool;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class ToolCallerTest extends TestCase
|
||||
{
|
||||
private ToolCaller $toolCaller;
|
||||
private ReferenceProviderInterface|MockObject $referenceProvider;
|
||||
private ReferenceHandlerInterface|MockObject $referenceHandler;
|
||||
private LoggerInterface|MockObject $logger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->referenceProvider = $this->createMock(ReferenceProviderInterface::class);
|
||||
$this->referenceHandler = $this->createMock(ReferenceHandlerInterface::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
|
||||
$this->toolCaller = new ToolCaller(
|
||||
$this->referenceProvider,
|
||||
$this->referenceHandler,
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
|
||||
public function testCallExecutesToolSuccessfully(): void
|
||||
{
|
||||
$request = new CallToolRequest('test_tool', ['param' => 'value']);
|
||||
$tool = $this->createValidTool('test_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => 'test result');
|
||||
$handlerResult = 'test result';
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('test_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, ['param' => 'value'])
|
||||
->willReturn($handlerResult);
|
||||
|
||||
$this->logger
|
||||
->expects($this->exactly(2))
|
||||
->method('debug')
|
||||
->with(
|
||||
$this->logicalOr(
|
||||
$this->equalTo('Executing tool'),
|
||||
$this->equalTo('Tool executed successfully')
|
||||
),
|
||||
$this->logicalOr(
|
||||
$this->equalTo(['name' => 'test_tool', 'arguments' => ['param' => 'value']]),
|
||||
$this->equalTo(['name' => 'test_tool', 'result_type' => 'string'])
|
||||
)
|
||||
);
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
$this->assertCount(1, $result->content);
|
||||
$this->assertInstanceOf(TextContent::class, $result->content[0]);
|
||||
$this->assertEquals('test result', $result->content[0]->text);
|
||||
$this->assertFalse($result->isError);
|
||||
}
|
||||
|
||||
public function testCallWithEmptyArguments(): void
|
||||
{
|
||||
$request = new CallToolRequest('test_tool', []);
|
||||
$tool = $this->createValidTool('test_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => 'result');
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('test_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willReturn('result');
|
||||
|
||||
$this->logger
|
||||
->expects($this->exactly(2))
|
||||
->method('debug');
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
}
|
||||
|
||||
public function testCallWithComplexArguments(): void
|
||||
{
|
||||
$arguments = [
|
||||
'string_param' => 'value',
|
||||
'int_param' => 42,
|
||||
'bool_param' => true,
|
||||
'array_param' => ['nested' => 'data'],
|
||||
'null_param' => null,
|
||||
];
|
||||
$request = new CallToolRequest('complex_tool', $arguments);
|
||||
$tool = $this->createValidTool('complex_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => ['processed' => true]);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('complex_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, $arguments)
|
||||
->willReturn(['processed' => true]);
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
$this->assertCount(1, $result->content);
|
||||
}
|
||||
|
||||
public function testCallThrowsToolNotFoundExceptionWhenToolNotFound(): void
|
||||
{
|
||||
$request = new CallToolRequest('nonexistent_tool', ['param' => 'value']);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('nonexistent_tool')
|
||||
->willReturn(null);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->never())
|
||||
->method('handle');
|
||||
|
||||
$this->logger
|
||||
->expects($this->once())
|
||||
->method('debug')
|
||||
->with('Executing tool', ['name' => 'nonexistent_tool', 'arguments' => ['param' => 'value']]);
|
||||
|
||||
$this->logger
|
||||
->expects($this->once())
|
||||
->method('warning')
|
||||
->with('Tool not found', ['name' => 'nonexistent_tool']);
|
||||
|
||||
$this->expectException(ToolNotFoundException::class);
|
||||
$this->expectExceptionMessage('Tool not found for call: "nonexistent_tool".');
|
||||
|
||||
$this->toolCaller->call($request);
|
||||
}
|
||||
|
||||
public function testCallThrowsToolExecutionExceptionWhenHandlerThrowsException(): void
|
||||
{
|
||||
$request = new CallToolRequest('failing_tool', ['param' => 'value']);
|
||||
$tool = $this->createValidTool('failing_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => throw new \RuntimeException('Handler failed'));
|
||||
$handlerException = new \RuntimeException('Handler failed');
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('failing_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, ['param' => 'value'])
|
||||
->willThrowException($handlerException);
|
||||
|
||||
$this->logger
|
||||
->expects($this->once())
|
||||
->method('debug')
|
||||
->with('Executing tool', ['name' => 'failing_tool', 'arguments' => ['param' => 'value']]);
|
||||
|
||||
$this->logger
|
||||
->expects($this->once())
|
||||
->method('error')
|
||||
->with(
|
||||
'Tool execution failed',
|
||||
$this->callback(function ($context) {
|
||||
return 'failing_tool' === $context['name']
|
||||
&& 'Handler failed' === $context['exception']
|
||||
&& isset($context['trace']);
|
||||
})
|
||||
);
|
||||
|
||||
$this->expectException(ToolCallException::class);
|
||||
$this->expectExceptionMessage('Tool call "failing_tool" failed with error: "Handler failed".');
|
||||
|
||||
$thrownException = null;
|
||||
try {
|
||||
$this->toolCaller->call($request);
|
||||
} catch (ToolCallException $e) {
|
||||
$thrownException = $e;
|
||||
throw $e;
|
||||
} finally {
|
||||
if ($thrownException) {
|
||||
$this->assertSame($request, $thrownException->request);
|
||||
$this->assertSame($handlerException, $thrownException->getPrevious());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testCallHandlesNullResult(): void
|
||||
{
|
||||
$request = new CallToolRequest('null_tool', []);
|
||||
$tool = $this->createValidTool('null_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => null);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('null_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willReturn(null);
|
||||
|
||||
$this->logger
|
||||
->expects($this->exactly(2))
|
||||
->method('debug');
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
$this->assertCount(1, $result->content);
|
||||
$this->assertInstanceOf(TextContent::class, $result->content[0]);
|
||||
$this->assertEquals('(null)', $result->content[0]->text);
|
||||
}
|
||||
|
||||
public function testCallHandlesBooleanResults(): void
|
||||
{
|
||||
$request = new CallToolRequest('bool_tool', []);
|
||||
$tool = $this->createValidTool('bool_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => true);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('bool_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willReturn(true);
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
$this->assertCount(1, $result->content);
|
||||
$this->assertInstanceOf(TextContent::class, $result->content[0]);
|
||||
$this->assertEquals('true', $result->content[0]->text);
|
||||
}
|
||||
|
||||
public function testCallHandlesArrayResults(): void
|
||||
{
|
||||
$request = new CallToolRequest('array_tool', []);
|
||||
$tool = $this->createValidTool('array_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => ['key' => 'value', 'number' => 42]);
|
||||
$arrayResult = ['key' => 'value', 'number' => 42];
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('array_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willReturn($arrayResult);
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
$this->assertCount(1, $result->content);
|
||||
$this->assertInstanceOf(TextContent::class, $result->content[0]);
|
||||
$this->assertJsonStringEqualsJsonString(
|
||||
json_encode($arrayResult, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE),
|
||||
$result->content[0]->text
|
||||
);
|
||||
}
|
||||
|
||||
public function testCallHandlesContentObjectResults(): void
|
||||
{
|
||||
$request = new CallToolRequest('content_tool', []);
|
||||
$tool = $this->createValidTool('content_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => new TextContent('Direct content'));
|
||||
$contentResult = new TextContent('Direct content');
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('content_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willReturn($contentResult);
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
$this->assertCount(1, $result->content);
|
||||
$this->assertSame($contentResult, $result->content[0]);
|
||||
}
|
||||
|
||||
public function testCallHandlesArrayOfContentResults(): void
|
||||
{
|
||||
$request = new CallToolRequest('content_array_tool', []);
|
||||
$tool = $this->createValidTool('content_array_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => [
|
||||
new TextContent('First content'),
|
||||
new TextContent('Second content'),
|
||||
]);
|
||||
$contentArray = [
|
||||
new TextContent('First content'),
|
||||
new TextContent('Second content'),
|
||||
];
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('content_array_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willReturn($contentArray);
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
$this->assertCount(2, $result->content);
|
||||
$this->assertSame($contentArray[0], $result->content[0]);
|
||||
$this->assertSame($contentArray[1], $result->content[1]);
|
||||
}
|
||||
|
||||
public function testCallWithDifferentExceptionTypes(): void
|
||||
{
|
||||
$request = new CallToolRequest('error_tool', []);
|
||||
$tool = $this->createValidTool('error_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => throw new \InvalidArgumentException('Invalid input'));
|
||||
$handlerException = new \InvalidArgumentException('Invalid input');
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('error_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willThrowException($handlerException);
|
||||
|
||||
$this->logger
|
||||
->expects($this->once())
|
||||
->method('error')
|
||||
->with(
|
||||
'Tool execution failed',
|
||||
$this->callback(function ($context) {
|
||||
return 'error_tool' === $context['name']
|
||||
&& 'Invalid input' === $context['exception']
|
||||
&& isset($context['trace']);
|
||||
})
|
||||
);
|
||||
|
||||
$this->expectException(ToolCallException::class);
|
||||
$this->expectExceptionMessage('Tool call "error_tool" failed with error: "Invalid input".');
|
||||
|
||||
$this->toolCaller->call($request);
|
||||
}
|
||||
|
||||
public function testCallLogsResultTypeCorrectlyForString(): void
|
||||
{
|
||||
$request = new CallToolRequest('string_tool', []);
|
||||
$tool = $this->createValidTool('string_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => 'string result');
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('string_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willReturn('string result');
|
||||
|
||||
$this->logger
|
||||
->expects($this->exactly(2))
|
||||
->method('debug');
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
}
|
||||
|
||||
public function testCallLogsResultTypeCorrectlyForInteger(): void
|
||||
{
|
||||
$request = new CallToolRequest('int_tool', []);
|
||||
$tool = $this->createValidTool('int_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => 42);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('int_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willReturn(42);
|
||||
|
||||
$this->logger
|
||||
->expects($this->exactly(2))
|
||||
->method('debug');
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
}
|
||||
|
||||
public function testCallLogsResultTypeCorrectlyForArray(): void
|
||||
{
|
||||
$request = new CallToolRequest('array_tool', []);
|
||||
$tool = $this->createValidTool('array_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => ['test']);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('array_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willReturn(['test']);
|
||||
|
||||
$this->logger
|
||||
->expects($this->exactly(2))
|
||||
->method('debug');
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
}
|
||||
|
||||
public function testConstructorWithDefaultLogger(): void
|
||||
{
|
||||
$executor = new ToolCaller($this->referenceProvider, $this->referenceHandler);
|
||||
|
||||
// Verify it's constructed without throwing exceptions
|
||||
$this->assertInstanceOf(ToolCaller::class, $executor);
|
||||
}
|
||||
|
||||
public function testCallHandlesEmptyArrayResult(): void
|
||||
{
|
||||
$request = new CallToolRequest('empty_array_tool', []);
|
||||
$tool = $this->createValidTool('empty_array_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => []);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('empty_array_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willReturn([]);
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
$this->assertCount(1, $result->content);
|
||||
$this->assertInstanceOf(TextContent::class, $result->content[0]);
|
||||
$this->assertEquals('[]', $result->content[0]->text);
|
||||
}
|
||||
|
||||
public function testCallHandlesMixedContentAndNonContentArray(): void
|
||||
{
|
||||
$request = new CallToolRequest('mixed_tool', []);
|
||||
$tool = $this->createValidTool('mixed_tool');
|
||||
$mixedResult = [
|
||||
new TextContent('First content'),
|
||||
'plain string',
|
||||
42,
|
||||
new TextContent('Second content'),
|
||||
];
|
||||
$toolReference = new ToolReference($tool, fn () => $mixedResult);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('mixed_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willReturn($mixedResult);
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
// The ToolReference.formatResult should handle this mixed array
|
||||
$this->assertGreaterThan(1, \count($result->content));
|
||||
}
|
||||
|
||||
public function testCallHandlesStdClassResult(): void
|
||||
{
|
||||
$request = new CallToolRequest('object_tool', []);
|
||||
$tool = $this->createValidTool('object_tool');
|
||||
$objectResult = new \stdClass();
|
||||
$objectResult->property = 'value';
|
||||
$toolReference = new ToolReference($tool, fn () => $objectResult);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('object_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willReturn($objectResult);
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
$this->assertCount(1, $result->content);
|
||||
$this->assertInstanceOf(TextContent::class, $result->content[0]);
|
||||
$this->assertStringContainsString('"property": "value"', $result->content[0]->text);
|
||||
}
|
||||
|
||||
public function testCallHandlesBooleanFalseResult(): void
|
||||
{
|
||||
$request = new CallToolRequest('false_tool', []);
|
||||
$tool = $this->createValidTool('false_tool');
|
||||
$toolReference = new ToolReference($tool, fn () => false);
|
||||
|
||||
$this->referenceProvider
|
||||
->expects($this->once())
|
||||
->method('getTool')
|
||||
->with('false_tool')
|
||||
->willReturn($toolReference);
|
||||
|
||||
$this->referenceHandler
|
||||
->expects($this->once())
|
||||
->method('handle')
|
||||
->with($toolReference, [])
|
||||
->willReturn(false);
|
||||
|
||||
$result = $this->toolCaller->call($request);
|
||||
|
||||
$this->assertInstanceOf(CallToolResult::class, $result);
|
||||
$this->assertCount(1, $result->content);
|
||||
$this->assertInstanceOf(TextContent::class, $result->content[0]);
|
||||
$this->assertEquals('false', $result->content[0]->text);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
406
tests/Schema/ServerCapabilitiesTest.php
Normal file
406
tests/Schema/ServerCapabilitiesTest.php
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
<?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\Schema;
|
||||
|
||||
use Mcp\Schema\ServerCapabilities;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ServerCapabilitiesTest extends TestCase
|
||||
{
|
||||
public function testConstructorWithDefaults(): void
|
||||
{
|
||||
$capabilities = new ServerCapabilities();
|
||||
|
||||
$this->assertTrue($capabilities->tools);
|
||||
$this->assertFalse($capabilities->toolsListChanged);
|
||||
$this->assertTrue($capabilities->resources);
|
||||
$this->assertFalse($capabilities->resourcesSubscribe);
|
||||
$this->assertFalse($capabilities->resourcesListChanged);
|
||||
$this->assertTrue($capabilities->prompts);
|
||||
$this->assertFalse($capabilities->promptsListChanged);
|
||||
$this->assertFalse($capabilities->logging);
|
||||
$this->assertFalse($capabilities->completions);
|
||||
$this->assertNull($capabilities->experimental);
|
||||
}
|
||||
|
||||
public function testConstructorWithAllParameters(): void
|
||||
{
|
||||
$experimental = ['feature1' => true, 'feature2' => 'enabled'];
|
||||
|
||||
$capabilities = new ServerCapabilities(
|
||||
tools: false,
|
||||
toolsListChanged: true,
|
||||
resources: false,
|
||||
resourcesSubscribe: true,
|
||||
resourcesListChanged: true,
|
||||
prompts: false,
|
||||
promptsListChanged: true,
|
||||
logging: true,
|
||||
completions: true,
|
||||
experimental: $experimental
|
||||
);
|
||||
|
||||
$this->assertFalse($capabilities->tools);
|
||||
$this->assertTrue($capabilities->toolsListChanged);
|
||||
$this->assertFalse($capabilities->resources);
|
||||
$this->assertTrue($capabilities->resourcesSubscribe);
|
||||
$this->assertTrue($capabilities->resourcesListChanged);
|
||||
$this->assertFalse($capabilities->prompts);
|
||||
$this->assertTrue($capabilities->promptsListChanged);
|
||||
$this->assertTrue($capabilities->logging);
|
||||
$this->assertTrue($capabilities->completions);
|
||||
$this->assertEquals($experimental, $capabilities->experimental);
|
||||
}
|
||||
|
||||
public function testConstructorWithNullValues(): void
|
||||
{
|
||||
$capabilities = new ServerCapabilities(
|
||||
tools: null,
|
||||
toolsListChanged: null,
|
||||
resources: null,
|
||||
resourcesSubscribe: null,
|
||||
resourcesListChanged: null,
|
||||
prompts: null,
|
||||
promptsListChanged: null,
|
||||
logging: null,
|
||||
completions: null,
|
||||
experimental: null
|
||||
);
|
||||
|
||||
$this->assertNull($capabilities->tools);
|
||||
$this->assertNull($capabilities->toolsListChanged);
|
||||
$this->assertNull($capabilities->resources);
|
||||
$this->assertNull($capabilities->resourcesSubscribe);
|
||||
$this->assertNull($capabilities->resourcesListChanged);
|
||||
$this->assertNull($capabilities->prompts);
|
||||
$this->assertNull($capabilities->promptsListChanged);
|
||||
$this->assertNull($capabilities->logging);
|
||||
$this->assertNull($capabilities->completions);
|
||||
$this->assertNull($capabilities->experimental);
|
||||
}
|
||||
|
||||
public function testFromArrayWithEmptyArray(): void
|
||||
{
|
||||
$capabilities = ServerCapabilities::fromArray([]);
|
||||
|
||||
$this->assertFalse($capabilities->logging);
|
||||
$this->assertFalse($capabilities->completions);
|
||||
$this->assertFalse($capabilities->tools);
|
||||
$this->assertFalse($capabilities->prompts);
|
||||
$this->assertFalse($capabilities->resources);
|
||||
$this->assertNull($capabilities->toolsListChanged);
|
||||
$this->assertNull($capabilities->promptsListChanged);
|
||||
$this->assertNull($capabilities->resourcesSubscribe);
|
||||
$this->assertNull($capabilities->resourcesListChanged);
|
||||
$this->assertNull($capabilities->experimental);
|
||||
}
|
||||
|
||||
public function testFromArrayWithBasicCapabilities(): void
|
||||
{
|
||||
$data = [
|
||||
'tools' => new \stdClass(),
|
||||
'resources' => new \stdClass(),
|
||||
'prompts' => new \stdClass(),
|
||||
'logging' => new \stdClass(),
|
||||
'completions' => new \stdClass(),
|
||||
];
|
||||
|
||||
$capabilities = ServerCapabilities::fromArray($data);
|
||||
|
||||
$this->assertTrue($capabilities->tools);
|
||||
$this->assertTrue($capabilities->resources);
|
||||
$this->assertTrue($capabilities->prompts);
|
||||
$this->assertTrue($capabilities->logging);
|
||||
$this->assertTrue($capabilities->completions);
|
||||
$this->assertNull($capabilities->toolsListChanged);
|
||||
$this->assertNull($capabilities->promptsListChanged);
|
||||
$this->assertNull($capabilities->resourcesSubscribe);
|
||||
$this->assertNull($capabilities->resourcesListChanged);
|
||||
}
|
||||
|
||||
public function testFromArrayWithPromptsArrayListChanged(): void
|
||||
{
|
||||
$data = [
|
||||
'prompts' => ['listChanged' => true],
|
||||
];
|
||||
|
||||
$capabilities = ServerCapabilities::fromArray($data);
|
||||
|
||||
$this->assertTrue($capabilities->prompts);
|
||||
$this->assertTrue($capabilities->promptsListChanged);
|
||||
}
|
||||
|
||||
public function testFromArrayWithPromptsObjectListChanged(): void
|
||||
{
|
||||
$prompts = new \stdClass();
|
||||
$prompts->listChanged = true;
|
||||
|
||||
$data = [
|
||||
'prompts' => $prompts,
|
||||
];
|
||||
|
||||
$capabilities = ServerCapabilities::fromArray($data);
|
||||
|
||||
$this->assertTrue($capabilities->prompts);
|
||||
$this->assertTrue($capabilities->promptsListChanged);
|
||||
}
|
||||
|
||||
public function testFromArrayWithResourcesArraySubscribeAndListChanged(): void
|
||||
{
|
||||
$data = [
|
||||
'resources' => [
|
||||
'subscribe' => true,
|
||||
'listChanged' => false,
|
||||
],
|
||||
];
|
||||
|
||||
$capabilities = ServerCapabilities::fromArray($data);
|
||||
|
||||
$this->assertTrue($capabilities->resources);
|
||||
$this->assertTrue($capabilities->resourcesSubscribe);
|
||||
$this->assertFalse($capabilities->resourcesListChanged);
|
||||
}
|
||||
|
||||
public function testFromArrayWithResourcesObjectSubscribeAndListChanged(): void
|
||||
{
|
||||
$resources = new \stdClass();
|
||||
$resources->subscribe = false;
|
||||
$resources->listChanged = true;
|
||||
|
||||
$data = [
|
||||
'resources' => $resources,
|
||||
];
|
||||
|
||||
$capabilities = ServerCapabilities::fromArray($data);
|
||||
|
||||
$this->assertTrue($capabilities->resources);
|
||||
$this->assertFalse($capabilities->resourcesSubscribe);
|
||||
$this->assertTrue($capabilities->resourcesListChanged);
|
||||
}
|
||||
|
||||
public function testFromArrayWithToolsArrayListChanged(): void
|
||||
{
|
||||
$data = [
|
||||
'tools' => ['listChanged' => false],
|
||||
];
|
||||
|
||||
$capabilities = ServerCapabilities::fromArray($data);
|
||||
|
||||
$this->assertTrue($capabilities->tools);
|
||||
$this->assertFalse($capabilities->toolsListChanged);
|
||||
}
|
||||
|
||||
public function testFromArrayWithToolsObjectListChanged(): void
|
||||
{
|
||||
$tools = new \stdClass();
|
||||
$tools->listChanged = true;
|
||||
|
||||
$data = [
|
||||
'tools' => $tools,
|
||||
];
|
||||
|
||||
$capabilities = ServerCapabilities::fromArray($data);
|
||||
|
||||
$this->assertTrue($capabilities->tools);
|
||||
$this->assertTrue($capabilities->toolsListChanged);
|
||||
}
|
||||
|
||||
public function testFromArrayWithExperimental(): void
|
||||
{
|
||||
$experimental = ['feature1' => true, 'feature2' => 'test'];
|
||||
$data = [
|
||||
'experimental' => $experimental,
|
||||
];
|
||||
|
||||
$capabilities = ServerCapabilities::fromArray($data);
|
||||
|
||||
$this->assertEquals($experimental, $capabilities->experimental);
|
||||
}
|
||||
|
||||
public function testFromArrayWithComplexData(): void
|
||||
{
|
||||
$data = [
|
||||
'tools' => ['listChanged' => true],
|
||||
'resources' => [
|
||||
'subscribe' => true,
|
||||
'listChanged' => false,
|
||||
],
|
||||
'prompts' => ['listChanged' => true],
|
||||
'logging' => new \stdClass(),
|
||||
'completions' => new \stdClass(),
|
||||
'experimental' => ['customFeature' => 'enabled'],
|
||||
];
|
||||
|
||||
$capabilities = ServerCapabilities::fromArray($data);
|
||||
|
||||
$this->assertTrue($capabilities->tools);
|
||||
$this->assertTrue($capabilities->toolsListChanged);
|
||||
$this->assertTrue($capabilities->resources);
|
||||
$this->assertTrue($capabilities->resourcesSubscribe);
|
||||
$this->assertFalse($capabilities->resourcesListChanged);
|
||||
$this->assertTrue($capabilities->prompts);
|
||||
$this->assertTrue($capabilities->promptsListChanged);
|
||||
$this->assertTrue($capabilities->logging);
|
||||
$this->assertTrue($capabilities->completions);
|
||||
$this->assertEquals(['customFeature' => 'enabled'], $capabilities->experimental);
|
||||
}
|
||||
|
||||
public function testJsonSerializeWithDefaults(): void
|
||||
{
|
||||
$capabilities = new ServerCapabilities();
|
||||
$json = $capabilities->jsonSerialize();
|
||||
|
||||
$expected = [
|
||||
'tools' => new \stdClass(),
|
||||
'resources' => new \stdClass(),
|
||||
'prompts' => new \stdClass(),
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $json);
|
||||
}
|
||||
|
||||
public function testJsonSerializeWithAllFeaturesEnabled(): void
|
||||
{
|
||||
$experimental = ['feature1' => true];
|
||||
$capabilities = new ServerCapabilities(
|
||||
tools: true,
|
||||
toolsListChanged: true,
|
||||
resources: true,
|
||||
resourcesSubscribe: true,
|
||||
resourcesListChanged: true,
|
||||
prompts: true,
|
||||
promptsListChanged: true,
|
||||
logging: true,
|
||||
completions: true,
|
||||
experimental: $experimental
|
||||
);
|
||||
|
||||
$json = $capabilities->jsonSerialize();
|
||||
|
||||
$this->assertArrayHasKey('logging', $json);
|
||||
$this->assertEquals(new \stdClass(), $json['logging']);
|
||||
|
||||
$this->assertArrayHasKey('completions', $json);
|
||||
$this->assertEquals(new \stdClass(), $json['completions']);
|
||||
|
||||
$this->assertArrayHasKey('prompts', $json);
|
||||
$this->assertTrue($json['prompts']->listChanged);
|
||||
|
||||
$this->assertArrayHasKey('resources', $json);
|
||||
$this->assertTrue($json['resources']->subscribe);
|
||||
$this->assertTrue($json['resources']->listChanged);
|
||||
|
||||
$this->assertArrayHasKey('tools', $json);
|
||||
$this->assertTrue($json['tools']->listChanged);
|
||||
|
||||
$this->assertArrayHasKey('experimental', $json);
|
||||
$this->assertEquals((object) $experimental, $json['experimental']);
|
||||
}
|
||||
|
||||
public function testJsonSerializeWithFalseValues(): void
|
||||
{
|
||||
$capabilities = new ServerCapabilities(
|
||||
tools: false,
|
||||
resources: false,
|
||||
prompts: false,
|
||||
logging: false,
|
||||
completions: false
|
||||
);
|
||||
|
||||
$json = $capabilities->jsonSerialize();
|
||||
|
||||
$this->assertEquals([], $json);
|
||||
}
|
||||
|
||||
public function testJsonSerializeWithMixedValues(): void
|
||||
{
|
||||
$capabilities = new ServerCapabilities(
|
||||
tools: true,
|
||||
toolsListChanged: false,
|
||||
resources: false,
|
||||
resourcesSubscribe: true,
|
||||
resourcesListChanged: true,
|
||||
prompts: true,
|
||||
promptsListChanged: false,
|
||||
logging: false,
|
||||
completions: true
|
||||
);
|
||||
|
||||
$json = $capabilities->jsonSerialize();
|
||||
|
||||
$expected = [
|
||||
'completions' => new \stdClass(),
|
||||
'prompts' => new \stdClass(),
|
||||
'resources' => (object) [
|
||||
'subscribe' => true,
|
||||
'listChanged' => true,
|
||||
],
|
||||
'tools' => new \stdClass(),
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $json);
|
||||
}
|
||||
|
||||
public function testJsonSerializeWithOnlyListChangedFlags(): void
|
||||
{
|
||||
$capabilities = new ServerCapabilities(
|
||||
tools: false,
|
||||
toolsListChanged: true,
|
||||
resources: false,
|
||||
resourcesListChanged: true,
|
||||
prompts: false,
|
||||
promptsListChanged: true
|
||||
);
|
||||
|
||||
$json = $capabilities->jsonSerialize();
|
||||
|
||||
$expected = [
|
||||
'prompts' => (object) ['listChanged' => true],
|
||||
'resources' => (object) ['listChanged' => true],
|
||||
'tools' => (object) ['listChanged' => true],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $json);
|
||||
}
|
||||
|
||||
public function testJsonSerializeWithNullExperimental(): void
|
||||
{
|
||||
$capabilities = new ServerCapabilities(
|
||||
tools: true,
|
||||
experimental: null
|
||||
);
|
||||
|
||||
$json = $capabilities->jsonSerialize();
|
||||
|
||||
$this->assertArrayNotHasKey('experimental', $json);
|
||||
$this->assertArrayHasKey('tools', $json);
|
||||
}
|
||||
|
||||
public function testFromArrayHandlesEdgeCasesGracefully(): void
|
||||
{
|
||||
$data = [
|
||||
'prompts' => [],
|
||||
'resources' => [],
|
||||
'tools' => [],
|
||||
];
|
||||
|
||||
$capabilities = ServerCapabilities::fromArray($data);
|
||||
|
||||
$this->assertTrue($capabilities->prompts);
|
||||
$this->assertNull($capabilities->promptsListChanged);
|
||||
$this->assertTrue($capabilities->resources);
|
||||
$this->assertNull($capabilities->resourcesSubscribe);
|
||||
$this->assertNull($capabilities->resourcesListChanged);
|
||||
$this->assertTrue($capabilities->tools);
|
||||
$this->assertNull($capabilities->toolsListChanged);
|
||||
}
|
||||
}
|
||||
294
tests/Server/RequestHandler/CallToolHandlerTest.php
Normal file
294
tests/Server/RequestHandler/CallToolHandlerTest.php
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
<?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\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Tool\ToolCallerInterface;
|
||||
use Mcp\Exception\ToolCallException;
|
||||
use Mcp\Exception\ToolNotFoundException;
|
||||
use Mcp\Schema\Content\TextContent;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\CallToolRequest;
|
||||
use Mcp\Schema\Result\CallToolResult;
|
||||
use Mcp\Server\RequestHandler\CallToolHandler;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class CallToolHandlerTest extends TestCase
|
||||
{
|
||||
private CallToolHandler $handler;
|
||||
private ToolCallerInterface|MockObject $toolExecutor;
|
||||
private LoggerInterface|MockObject $logger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->toolExecutor = $this->createMock(ToolCallerInterface::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
|
||||
$this->handler = new CallToolHandler(
|
||||
$this->toolExecutor,
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
|
||||
public function testSupportsCallToolRequest(): void
|
||||
{
|
||||
$request = $this->createCallToolRequest('test_tool', ['param' => 'value']);
|
||||
|
||||
$this->assertTrue($this->handler->supports($request));
|
||||
}
|
||||
|
||||
public function testHandleSuccessfulToolCall(): void
|
||||
{
|
||||
$request = $this->createCallToolRequest('greet_user', ['name' => 'John']);
|
||||
$expectedResult = new CallToolResult([new TextContent('Hello, John!')]);
|
||||
|
||||
$this->toolExecutor
|
||||
->expects($this->once())
|
||||
->method('call')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$this->logger
|
||||
->expects($this->never())
|
||||
->method('error');
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertEquals($request->getId(), $response->id);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandleToolCallWithEmptyArguments(): void
|
||||
{
|
||||
$request = $this->createCallToolRequest('simple_tool', []);
|
||||
$expectedResult = new CallToolResult([new TextContent('Simple result')]);
|
||||
|
||||
$this->toolExecutor
|
||||
->expects($this->once())
|
||||
->method('call')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandleToolCallWithComplexArguments(): void
|
||||
{
|
||||
$arguments = [
|
||||
'string_param' => 'value',
|
||||
'int_param' => 42,
|
||||
'bool_param' => true,
|
||||
'array_param' => ['nested' => 'data'],
|
||||
'null_param' => null,
|
||||
];
|
||||
$request = $this->createCallToolRequest('complex_tool', $arguments);
|
||||
$expectedResult = new CallToolResult([new TextContent('Complex result')]);
|
||||
|
||||
$this->toolExecutor
|
||||
->expects($this->once())
|
||||
->method('call')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandleToolNotFoundExceptionReturnsError(): void
|
||||
{
|
||||
$request = $this->createCallToolRequest('nonexistent_tool', ['param' => 'value']);
|
||||
$exception = new ToolNotFoundException($request);
|
||||
|
||||
$this->toolExecutor
|
||||
->expects($this->once())
|
||||
->method('call')
|
||||
->with($request)
|
||||
->willThrowException($exception);
|
||||
|
||||
$this->logger
|
||||
->expects($this->once())
|
||||
->method('error')
|
||||
->with(
|
||||
'Error while executing tool "nonexistent_tool": "Tool not found for call: "nonexistent_tool".".',
|
||||
[
|
||||
'tool' => 'nonexistent_tool',
|
||||
'arguments' => ['param' => 'value'],
|
||||
],
|
||||
);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Error::class, $response);
|
||||
$this->assertEquals($request->getId(), $response->id);
|
||||
$this->assertEquals(Error::INTERNAL_ERROR, $response->code);
|
||||
$this->assertEquals('Error while executing tool', $response->message);
|
||||
}
|
||||
|
||||
public function testHandleToolExecutionExceptionReturnsError(): void
|
||||
{
|
||||
$request = $this->createCallToolRequest('failing_tool', ['param' => 'value']);
|
||||
$exception = new ToolCallException($request, new \RuntimeException('Tool execution failed'));
|
||||
|
||||
$this->toolExecutor
|
||||
->expects($this->once())
|
||||
->method('call')
|
||||
->with($request)
|
||||
->willThrowException($exception);
|
||||
|
||||
$this->logger
|
||||
->expects($this->once())
|
||||
->method('error')
|
||||
->with(
|
||||
'Error while executing tool "failing_tool": "Tool call "failing_tool" failed with error: "Tool execution failed".".',
|
||||
[
|
||||
'tool' => 'failing_tool',
|
||||
'arguments' => ['param' => 'value'],
|
||||
],
|
||||
);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Error::class, $response);
|
||||
$this->assertEquals($request->getId(), $response->id);
|
||||
$this->assertEquals(Error::INTERNAL_ERROR, $response->code);
|
||||
$this->assertEquals('Error while executing tool', $response->message);
|
||||
}
|
||||
|
||||
public function testHandleWithNullResult(): void
|
||||
{
|
||||
$request = $this->createCallToolRequest('null_tool', []);
|
||||
$expectedResult = new CallToolResult([]);
|
||||
|
||||
$this->toolExecutor
|
||||
->expects($this->once())
|
||||
->method('call')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandleWithErrorResult(): void
|
||||
{
|
||||
$request = $this->createCallToolRequest('error_tool', []);
|
||||
$expectedResult = CallToolResult::error([new TextContent('Tool error occurred')]);
|
||||
|
||||
$this->toolExecutor
|
||||
->expects($this->once())
|
||||
->method('call')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
$this->assertTrue($response->result->isError);
|
||||
}
|
||||
|
||||
public function testConstructorWithDefaultLogger(): void
|
||||
{
|
||||
$handler = new CallToolHandler($this->toolExecutor);
|
||||
|
||||
$this->assertInstanceOf(CallToolHandler::class, $handler);
|
||||
}
|
||||
|
||||
public function testHandleLogsErrorWithCorrectParameters(): void
|
||||
{
|
||||
$request = $this->createCallToolRequest('test_tool', ['key1' => 'value1', 'key2' => 42]);
|
||||
$exception = new ToolCallException($request, new \RuntimeException('Custom error message'));
|
||||
|
||||
$this->toolExecutor
|
||||
->expects($this->once())
|
||||
->method('call')
|
||||
->willThrowException($exception);
|
||||
|
||||
$this->logger
|
||||
->expects($this->once())
|
||||
->method('error')
|
||||
->with(
|
||||
'Error while executing tool "test_tool": "Tool call "test_tool" failed with error: "Custom error message".".',
|
||||
[
|
||||
'tool' => 'test_tool',
|
||||
'arguments' => ['key1' => 'value1', 'key2' => 42],
|
||||
],
|
||||
);
|
||||
|
||||
$this->handler->handle($request);
|
||||
}
|
||||
|
||||
public function testHandleWithSpecialCharactersInToolName(): void
|
||||
{
|
||||
$request = $this->createCallToolRequest('tool-with_special.chars', []);
|
||||
$expectedResult = new CallToolResult([new TextContent('Special tool result')]);
|
||||
|
||||
$this->toolExecutor
|
||||
->expects($this->once())
|
||||
->method('call')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandleWithSpecialCharactersInArguments(): void
|
||||
{
|
||||
$arguments = [
|
||||
'special_chars' => 'äöü ñ 中文 🚀',
|
||||
'unicode' => '\\u{1F600}',
|
||||
'quotes' => 'text with "quotes" and \'single quotes\'',
|
||||
];
|
||||
$request = $this->createCallToolRequest('unicode_tool', $arguments);
|
||||
$expectedResult = new CallToolResult([new TextContent('Unicode handled')]);
|
||||
|
||||
$this->toolExecutor
|
||||
->expects($this->once())
|
||||
->method('call')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $arguments
|
||||
*/
|
||||
private function createCallToolRequest(string $name, array $arguments): CallToolRequest
|
||||
{
|
||||
return CallToolRequest::fromArray([
|
||||
'jsonrpc' => '2.0',
|
||||
'method' => CallToolRequest::getMethod(),
|
||||
'id' => 'test-request-'.uniqid(),
|
||||
'params' => [
|
||||
'name' => $name,
|
||||
'arguments' => $arguments,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
342
tests/Server/RequestHandler/GetPromptHandlerTest.php
Normal file
342
tests/Server/RequestHandler/GetPromptHandlerTest.php
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
<?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\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Prompt\PromptGetterInterface;
|
||||
use Mcp\Exception\PromptGetException;
|
||||
use Mcp\Exception\PromptNotFoundException;
|
||||
use Mcp\Schema\Content\PromptMessage;
|
||||
use Mcp\Schema\Content\TextContent;
|
||||
use Mcp\Schema\Enum\Role;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\GetPromptRequest;
|
||||
use Mcp\Schema\Result\GetPromptResult;
|
||||
use Mcp\Server\RequestHandler\GetPromptHandler;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class GetPromptHandlerTest extends TestCase
|
||||
{
|
||||
private GetPromptHandler $handler;
|
||||
private PromptGetterInterface|MockObject $promptGetter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->promptGetter = $this->createMock(PromptGetterInterface::class);
|
||||
|
||||
$this->handler = new GetPromptHandler($this->promptGetter);
|
||||
}
|
||||
|
||||
public function testSupportsGetPromptRequest(): void
|
||||
{
|
||||
$request = $this->createGetPromptRequest('test_prompt');
|
||||
|
||||
$this->assertTrue($this->handler->supports($request));
|
||||
}
|
||||
|
||||
public function testHandleSuccessfulPromptGet(): void
|
||||
{
|
||||
$request = $this->createGetPromptRequest('greeting_prompt');
|
||||
$expectedMessages = [
|
||||
new PromptMessage(Role::User, new TextContent('Hello, how can I help you?')),
|
||||
];
|
||||
$expectedResult = new GetPromptResult(
|
||||
description: 'A greeting prompt',
|
||||
messages: $expectedMessages,
|
||||
);
|
||||
|
||||
$this->promptGetter
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertEquals($request->getId(), $response->id);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandlePromptGetWithArguments(): void
|
||||
{
|
||||
$arguments = [
|
||||
'name' => 'John',
|
||||
'context' => 'business meeting',
|
||||
'formality' => 'formal',
|
||||
];
|
||||
$request = $this->createGetPromptRequest('personalized_prompt', $arguments);
|
||||
$expectedMessages = [
|
||||
new PromptMessage(
|
||||
Role::User,
|
||||
new TextContent('Good morning, John. How may I assist you in your business meeting?'),
|
||||
),
|
||||
];
|
||||
$expectedResult = new GetPromptResult(
|
||||
description: 'A personalized greeting prompt',
|
||||
messages: $expectedMessages,
|
||||
);
|
||||
|
||||
$this->promptGetter
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandlePromptGetWithNullArguments(): void
|
||||
{
|
||||
$request = $this->createGetPromptRequest('simple_prompt', null);
|
||||
$expectedMessages = [
|
||||
new PromptMessage(Role::Assistant, new TextContent('I am ready to help.')),
|
||||
];
|
||||
$expectedResult = new GetPromptResult(
|
||||
description: 'A simple prompt',
|
||||
messages: $expectedMessages,
|
||||
);
|
||||
|
||||
$this->promptGetter
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandlePromptGetWithEmptyArguments(): void
|
||||
{
|
||||
$request = $this->createGetPromptRequest('empty_args_prompt', []);
|
||||
$expectedMessages = [
|
||||
new PromptMessage(Role::User, new TextContent('Default message')),
|
||||
];
|
||||
$expectedResult = new GetPromptResult(
|
||||
description: 'A prompt with empty arguments',
|
||||
messages: $expectedMessages,
|
||||
);
|
||||
|
||||
$this->promptGetter
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandlePromptGetWithMultipleMessages(): void
|
||||
{
|
||||
$request = $this->createGetPromptRequest('conversation_prompt');
|
||||
$expectedMessages = [
|
||||
new PromptMessage(Role::User, new TextContent('Hello')),
|
||||
new PromptMessage(Role::Assistant, new TextContent('Hi there! How can I help you today?')),
|
||||
new PromptMessage(Role::User, new TextContent('I need assistance with my project')),
|
||||
];
|
||||
$expectedResult = new GetPromptResult(
|
||||
description: 'A conversation prompt',
|
||||
messages: $expectedMessages,
|
||||
);
|
||||
|
||||
$this->promptGetter
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
$this->assertCount(3, $response->result->messages);
|
||||
}
|
||||
|
||||
public function testHandlePromptNotFoundExceptionReturnsError(): void
|
||||
{
|
||||
$request = $this->createGetPromptRequest('nonexistent_prompt');
|
||||
$exception = new PromptNotFoundException($request);
|
||||
|
||||
$this->promptGetter
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with($request)
|
||||
->willThrowException($exception);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Error::class, $response);
|
||||
$this->assertEquals($request->getId(), $response->id);
|
||||
$this->assertEquals(Error::INTERNAL_ERROR, $response->code);
|
||||
$this->assertEquals('Error while handling prompt', $response->message);
|
||||
}
|
||||
|
||||
public function testHandlePromptGetExceptionReturnsError(): void
|
||||
{
|
||||
$request = $this->createGetPromptRequest('failing_prompt');
|
||||
$exception = new PromptGetException($request, new \RuntimeException('Failed to get prompt'));
|
||||
|
||||
$this->promptGetter
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with($request)
|
||||
->willThrowException($exception);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Error::class, $response);
|
||||
$this->assertEquals($request->getId(), $response->id);
|
||||
$this->assertEquals(Error::INTERNAL_ERROR, $response->code);
|
||||
$this->assertEquals('Error while handling prompt', $response->message);
|
||||
}
|
||||
|
||||
public function testHandlePromptGetWithComplexArguments(): void
|
||||
{
|
||||
$arguments = [
|
||||
'user_data' => [
|
||||
'name' => 'Alice',
|
||||
'preferences' => ['formal', 'concise'],
|
||||
'history' => [
|
||||
'last_interaction' => '2025-01-15',
|
||||
'topics' => ['technology', 'business'],
|
||||
],
|
||||
],
|
||||
'context' => 'technical consultation',
|
||||
'metadata' => [
|
||||
'session_id' => 'sess_123456',
|
||||
'timestamp' => 1705392000,
|
||||
],
|
||||
];
|
||||
$request = $this->createGetPromptRequest('complex_prompt', $arguments);
|
||||
$expectedMessages = [
|
||||
new PromptMessage(Role::User, new TextContent('Complex prompt generated with all parameters')),
|
||||
];
|
||||
$expectedResult = new GetPromptResult(
|
||||
description: 'A complex prompt with nested arguments',
|
||||
messages: $expectedMessages,
|
||||
);
|
||||
|
||||
$this->promptGetter
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandlePromptGetWithSpecialCharacters(): void
|
||||
{
|
||||
$arguments = [
|
||||
'message' => 'Hello 世界! How are you? 😊',
|
||||
'special' => 'äöü ñ ß',
|
||||
'quotes' => 'Text with "double" and \'single\' quotes',
|
||||
];
|
||||
$request = $this->createGetPromptRequest('unicode_prompt', $arguments);
|
||||
$expectedMessages = [
|
||||
new PromptMessage(Role::User, new TextContent('Unicode message processed')),
|
||||
];
|
||||
$expectedResult = new GetPromptResult(
|
||||
description: 'A prompt handling unicode characters',
|
||||
messages: $expectedMessages,
|
||||
);
|
||||
|
||||
$this->promptGetter
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandlePromptGetReturnsEmptyMessages(): void
|
||||
{
|
||||
$request = $this->createGetPromptRequest('empty_prompt');
|
||||
$expectedResult = new GetPromptResult(
|
||||
description: 'An empty prompt',
|
||||
messages: [],
|
||||
);
|
||||
|
||||
$this->promptGetter
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
$this->assertCount(0, $response->result->messages);
|
||||
}
|
||||
|
||||
public function testHandlePromptGetWithLargeNumberOfArguments(): void
|
||||
{
|
||||
$arguments = [];
|
||||
for ($i = 0; $i < 100; ++$i) {
|
||||
$arguments["arg_{$i}"] = "value_{$i}";
|
||||
}
|
||||
|
||||
$request = $this->createGetPromptRequest('many_args_prompt', $arguments);
|
||||
$expectedMessages = [
|
||||
new PromptMessage(Role::User, new TextContent('Processed 100 arguments')),
|
||||
];
|
||||
$expectedResult = new GetPromptResult(
|
||||
description: 'A prompt with many arguments',
|
||||
messages: $expectedMessages,
|
||||
);
|
||||
|
||||
$this->promptGetter
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $arguments
|
||||
*/
|
||||
private function createGetPromptRequest(string $name, ?array $arguments = null): GetPromptRequest
|
||||
{
|
||||
return GetPromptRequest::fromArray([
|
||||
'jsonrpc' => '2.0',
|
||||
'method' => GetPromptRequest::getMethod(),
|
||||
'id' => 'test-request-'.uniqid(),
|
||||
'params' => [
|
||||
'name' => $name,
|
||||
'arguments' => $arguments,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
147
tests/Server/RequestHandler/PingHandlerTest.php
Normal file
147
tests/Server/RequestHandler/PingHandlerTest.php
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
<?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\Server\RequestHandler;
|
||||
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\PingRequest;
|
||||
use Mcp\Schema\Result\EmptyResult;
|
||||
use Mcp\Server\RequestHandler\PingHandler;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class PingHandlerTest extends TestCase
|
||||
{
|
||||
private PingHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->handler = new PingHandler();
|
||||
}
|
||||
|
||||
public function testSupportsPingRequest(): void
|
||||
{
|
||||
$request = $this->createPingRequest();
|
||||
|
||||
$this->assertTrue($this->handler->supports($request));
|
||||
}
|
||||
|
||||
public function testHandlePingRequest(): void
|
||||
{
|
||||
$request = $this->createPingRequest();
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertEquals($request->getId(), $response->id);
|
||||
$this->assertInstanceOf(EmptyResult::class, $response->result);
|
||||
}
|
||||
|
||||
public function testHandleMultiplePingRequests(): void
|
||||
{
|
||||
$request1 = $this->createPingRequest();
|
||||
$request2 = $this->createPingRequest();
|
||||
|
||||
$response1 = $this->handler->handle($request1);
|
||||
$response2 = $this->handler->handle($request2);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response1);
|
||||
$this->assertInstanceOf(Response::class, $response2);
|
||||
$this->assertInstanceOf(EmptyResult::class, $response1->result);
|
||||
$this->assertInstanceOf(EmptyResult::class, $response2->result);
|
||||
$this->assertEquals($request1->getId(), $response1->id);
|
||||
$this->assertEquals($request2->getId(), $response2->id);
|
||||
}
|
||||
|
||||
public function testHandlerHasNoSideEffects(): void
|
||||
{
|
||||
$request = $this->createPingRequest();
|
||||
|
||||
// Handle same request multiple times
|
||||
$response1 = $this->handler->handle($request);
|
||||
$response2 = $this->handler->handle($request);
|
||||
|
||||
// Both responses should be identical
|
||||
$this->assertEquals($response1->id, $response2->id);
|
||||
$this->assertEquals(
|
||||
\get_class($response1->result),
|
||||
\get_class($response2->result),
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmptyResultIsCorrectType(): void
|
||||
{
|
||||
$request = $this->createPingRequest();
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(EmptyResult::class, $response->result);
|
||||
|
||||
// Verify EmptyResult serializes to empty object
|
||||
$serialized = json_encode($response->result);
|
||||
$this->assertEquals('{}', $serialized);
|
||||
}
|
||||
|
||||
public function testHandlerIsStateless(): void
|
||||
{
|
||||
$handler1 = new PingHandler();
|
||||
$handler2 = new PingHandler();
|
||||
|
||||
$request = $this->createPingRequest();
|
||||
|
||||
$response1 = $handler1->handle($request);
|
||||
$response2 = $handler2->handle($request);
|
||||
|
||||
// Both handlers should produce equivalent results
|
||||
$this->assertEquals($response1->id, $response2->id);
|
||||
$this->assertEquals(
|
||||
\get_class($response1->result),
|
||||
\get_class($response2->result),
|
||||
);
|
||||
}
|
||||
|
||||
public function testSupportsMethodIsConsistent(): void
|
||||
{
|
||||
$request = $this->createPingRequest();
|
||||
|
||||
// Multiple calls to supports should return same result
|
||||
$this->assertTrue($this->handler->supports($request));
|
||||
$this->assertTrue($this->handler->supports($request));
|
||||
$this->assertTrue($this->handler->supports($request));
|
||||
}
|
||||
|
||||
public function testHandlerCanBeReused(): void
|
||||
{
|
||||
$requests = [];
|
||||
$responses = [];
|
||||
|
||||
// Create multiple ping requests
|
||||
for ($i = 0; $i < 5; ++$i) {
|
||||
$requests[$i] = $this->createPingRequest();
|
||||
$responses[$i] = $this->handler->handle($requests[$i]);
|
||||
}
|
||||
|
||||
// All responses should be valid
|
||||
foreach ($responses as $i => $response) {
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertEquals($requests[$i]->getId(), $response->id);
|
||||
$this->assertInstanceOf(EmptyResult::class, $response->result);
|
||||
}
|
||||
}
|
||||
|
||||
private function createPingRequest(): Request
|
||||
{
|
||||
return PingRequest::fromArray([
|
||||
'jsonrpc' => '2.0',
|
||||
'method' => PingRequest::getMethod(),
|
||||
'id' => 'test-request-'.uniqid(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
351
tests/Server/RequestHandler/ReadResourceHandlerTest.php
Normal file
351
tests/Server/RequestHandler/ReadResourceHandlerTest.php
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
<?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\Server\RequestHandler;
|
||||
|
||||
use Mcp\Capability\Resource\ResourceReaderInterface;
|
||||
use Mcp\Exception\ResourceNotFoundException;
|
||||
use Mcp\Exception\ResourceReadException;
|
||||
use Mcp\Schema\Content\BlobResourceContents;
|
||||
use Mcp\Schema\Content\TextResourceContents;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\ReadResourceRequest;
|
||||
use Mcp\Schema\Result\ReadResourceResult;
|
||||
use Mcp\Server\RequestHandler\ReadResourceHandler;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ReadResourceHandlerTest extends TestCase
|
||||
{
|
||||
private ReadResourceHandler $handler;
|
||||
private ResourceReaderInterface|MockObject $resourceReader;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->resourceReader = $this->createMock(ResourceReaderInterface::class);
|
||||
|
||||
$this->handler = new ReadResourceHandler($this->resourceReader);
|
||||
}
|
||||
|
||||
public function testSupportsReadResourceRequest(): void
|
||||
{
|
||||
$request = $this->createReadResourceRequest('file://test.txt');
|
||||
|
||||
$this->assertTrue($this->handler->supports($request));
|
||||
}
|
||||
|
||||
public function testHandleSuccessfulResourceRead(): void
|
||||
{
|
||||
$uri = 'file://documents/readme.txt';
|
||||
$request = $this->createReadResourceRequest($uri);
|
||||
$expectedContent = new TextResourceContents(
|
||||
uri: $uri,
|
||||
mimeType: 'text/plain',
|
||||
text: 'This is the content of the readme file.',
|
||||
);
|
||||
$expectedResult = new ReadResourceResult([$expectedContent]);
|
||||
|
||||
$this->resourceReader
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertEquals($request->getId(), $response->id);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandleResourceReadWithBlobContent(): void
|
||||
{
|
||||
$uri = 'file://images/logo.png';
|
||||
$request = $this->createReadResourceRequest($uri);
|
||||
$expectedContent = new BlobResourceContents(
|
||||
uri: $uri,
|
||||
mimeType: 'image/png',
|
||||
blob: base64_encode('fake-image-data'),
|
||||
);
|
||||
$expectedResult = new ReadResourceResult([$expectedContent]);
|
||||
|
||||
$this->resourceReader
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandleResourceReadWithMultipleContents(): void
|
||||
{
|
||||
$uri = 'app://data/mixed-content';
|
||||
$request = $this->createReadResourceRequest($uri);
|
||||
$textContent = new TextResourceContents(
|
||||
uri: $uri,
|
||||
mimeType: 'text/plain',
|
||||
text: 'Text part of the resource',
|
||||
);
|
||||
$blobContent = new BlobResourceContents(
|
||||
uri: $uri,
|
||||
mimeType: 'application/octet-stream',
|
||||
blob: base64_encode('binary-data'),
|
||||
);
|
||||
$expectedResult = new ReadResourceResult([$textContent, $blobContent]);
|
||||
|
||||
$this->resourceReader
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
$this->assertCount(2, $response->result->contents);
|
||||
}
|
||||
|
||||
public function testHandleResourceNotFoundExceptionReturnsSpecificError(): void
|
||||
{
|
||||
$uri = 'file://nonexistent/file.txt';
|
||||
$request = $this->createReadResourceRequest($uri);
|
||||
$exception = new ResourceNotFoundException($request);
|
||||
|
||||
$this->resourceReader
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->with($request)
|
||||
->willThrowException($exception);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Error::class, $response);
|
||||
$this->assertEquals($request->getId(), $response->id);
|
||||
$this->assertEquals(Error::RESOURCE_NOT_FOUND, $response->code);
|
||||
$this->assertEquals('Resource not found for uri: "'.$uri.'".', $response->message);
|
||||
}
|
||||
|
||||
public function testHandleResourceReadExceptionReturnsGenericError(): void
|
||||
{
|
||||
$uri = 'file://corrupted/file.txt';
|
||||
$request = $this->createReadResourceRequest($uri);
|
||||
$exception = new ResourceReadException(
|
||||
$request,
|
||||
new \RuntimeException('Failed to read resource: corrupted data'),
|
||||
);
|
||||
|
||||
$this->resourceReader
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->with($request)
|
||||
->willThrowException($exception);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Error::class, $response);
|
||||
$this->assertEquals($request->getId(), $response->id);
|
||||
$this->assertEquals(Error::INTERNAL_ERROR, $response->code);
|
||||
$this->assertEquals('Error while reading resource', $response->message);
|
||||
}
|
||||
|
||||
public function testHandleResourceReadWithDifferentUriSchemes(): void
|
||||
{
|
||||
$uriSchemes = [
|
||||
'file://local/path/file.txt',
|
||||
'http://example.com/resource',
|
||||
'https://secure.example.com/api/data',
|
||||
'ftp://files.example.com/document.pdf',
|
||||
'app://internal/resource/123',
|
||||
'custom-scheme://special/resource',
|
||||
];
|
||||
|
||||
foreach ($uriSchemes as $uri) {
|
||||
$request = $this->createReadResourceRequest($uri);
|
||||
$expectedContent = new TextResourceContents(
|
||||
uri: $uri,
|
||||
mimeType: 'text/plain',
|
||||
text: "Content for {$uri}",
|
||||
);
|
||||
$expectedResult = new ReadResourceResult([$expectedContent]);
|
||||
|
||||
$this->resourceReader
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
|
||||
// Reset the mock for next iteration
|
||||
$this->resourceReader = $this->createMock(ResourceReaderInterface::class);
|
||||
$this->handler = new ReadResourceHandler($this->resourceReader);
|
||||
}
|
||||
}
|
||||
|
||||
public function testHandleResourceReadWithSpecialCharactersInUri(): void
|
||||
{
|
||||
$uri = 'file://path/with spaces/äöü-file-ñ.txt';
|
||||
$request = $this->createReadResourceRequest($uri);
|
||||
$expectedContent = new TextResourceContents(
|
||||
uri: $uri,
|
||||
mimeType: 'text/plain',
|
||||
text: 'Content with unicode characters: äöü ñ 世界 🚀',
|
||||
);
|
||||
$expectedResult = new ReadResourceResult([$expectedContent]);
|
||||
|
||||
$this->resourceReader
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
}
|
||||
|
||||
public function testHandleResourceReadWithEmptyContent(): void
|
||||
{
|
||||
$uri = 'file://empty/file.txt';
|
||||
$request = $this->createReadResourceRequest($uri);
|
||||
$expectedContent = new TextResourceContents(
|
||||
uri: $uri,
|
||||
mimeType: 'text/plain',
|
||||
text: '',
|
||||
);
|
||||
$expectedResult = new ReadResourceResult([$expectedContent]);
|
||||
|
||||
$this->resourceReader
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
$this->assertInstanceOf(TextResourceContents::class, $response->result->contents[0]);
|
||||
$this->assertEquals('', $response->result->contents[0]->text);
|
||||
}
|
||||
|
||||
public function testHandleResourceReadWithDifferentMimeTypes(): void
|
||||
{
|
||||
$mimeTypes = [
|
||||
'text/plain',
|
||||
'text/html',
|
||||
'application/json',
|
||||
'application/xml',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'application/pdf',
|
||||
'video/mp4',
|
||||
'audio/mpeg',
|
||||
'application/octet-stream',
|
||||
];
|
||||
|
||||
foreach ($mimeTypes as $i => $mimeType) {
|
||||
$uri = "file://test/file{$i}";
|
||||
$request = $this->createReadResourceRequest($uri);
|
||||
|
||||
if (str_starts_with($mimeType, 'text/') || str_starts_with($mimeType, 'application/json')) {
|
||||
$expectedContent = new TextResourceContents(
|
||||
uri: $uri,
|
||||
mimeType: $mimeType,
|
||||
text: "Content for {$mimeType}",
|
||||
);
|
||||
} else {
|
||||
$expectedContent = new BlobResourceContents(
|
||||
uri: $uri,
|
||||
mimeType: $mimeType,
|
||||
blob: base64_encode("binary-content-for-{$mimeType}"),
|
||||
);
|
||||
}
|
||||
$expectedResult = new ReadResourceResult([$expectedContent]);
|
||||
|
||||
$this->resourceReader
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
$this->assertEquals($mimeType, $response->result->contents[0]->mimeType);
|
||||
|
||||
// Reset the mock for next iteration
|
||||
$this->resourceReader = $this->createMock(ResourceReaderInterface::class);
|
||||
$this->handler = new ReadResourceHandler($this->resourceReader);
|
||||
}
|
||||
}
|
||||
|
||||
public function testHandleResourceNotFoundWithCustomMessage(): void
|
||||
{
|
||||
$uri = 'file://custom/missing.txt';
|
||||
$request = $this->createReadResourceRequest($uri);
|
||||
$exception = new ResourceNotFoundException($request);
|
||||
|
||||
$this->resourceReader
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->with($request)
|
||||
->willThrowException($exception);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Error::class, $response);
|
||||
$this->assertEquals(Error::RESOURCE_NOT_FOUND, $response->code);
|
||||
$this->assertEquals('Resource not found for uri: "'.$uri.'".', $response->message);
|
||||
}
|
||||
|
||||
public function testHandleResourceReadWithEmptyResult(): void
|
||||
{
|
||||
$uri = 'file://empty/resource';
|
||||
$request = $this->createReadResourceRequest($uri);
|
||||
$expectedResult = new ReadResourceResult([]);
|
||||
|
||||
$this->resourceReader
|
||||
->expects($this->once())
|
||||
->method('read')
|
||||
->with($request)
|
||||
->willReturn($expectedResult);
|
||||
|
||||
$response = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
$this->assertSame($expectedResult, $response->result);
|
||||
$this->assertCount(0, $response->result->contents);
|
||||
}
|
||||
|
||||
private function createReadResourceRequest(string $uri): ReadResourceRequest
|
||||
{
|
||||
return ReadResourceRequest::fromArray([
|
||||
'jsonrpc' => '2.0',
|
||||
'method' => ReadResourceRequest::getMethod(),
|
||||
'id' => 'test-request-'.uniqid(),
|
||||
'params' => [
|
||||
'uri' => $uri,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ class ServerTest extends TestCase
|
|||
->disableOriginalConstructor()
|
||||
->onlyMethods(['process'])
|
||||
->getMock();
|
||||
|
||||
$handler->expects($this->exactly(2))->method('process')->willReturnOnConsecutiveCalls(new Exception(new \JsonException('foobar')), ['success']);
|
||||
|
||||
$transport = $this->getMockBuilder(InMemoryTransport::class)
|
||||
|
|
|
|||
Loading…
Reference in a new issue