Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6e4c57d
refactor: Separate Registry concerns following SOLID principles [WIP]
butschster Sep 7, 2025
047004e
refactor: implement SOLID principles with separated execution concerns
butschster Sep 7, 2025
9bf2a7e
chore: remove extra comments
butschster Sep 7, 2025
4aafe6b
feat: introduce DispatchableRegistry for enhanced tool and resource m…
butschster Sep 7, 2025
bd4791a
refactor: use proper interfaces
butschster Sep 7, 2025
fc467d8
refactor: implement HandlerInterface for improved abstraction and fle…
butschster Sep 7, 2025
e6dc2af
test: add unit tests for DispatchableRegistry and Registry classes
butschster Sep 7, 2025
20de24f
refactor: cover with unit tests Resource reader, Prompt getter and To…
butschster Sep 7, 2025
7237665
cs fix
butschster Sep 7, 2025
1f160f9
phpstan fix
butschster Sep 7, 2025
e7494da
test: add unit tests for CallToolHandler, GetPromptHandler, PingHandl…
butschster Sep 8, 2025
3773e75
refactor: revert Handler
butschster Sep 9, 2025
8a66539
chore: remove style guide for tests
butschster Sep 9, 2025
156f842
refactor: remove "Default" prefix from classes
butschster Sep 9, 2025
4f64998
refactor: remove DispatchableRegistry
butschster Sep 9, 2025
d9cd7ba
refactor: use package specific exception classes
butschster Sep 9, 2025
addfd23
refactor: remove DispatchableRegistry
butschster Sep 9, 2025
f3110eb
refactor: add logger support to PromptGetter and ResourceReader classes
butschster Sep 9, 2025
79618f7
Update src/Capability/Prompt/PromptGetter.php
butschster Sep 9, 2025
f1edddc
Merge branch 'main' into feature/tool-execution
butschster Sep 9, 2025
e413594
refactor: Use FQN for Resource class to avoid cs-fixer misinterpretin…
butschster Sep 9, 2025
9e798ad
refactor: rename ToolExecutor to ToolCaller and related classes for c…
butschster Sep 9, 2025
e11c0c1
refactor: add missed docblock
butschster Sep 9, 2025
3c70c5c
refactor: rename ToolExecutor references to ToolCaller
butschster Sep 9, 2025
7bd23b3
refactor: rename ToolCallerTest property and variable references
butschster Sep 9, 2025
32f2de9
cs fix
butschster Sep 9, 2025
0ca952b
ignore some phpstan errors
butschster Sep 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/09-standalone-cli/src/ExampleTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
namespace App;

use Mcp\Capability\Tool\MetadataInterface;
use Mcp\Capability\Tool\ToolExecutorInterface;
use Mcp\Capability\Tool\ToolCallerInterface;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Request\CallToolRequest;
use Mcp\Schema\Result\CallToolResult;

