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:
Pavel Buchnev 2025-09-14 22:44:18 +04:00 • committed by GitHub
commit 99bf56eef8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 4656 additions and 334 deletions

View file

@ -11,13 +11,12 @@
namespace Mcp\Server\RequestHandler;
use Mcp\Capability\Registry;
use Mcp\Capability\Tool\ToolCallerInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\CallToolRequest;
use Mcp\Schema\Result\CallToolResult;
use Mcp\Server\MethodHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
@ -29,7 +28,7 @@ use Psr\Log\NullLogger;
final class CallToolHandler implements MethodHandlerInterface
{
public function __construct(
private readonly Registry $registry,
private readonly ToolCallerInterface $toolCaller,
private readonly LoggerInterface $logger = new NullLogger(),
) {
}
@ -44,16 +43,19 @@ final class CallToolHandler implements MethodHandlerInterface
\assert($message instanceof CallToolRequest);
try {
$content = $this->registry->handleCallTool($message->name, $message->arguments);
$content = $this->toolCaller->call($message);
} catch (ExceptionInterface $exception) {
$this->logger->error(\sprintf('Error while executing tool "%s": "%s".', $message->name, $exception->getMessage()), [
'tool' => $message->name,
'arguments' => $message->arguments,
]);
$this->logger->error(
\sprintf('Error while executing tool "%s": "%s".', $message->name, $exception->getMessage()),
[
'tool' => $message->name,
'arguments' => $message->arguments,
],
);
return Error::forInternalError('Error while executing tool', $message->getId());
}
return new Response($message->getId(), new CallToolResult($content));
return new Response($message->getId(), $content);
}
}

View file

@ -11,13 +11,12 @@
namespace Mcp\Server\RequestHandler;
use Mcp\Capability\Registry;
use Mcp\Capability\Prompt\PromptGetterInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\GetPromptRequest;
use Mcp\Schema\Result\GetPromptResult;
use Mcp\Server\MethodHandlerInterface;
/**
@ -26,7 +25,7 @@ use Mcp\Server\MethodHandlerInterface;
final class GetPromptHandler implements MethodHandlerInterface
{
public function __construct(
private readonly Registry $registry,
private readonly PromptGetterInterface $promptGetter,
) {
}
@ -40,11 +39,11 @@ final class GetPromptHandler implements MethodHandlerInterface
\assert($message instanceof GetPromptRequest);
try {
$messages = $this->registry->handleGetPrompt($message->name, $message->arguments);
$messages = $this->promptGetter->get($message);
} catch (ExceptionInterface) {
return Error::forInternalError('Error while handling prompt', $message->getId());
}
return new Response($message->getId(), new GetPromptResult($messages));
return new Response($message->getId(), $messages);
}
}

View file

@ -11,7 +11,7 @@
namespace Mcp\Server\RequestHandler;
use Mcp\Capability\Registry;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ListPromptsRequest;
@ -24,7 +24,7 @@ use Mcp\Server\MethodHandlerInterface;
final class ListPromptsHandler implements MethodHandlerInterface
{
public function __construct(
private readonly Registry $registry,
private readonly ReferenceProviderInterface $registry,
private readonly int $pageSize = 20,
) {
}

View file

@ -11,7 +11,7 @@
namespace Mcp\Server\RequestHandler;
use Mcp\Capability\Registry;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ListResourcesRequest;
@ -24,7 +24,7 @@ use Mcp\Server\MethodHandlerInterface;
final class ListResourcesHandler implements MethodHandlerInterface
{
public function __construct(
private readonly Registry $registry,
private readonly ReferenceProviderInterface $registry,
private readonly int $pageSize = 20,
) {
}

View file

@ -11,7 +11,7 @@
namespace Mcp\Server\RequestHandler;
use Mcp\Capability\Registry;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ListToolsRequest;
@ -25,7 +25,7 @@ use Mcp\Server\MethodHandlerInterface;
final class ListToolsHandler implements MethodHandlerInterface
{
public function __construct(
private readonly Registry $registry,
private readonly ReferenceProviderInterface $registry,
private readonly int $pageSize = 20,
) {
}

View file

@ -11,14 +11,13 @@
namespace Mcp\Server\RequestHandler;
use Mcp\Capability\Registry;
use Mcp\Capability\Resource\ResourceReaderInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\ResourceNotFoundException;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ReadResourceRequest;
use Mcp\Schema\Result\ReadResourceResult;
use Mcp\Server\MethodHandlerInterface;
/**
@ -27,7 +26,7 @@ use Mcp\Server\MethodHandlerInterface;
final class ReadResourceHandler implements MethodHandlerInterface
{
public function __construct(
private readonly Registry $registry,
private readonly ResourceReaderInterface $resourceReader,
) {
}
@ -41,13 +40,13 @@ final class ReadResourceHandler implements MethodHandlerInterface
\assert($message instanceof ReadResourceRequest);
try {
$contents = $this->registry->handleReadResource($message->uri);
$contents = $this->resourceReader->read($message);
} catch (ResourceNotFoundException $e) {
return new Error($message->getId(), Error::RESOURCE_NOT_FOUND, $e->getMessage());
} catch (ExceptionInterface) {
return Error::forInternalError('Error while reading resource', $message->getId());
}
return new Response($message->getId(), new ReadResourceResult($contents));
return new Response($message->getId(), $contents);
}
}

View file

@ -18,9 +18,15 @@ use Mcp\Capability\Discovery\HandlerResolver;
use Mcp\Capability\Discovery\SchemaGenerator;
use Mcp\Capability\Prompt\Completion\EnumCompletionProvider;
use Mcp\Capability\Prompt\Completion\ListCompletionProvider;
use Mcp\Capability\Prompt\PromptGetter;
use Mcp\Capability\Prompt\PromptGetterInterface;
use Mcp\Capability\Registry;
use Mcp\Capability\Registry\Container;
use Mcp\Capability\Registry\ReferenceHandler;
use Mcp\Capability\Resource\ResourceReader;
use Mcp\Capability\Resource\ResourceReaderInterface;
use Mcp\Capability\Tool\ToolCaller;
use Mcp\Capability\Tool\ToolCallerInterface;
use Mcp\Exception\ConfigurationException;
use Mcp\JsonRpc\Handler;
use Mcp\Schema\Annotations;
@ -49,6 +55,12 @@ final class ServerBuilder
private ?CacheInterface $cache = null;
private ?ToolCallerInterface $toolCaller = null;
private ?ResourceReaderInterface $resourceReader = null;
private ?PromptGetterInterface $promptGetter = null;
private ?EventDispatcherInterface $eventDispatcher = null;
private ?ContainerInterface $container = null;
@ -149,6 +161,27 @@ final class ServerBuilder
return $this;
}
public function withToolCaller(ToolCallerInterface $toolCaller): self
{
$this->toolCaller = $toolCaller;
return $this;
}
public function withResourceReader(ResourceReaderInterface $resourceReader): self
{
$this->resourceReader = $resourceReader;
return $this;
}
public function withPromptGetter(PromptGetterInterface $promptGetter): self
{
$this->promptGetter = $promptGetter;
return $this;
}
/**
* Provides a PSR-11 DI container, primarily for resolving user-defined handler classes.
* Defaults to a basic internal container.
@ -175,8 +208,13 @@ final class ServerBuilder
/**
* Manually registers a tool handler.
*/
public function withTool(callable|array|string $handler, ?string $name = null, ?string $description = null, ?ToolAnnotations $annotations = null, ?array $inputSchema = null): self
{
public function withTool(
callable|array|string $handler,
?string $name = null,
?string $description = null,
?ToolAnnotations $annotations = null,
?array $inputSchema = null,
): self {
$this->manualTools[] = compact('handler', 'name', 'description', 'annotations', 'inputSchema');
return $this;
@ -185,8 +223,15 @@ final class ServerBuilder
/**
* Manually registers a resource handler.
*/
public function withResource(callable|array|string $handler, string $uri, ?string $name = null, ?string $description = null, ?string $mimeType = null, ?int $size = null, ?Annotations $annotations = null): self
{
public function withResource(
callable|array|string $handler,
string $uri,
?string $name = null,
?string $description = null,
?string $mimeType = null,
?int $size = null,
?Annotations $annotations = null,
): self {
$this->manualResources[] = compact('handler', 'uri', 'name', 'description', 'mimeType', 'size', 'annotations');
return $this;
@ -195,9 +240,22 @@ final class ServerBuilder
/**
* Manually registers a resource template handler.
*/
public function withResourceTemplate(callable|array|string $handler, string $uriTemplate, ?string $name = null, ?string $description = null, ?string $mimeType = null, ?Annotations $annotations = null): self
{
$this->manualResourceTemplates[] = compact('handler', 'uriTemplate', 'name', 'description', 'mimeType', 'annotations');
public function withResourceTemplate(
callable|array|string $handler,
string $uriTemplate,
?string $name = null,
?string $description = null,
?string $mimeType = null,
?Annotations $annotations = null,
): self {
$this->manualResourceTemplates[] = compact(
'handler',
'uriTemplate',
'name',
'description',
'mimeType',
'annotations',
);
return $this;
}
@ -220,7 +278,12 @@ final class ServerBuilder
$logger = $this->logger ?? new NullLogger();
$container = $this->container ?? new Container();
$registry = new Registry(new ReferenceHandler($container), $this->eventDispatcher, $logger);
$registry = new Registry($this->eventDispatcher, $logger);
$referenceHandler = new ReferenceHandler($container);
$toolCaller = $this->toolCaller ??= new ToolCaller($registry, $referenceHandler, $logger);
$resourceReader = $this->resourceReader ??= new ResourceReader($registry, $referenceHandler, $logger);
$promptGetter = $this->promptGetter ??= new PromptGetter($registry, $referenceHandler, $logger);
$this->registerManualElements($registry, $logger);
@ -230,8 +293,16 @@ final class ServerBuilder
}
return new Server(
Handler::make($registry, $this->serverInfo, $logger),
$logger,
jsonRpcHandler: Handler::make(
registry: $registry,
referenceProvider: $registry,
implementation: $this->serverInfo,
toolCaller: $toolCaller,
resourceReader: $resourceReader,
promptGetter: $promptGetter,
logger: $logger,
),
logger: $logger,
);
}
@ -239,8 +310,10 @@ final class ServerBuilder
* Helper to perform the actual registration based on stored data.
* Moved into the builder.
*/
private function registerManualElements(Registry $registry, LoggerInterface $logger = new NullLogger()): void
{
private function registerManualElements(
Registry\ReferenceRegistryInterface $registry,
LoggerInterface $logger = new NullLogger(),
): void {
if (empty($this->manualTools) && empty($this->manualResources) && empty($this->manualResourceTemplates) && empty($this->manualPrompts)) {
return;
}
@ -270,10 +343,15 @@ final class ServerBuilder
$tool = new Tool($name, $inputSchema, $description, $data['annotations']);
$registry->registerTool($tool, $data['handler'], true);
$handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (\is_array($data['handler']) ? implode('::', $data['handler']) : $data['handler']);
$handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (\is_array(
$data['handler'],
) ? implode('::', $data['handler']) : $data['handler']);
$logger->debug("Registered manual tool {$name} from handler {$handlerDesc}");
} catch (\Throwable $e) {
$logger->error('Failed to register manual tool', ['handler' => $data['handler'], 'name' => $data['name'], 'exception' => $e]);
$logger->error(
'Failed to register manual tool',
['handler' => $data['handler'], 'name' => $data['name'], 'exception' => $e],
);
throw new ConfigurationException("Error registering manual tool '{$data['name']}': {$e->getMessage()}", 0, $e);
}
}
@ -303,10 +381,15 @@ final class ServerBuilder
$resource = new Resource($uri, $name, $description, $mimeType, $annotations, $size);
$registry->registerResource($resource, $data['handler'], true);
$handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (\is_array($data['handler']) ? implode('::', $data['handler']) : $data['handler']);
$handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (\is_array(
$data['handler'],
) ? implode('::', $data['handler']) : $data['handler']);
$logger->debug("Registered manual resource {$name} from handler {$handlerDesc}");
} catch (\Throwable $e) {
$logger->error('Failed to register manual resource', ['handler' => $data['handler'], 'uri' => $data['uri'], 'exception' => $e]);
$logger->error(
'Failed to register manual resource',
['handler' => $data['handler'], 'uri' => $data['uri'], 'exception' => $e],
);
throw new ConfigurationException("Error registering manual resource '{$data['uri']}': {$e->getMessage()}", 0, $e);
}
}
@ -336,10 +419,15 @@ final class ServerBuilder
$completionProviders = $this->getCompletionProviders($reflection);
$registry->registerResourceTemplate($template, $data['handler'], $completionProviders, true);
$handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (\is_array($data['handler']) ? implode('::', $data['handler']) : $data['handler']);
$handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (\is_array(
$data['handler'],
) ? implode('::', $data['handler']) : $data['handler']);
$logger->debug("Registered manual template {$name} from handler {$handlerDesc}");
} catch (\Throwable $e) {
$logger->error('Failed to register manual template', ['handler' => $data['handler'], 'uriTemplate' => $data['uriTemplate'], 'exception' => $e]);
$logger->error(
'Failed to register manual template',
['handler' => $data['handler'], 'uriTemplate' => $data['uriTemplate'], 'exception' => $e],
);
throw new ConfigurationException("Error registering manual resource template '{$data['uriTemplate']}': {$e->getMessage()}", 0, $e);
}
}
@ -362,7 +450,9 @@ final class ServerBuilder
}
$arguments = [];
$paramTags = $reflection instanceof \ReflectionMethod ? $docBlockParser->getParamTags($docBlockParser->parseDocBlock($reflection->getDocComment() ?? null)) : [];
$paramTags = $reflection instanceof \ReflectionMethod ? $docBlockParser->getParamTags(
$docBlockParser->parseDocBlock($reflection->getDocComment() ?? null),
) : [];
foreach ($reflection->getParameters() as $param) {
$reflectionType = $param->getType();
@ -375,7 +465,7 @@ final class ServerBuilder
$arguments[] = new PromptArgument(
$param->getName(),
$paramTag ? trim((string) $paramTag->getDescription()) : null,
!$param->isOptional() && !$param->isDefaultValueAvailable()
!$param->isOptional() && !$param->isDefaultValueAvailable(),
);
}
@ -383,10 +473,15 @@ final class ServerBuilder
$completionProviders = $this->getCompletionProviders($reflection);
$registry->registerPrompt($prompt, $data['handler'], $completionProviders, true);
$handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (\is_array($data['handler']) ? implode('::', $data['handler']) : $data['handler']);
$handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (\is_array(
$data['handler'],
) ? implode('::', $data['handler']) : $data['handler']);
$logger->debug("Registered manual prompt {$name} from handler {$handlerDesc}");
} catch (\Throwable $e) {
$logger->error('Failed to register manual prompt', ['handler' => $data['handler'], 'name' => $data['name'], 'exception' => $e]);
$logger->error(
'Failed to register manual prompt',
['handler' => $data['handler'], 'name' => $data['name'], 'exception' => $e],
);
throw new ConfigurationException("Error registering manual prompt '{$data['name']}': {$e->getMessage()}", 0, $e);
}
}
@ -403,7 +498,10 @@ final class ServerBuilder
continue;
}
$completionAttributes = $param->getAttributes(CompletionProvider::class, \ReflectionAttribute::IS_INSTANCEOF);
$completionAttributes = $param->getAttributes(
CompletionProvider::class,
\ReflectionAttribute::IS_INSTANCEOF,
);
if (!empty($completionAttributes)) {
$attributeInstance = $completionAttributes[0]->newInstance();