/**
* @author Tobias Nyholm <[email protected]>
*/
class ExampleTool implements MetadataInterface, ToolExecutorInterface
class ExampleTool implements MetadataInterface, ToolCallerInterface
{
public function call(CallToolRequest $request): CallToolResult
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\ToolExecutionException;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Content\AudioContent;
use Mcp\Schema\Content\EmbeddedResource;
Expand All @@ -30,7 +30,7 @@
*
* @author Pavel Buchnev <[email protected]>
*/
final class ToolExecutor implements ToolExecutorInterface
final class ToolCaller implements ToolCallerInterface
{
public function __construct(
private readonly ReferenceProviderInterface $referenceProvider,
Expand All @@ -40,7 +40,7 @@ public function __construct(
}

/**
* @throws ToolExecutionException if the tool execution fails
* @throws ToolCallException if the tool execution fails
* @throws ToolNotFoundException if the tool is not found
*/
public function call(CallToolRequest $request): CallToolResult
Expand Down Expand Up @@ -75,7 +75,7 @@ public function call(CallToolRequest $request): CallToolResult
'trace' => $e->getTraceAsString(),
]);

throw new ToolExecutionException($request, $e);
throw new ToolCallException($request, $e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@

namespace Mcp\Capability\Tool;

use Mcp\Exception\ToolExecutionException;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Request\CallToolRequest;
use Mcp\Schema\Result\CallToolResult;

/**
* @author Tobias Nyholm <[email protected]>
*/
interface ToolExecutorInterface
interface ToolCallerInterface
{
/**
* @throws ToolExecutionException if the tool execution fails
* @throws ToolCallException if the tool execution fails
* @throws ToolNotFoundException if the tool is not found
*/
public function call(CallToolRequest $request): CallToolResult;
Expand Down
10 changes: 5 additions & 5 deletions src/Capability/ToolChain.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
use Mcp\Capability\Tool\CollectionInterface;
use Mcp\Capability\Tool\IdentifierInterface;
use Mcp\Capability\Tool\MetadataInterface;
use Mcp\Capability\Tool\ToolExecutorInterface;
use Mcp\Capability\Tool\ToolCallerInterface;
use Mcp\Exception\InvalidCursorException;
use Mcp\Exception\ToolExecutionException;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Request\CallToolRequest;
use Mcp\Schema\Result\CallToolResult;
Expand All @@ -26,7 +26,7 @@
*
* @author Tobias Nyholm <[email protected]>
*/
class ToolChain implements ToolExecutorInterface, CollectionInterface
class ToolChain implements ToolCallerInterface, CollectionInterface
{
public function __construct(
/**
Expand Down Expand Up @@ -63,11 +63,11 @@ public function getMetadata(int $count, ?string $lastIdentifier = null): iterabl
public function call(CallToolRequest $request): CallToolResult
{
foreach ($this->items as $item) {
if ($item instanceof ToolExecutorInterface && $request->name === $item->getName()) {
if ($item instanceof ToolCallerInterface && $request->name === $item->getName()) {
try {
return $item->call($request);
} catch (\Throwable $e) {
throw new ToolExecutionException($request, $e);
throw new ToolCallException($request, $e);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
/**
* @author Tobias Nyholm <[email protected]>
*/
final class ToolExecutionException extends \RuntimeException implements ExceptionInterface
final class ToolCallException extends \RuntimeException implements ExceptionInterface
{
public function __construct(
public readonly CallToolRequest $request,
?\Throwable $previous = null,
) {
parent::__construct(\sprintf('Execution of tool "%s" failed with error: "%s".', $request->name, $previous?->getMessage() ?? ''), previous: $previous);
parent::__construct(\sprintf('Tool call "%s" failed with error: "%s".', $request->name, $previous?->getMessage() ?? ''), previous: $previous);
}
}
4 changes: 2 additions & 2 deletions src/Server/RequestHandler/CallToolHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Mcp\Server\RequestHandler;

use Mcp\Capability\Tool\ToolExecutorInterface;
use Mcp\Capability\Tool\ToolCallerInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
Expand All @@ -28,7 +28,7 @@
final class CallToolHandler implements MethodHandlerInterface
{
public function __construct(
private readonly ToolExecutorInterface $toolExecutor,
private readonly ToolCallerInterface $toolExecutor,
private readonly LoggerInterface $logger = new NullLogger(),
) {
}
Expand Down
10 changes: 5 additions & 5 deletions src/Server/ServerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
use Mcp\Capability\Registry\ReferenceHandler;
use Mcp\Capability\Resource\ResourceReader;
use Mcp\Capability\Resource\ResourceReaderInterface;
use Mcp\Capability\Tool\ToolExecutor;
use Mcp\Capability\Tool\ToolExecutorInterface;
use Mcp\Capability\Tool\ToolCaller;
use Mcp\Capability\Tool\ToolCallerInterface;
use Mcp\Exception\ConfigurationException;
use Mcp\JsonRpc\Handler;
use Mcp\Schema\Annotations;
Expand Down Expand Up @@ -55,7 +55,7 @@ final class ServerBuilder

private ?CacheInterface $cache = null;

private ?ToolExecutorInterface $toolExecutor = null;
private ?ToolCallerInterface $toolExecutor = null;

private ?ResourceReaderInterface $resourceReader = null;

Expand Down Expand Up @@ -161,7 +161,7 @@ public function withEventDispatcher(EventDispatcherInterface $eventDispatcher):
return $this;
}

public function withToolExecutor(ToolExecutorInterface $toolExecutor): self
public function withToolExecutor(ToolCallerInterface $toolExecutor): self
{
$this->toolExecutor = $toolExecutor;

Expand Down Expand Up @@ -281,7 +281,7 @@ public function build(): Server
$registry = new Registry($this->eventDispatcher, $logger);

$referenceHandler = new ReferenceHandler($container);
$toolExecutor = $this->toolExecutor ??= new ToolExecutor($registry, $referenceHandler, $logger);
$toolExecutor = $this->toolExecutor ??= new ToolCaller($registry, $referenceHandler, $logger);
$resourceReader = $this->resourceReader ??= new ResourceReader($registry, $referenceHandler, $logger);
$promptGetter = $this->promptGetter ??= new PromptGetter($registry, $referenceHandler, $logger);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Capability\Registry\ToolReference;
use Mcp\Capability\Tool\ToolExecutor;
use Mcp\Exception\ToolExecutionException;
use Mcp\Capability\Tool\ToolCaller;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Request\CallToolRequest;
Expand All @@ -25,9 +25,9 @@
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class ToolExecutorTest extends TestCase
class ToolCallerTest extends TestCase
{
private ToolExecutor $toolExecutor;
private ToolCaller $toolExecutor;
private ReferenceProviderInterface|MockObject $referenceProvider;
private ReferenceHandlerInterface|MockObject $referenceHandler;
private LoggerInterface|MockObject $logger;
Expand All @@ -38,7 +38,7 @@ protected function setUp(): void
$this->referenceHandler = $this->createMock(ReferenceHandlerInterface::class);
$this->logger = $this->createMock(LoggerInterface::class);

$this->toolExecutor = new ToolExecutor(
$this->toolExecutor = new ToolCaller(
$this->referenceProvider,
$this->referenceHandler,
$this->logger,
Expand Down Expand Up @@ -211,13 +211,13 @@ public function testCallThrowsToolExecutionExceptionWhenHandlerThrowsException()
})
);

$this->expectException(ToolExecutionException::class);
$this->expectExceptionMessage('Execution of tool "failing_tool" failed with error: "Handler failed".');
$this->expectException(ToolCallException::class);
$this->expectExceptionMessage('Tool call "failing_tool" failed with error: "Handler failed".');

$thrownException = null;
try {
$this->toolExecutor->call($request);
} catch (ToolExecutionException $e) {
} catch (ToolCallException $e) {
$thrownException = $e;
throw $e;
} finally {
Expand Down Expand Up @@ -404,8 +404,8 @@ public function testCallWithDifferentExceptionTypes(): void
})
);

$this->expectException(ToolExecutionException::class);
$this->expectExceptionMessage('Execution of tool "error_tool" failed with error: "Invalid input".');
$this->expectException(ToolCallException::class);
$this->expectExceptionMessage('Tool call "error_tool" failed with error: "Invalid input".');

$this->toolExecutor->call($request);
}
Expand Down Expand Up @@ -493,10 +493,10 @@ public function testCallLogsResultTypeCorrectlyForArray(): void

public function testConstructorWithDefaultLogger(): void
{
$executor = new ToolExecutor($this->referenceProvider, $this->referenceHandler);
$executor = new ToolCaller($this->referenceProvider, $this->referenceHandler);

// Verify it's constructed without throwing exceptions
$this->assertInstanceOf(ToolExecutor::class, $executor);
$this->assertInstanceOf(ToolCaller::class, $executor);
}

public function testCallHandlesEmptyArrayResult(): void
Expand Down
21 changes: 12 additions & 9 deletions tests/Server/RequestHandler/CallToolHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

namespace Mcp\Tests\Server\RequestHandler;

use Mcp\Capability\Tool\ToolExecutorInterface;
use Mcp\Exception\ToolExecutionException;
use Mcp\Capability\Tool\ToolCallerInterface;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\JsonRpc\Error;
Expand All @@ -28,12 +28,12 @@
class CallToolHandlerTest extends TestCase
{
private CallToolHandler $handler;
private ToolExecutorInterface|MockObject $toolExecutor;
private ToolCallerInterface|MockObject $toolExecutor;
private LoggerInterface|MockObject $logger;

protected function setUp(): void
{
$this->toolExecutor = $this->createMock(ToolExecutorInterface::class);
$this->toolExecutor = $this->createMock(ToolCallerInterface::class);
$this->logger = $this->createMock(LoggerInterface::class);

$this->handler = new CallToolHandler(
Expand Down Expand Up @@ -145,7 +145,7 @@ public function testHandleToolNotFoundExceptionReturnsError(): void
public function testHandleToolExecutionExceptionReturnsError(): void
{
$request = $this->createCallToolRequest('failing_tool', ['param' => 'value']);
$exception = new ToolExecutionException($request, new \RuntimeException('Tool execution failed'));
$exception = new ToolCallException($request, new \RuntimeException('Tool execution failed'));

$this->toolExecutor
->expects($this->once())
Expand All @@ -157,7 +157,7 @@ public function testHandleToolExecutionExceptionReturnsError(): void
->expects($this->once())
->method('error')
->with(
'Error while executing tool "failing_tool": "Execution of tool "failing_tool" failed with error: "Tool execution failed".".',
'Error while executing tool "failing_tool": "Tool call "failing_tool" failed with error: "Tool execution failed".".',
[
'tool' => 'failing_tool',
'arguments' => ['param' => 'value'],
Expand Down Expand Up @@ -217,7 +217,7 @@ public function testConstructorWithDefaultLogger(): void
public function testHandleLogsErrorWithCorrectParameters(): void
{
$request = $this->createCallToolRequest('test_tool', ['key1' => 'value1', 'key2' => 42]);
$exception = new ToolExecutionException($request, new \RuntimeException('Custom error message'));
$exception = new ToolCallException($request, new \RuntimeException('Custom error message'));

$this->toolExecutor
->expects($this->once())
Expand All @@ -228,7 +228,7 @@ public function testHandleLogsErrorWithCorrectParameters(): void
->expects($this->once())
->method('error')
->with(
'Error while executing tool "test_tool": "Execution of tool "test_tool" failed with error: "Custom error message".".',
'Error while executing tool "test_tool": "Tool call "test_tool" failed with error: "Custom error message".".',
[
'tool' => 'test_tool',
'arguments' => ['key1' => 'value1', 'key2' => 42],
Expand Down Expand Up @@ -277,7 +277,10 @@ public function testHandleWithSpecialCharactersInArguments(): void
$this->assertSame($expectedResult, $response->result);
}

private function createCallToolRequest(string $name, array $arguments): Request
/**
* @param array<string, mixed> $arguments
*/
private function createCallToolRequest(string $name, array $arguments): CallToolRequest
{
return CallToolRequest::fromArray([
'jsonrpc' => '2.0',
Expand Down
7 changes: 6 additions & 1 deletion tests/Server/RequestHandler/GetPromptHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,12 @@ public function testHandlePromptGetWithLargeNumberOfArguments(): void
$this->assertSame($expectedResult, $response->result);
}

private function createGetPromptRequest(string $name, ?array $arguments = null): Request
/**
* @param string $name
* @param array<string, mixed>|null $arguments
* @return GetPromptRequest
*/
private function createGetPromptRequest(string $name, ?array $arguments = null): GetPromptRequest
{
return GetPromptRequest::fromArray([
'jsonrpc' => '2.0',
Expand Down
3 changes: 2 additions & 1 deletion tests/Server/RequestHandler/ReadResourceHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ public function testHandleResourceReadWithEmptyContent(): void

$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);
}

Expand Down Expand Up @@ -337,7 +338,7 @@ public function testHandleResourceReadWithEmptyResult(): void
$this->assertCount(0, $response->result->contents);
}

private function createReadResourceRequest(string $uri): Request
private function createReadResourceRequest(string $uri): ReadResourceRequest
{
return ReadResourceRequest::fromArray([
'jsonrpc' => '2.0',
Expand Down