[Server] Simplify server extensibility with pluggable method handlers (#100)

* refactor: use a custom method handler registration system

* refactor: remove redundant Completer and CompleterInterface

* docs: update server-builder documentation to reflect custom method handler changes
This commit is contained in:
Kyrian Obikwelu 2025-10-07 23:25:06 +01:00 • committed by GitHub
commit fcf605d5d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 896 additions and 2865 deletions

View file

@ -1,75 +0,0 @@
<?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\Capability\Completion;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\RuntimeException;
use Mcp\Schema\Request\CompletionCompleteRequest;
use Mcp\Schema\Result\CompletionCompleteResult;
use Psr\Container\ContainerInterface;
/**
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
final class Completer implements CompleterInterface
{
public function __construct(
private readonly ReferenceProviderInterface $referenceProvider,
private readonly ?ContainerInterface $container = null,
) {
}
public function complete(CompletionCompleteRequest $request): CompletionCompleteResult
{
$argumentName = $request->argument['name'] ?? '';
$currentValue = $request->argument['value'] ?? '';
$reference = match (true) {
'ref/prompt' === $request->ref->type => $this->referenceProvider->getPrompt($request->ref->name),
'ref/resource' === $request->ref->type => $this->referenceProvider->getResourceTemplate($request->ref->uri),
default => null,
};
if (null === $reference) {
return new CompletionCompleteResult([]);
}
$providerClassOrInstance = $reference->completionProviders[$argumentName] ?? null;
if (null === $providerClassOrInstance) {
return new CompletionCompleteResult([]);
}
if (\is_string($providerClassOrInstance)) {
if (!class_exists($providerClassOrInstance)) {
throw new RuntimeException(\sprintf('Completion provider class "%s" does not exist.', $providerClassOrInstance));
}
$provider = $this->container?->has($providerClassOrInstance)
? $this->container->get($providerClassOrInstance)
: new $providerClassOrInstance();
} else {
$provider = $providerClassOrInstance;
}
if (!$provider instanceof ProviderInterface) {
throw new RuntimeException('Completion provider must implement ProviderInterface.');
}
$completions = $provider->getCompletions($currentValue);
$total = \count($completions);
$hasMore = $total > 100;
$pagedCompletions = \array_slice($completions, 0, 100);
return new CompletionCompleteResult($pagedCompletions, $total, $hasMore);
}
}

View file

@ -1,25 +0,0 @@
<?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\Capability\Completion;
use Mcp\Schema\Request\CompletionCompleteRequest;
use Mcp\Schema\Result\CompletionCompleteResult;
/**
* Provides completion options for prompt arguments and resource template variables.
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
interface CompleterInterface
{
public function complete(CompletionCompleteRequest $request): CompletionCompleteResult;
}

View file

@ -1,69 +0,0 @@
<?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\Capability\Prompt;
use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\PromptGetException;
use Mcp\Exception\PromptNotFoundException;
use Mcp\Schema\Request\GetPromptRequest;
use Mcp\Schema\Result\GetPromptResult;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* @author Pavel Buchnev <butschster@gmail.com>
*/
final class PromptGetter implements PromptGetterInterface
{
public function __construct(
private readonly ReferenceProviderInterface $referenceProvider,
private readonly ReferenceHandlerInterface $referenceHandler,
private readonly LoggerInterface $logger = new NullLogger(),
) {
}
public function get(GetPromptRequest $request): GetPromptResult
{
$promptName = $request->name;
$arguments = $request->arguments ?? [];
$this->logger->debug('Getting prompt', ['name' => $promptName, 'arguments' => $arguments]);
$reference = $this->referenceProvider->getPrompt($promptName);
if (null === $reference) {
$this->logger->warning('Prompt not found', ['name' => $promptName]);
throw new PromptNotFoundException($request);
}
try {
$result = $this->referenceHandler->handle($reference, $arguments);
$formattedResult = $reference->formatResult($result);
$this->logger->debug('Prompt retrieved successfully', [
'name' => $promptName,
'result_type' => \gettype($result),
]);
return new GetPromptResult($formattedResult);
} catch (\Throwable $e) {
$this->logger->error('Prompt retrieval failed', [
'name' => $promptName,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw new PromptGetException($request, $e);
}
}
}

View file

@ -1,29 +0,0 @@
<?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\Capability\Prompt;
use Mcp\Exception\PromptGetException;
use Mcp\Exception\PromptNotFoundException;
use Mcp\Schema\Request\GetPromptRequest;
use Mcp\Schema\Result\GetPromptResult;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
interface PromptGetterInterface
{
/**
* @throws PromptGetException if the prompt execution fails
* @throws PromptNotFoundException if the prompt is not found
*/
public function get(GetPromptRequest $request): GetPromptResult;
}

View file

@ -1,68 +0,0 @@
<?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\Capability\Resource;
use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\ResourceNotFoundException;
use Mcp\Exception\ResourceReadException;
use Mcp\Schema\Request\ReadResourceRequest;
use Mcp\Schema\Result\ReadResourceResult;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* @author Pavel Buchnev <butschster@gmail.com>
*/
final class ResourceReader implements ResourceReaderInterface
{
public function __construct(
private readonly ReferenceProviderInterface $referenceProvider,
private readonly ReferenceHandlerInterface $referenceHandler,
private readonly LoggerInterface $logger = new NullLogger(),
) {
}
public function read(ReadResourceRequest $request): ReadResourceResult
{
$uri = $request->uri;
$this->logger->debug('Reading resource', ['uri' => $uri]);
$reference = $this->referenceProvider->getResource($uri);
if (null === $reference) {
$this->logger->warning('Resource not found', ['uri' => $uri]);
throw new ResourceNotFoundException($request);
}
try {
$result = $this->referenceHandler->handle($reference, ['uri' => $uri]);
$formattedResult = $reference->formatResult($result, $uri);
$this->logger->debug('Resource read successfully', [
'uri' => $uri,
'result_type' => \gettype($result),
]);
return new ReadResourceResult($formattedResult);
} catch (\Throwable $e) {
$this->logger->error('Resource read failed', [
'uri' => $uri,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw new ResourceReadException($request, $e);
}
}
}

View file

@ -1,29 +0,0 @@
<?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\Capability\Resource;
use Mcp\Exception\ResourceNotFoundException;
use Mcp\Exception\ResourceReadException;
use Mcp\Schema\Request\ReadResourceRequest;
use Mcp\Schema\Result\ReadResourceResult;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
interface ResourceReaderInterface
{
/**
* @throws ResourceReadException if the resource execution fails
* @throws ResourceNotFoundException if the resource is not found
*/
public function read(ReadResourceRequest $request): ReadResourceResult;
}

View file

@ -1,81 +0,0 @@
<?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\Capability\Tool;
use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Content\AudioContent;
use Mcp\Schema\Content\EmbeddedResource;
use Mcp\Schema\Content\ImageContent;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Request\CallToolRequest;
use Mcp\Schema\Result\CallToolResult;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* Default implementation of ToolCallerInterface that uses ReferenceProvider
* and ReferenceHandlerInterface to execute tools.
*
* @author Pavel Buchnev <butschster@gmail.com>
*/
final class ToolCaller implements ToolCallerInterface
{
public function __construct(
private readonly ReferenceProviderInterface $referenceProvider,
private readonly ReferenceHandlerInterface $referenceHandler,
private readonly LoggerInterface $logger = new NullLogger(),
) {
}
/**
* @throws ToolCallException if the tool execution fails
* @throws ToolNotFoundException if the tool is not found
*/
public function call(CallToolRequest $request): CallToolResult
{
$toolName = $request->name;
$arguments = $request->arguments ?? [];
$this->logger->debug('Executing tool', ['name' => $toolName, 'arguments' => $arguments]);
$toolReference = $this->referenceProvider->getTool($toolName);
if (null === $toolReference) {
$this->logger->warning('Tool not found', ['name' => $toolName]);
throw new ToolNotFoundException($request);
}
try {
$result = $this->referenceHandler->handle($toolReference, $arguments);
/** @var TextContent[]|ImageContent[]|EmbeddedResource[]|AudioContent[] $formattedResult */
$formattedResult = $toolReference->formatResult($result);
$this->logger->debug('Tool executed successfully', [
'name' => $toolName,
'result_type' => \gettype($result),
]);
return new CallToolResult($formattedResult);
} catch (\Throwable $e) {
$this->logger->error('Tool execution failed', [
'name' => $toolName,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw new ToolCallException($request, $e);
}
}
}

View file

@ -1,29 +0,0 @@
<?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\Capability\Tool;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Request\CallToolRequest;
use Mcp\Schema\Result\CallToolResult;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
interface ToolCallerInterface
{
/**
* @throws ToolCallException if the tool execution fails
* @throws ToolNotFoundException if the tool is not found
*/
public function call(CallToolRequest $request): CallToolResult;
}

View file

@ -17,7 +17,6 @@ use Mcp\Schema\Content\Content;
use Mcp\Schema\Content\EmbeddedResource;
use Mcp\Schema\Content\ImageContent;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\JsonRpc\ResultInterface;
/**
@ -32,11 +31,6 @@ use Mcp\Schema\JsonRpc\ResultInterface;
* server does not support tool calls, or any other exceptional conditions,
* should be reported as an MCP error response.
*
* @phpstan-import-type TextContentData from TextContent
* @phpstan-import-type ImageContentData from ImageContent
* @phpstan-import-type AudioContentData from AudioContent
* @phpstan-import-type EmbeddedResourceData from EmbeddedResource
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
class CallToolResult implements ResultInterface
@ -44,8 +38,8 @@ class CallToolResult implements ResultInterface
/**
* Create a new CallToolResult.
*
* @param array<TextContent|ImageContent|AudioContent|EmbeddedResource> $content The content of the tool result
* @param bool $isError Whether the tool execution resulted in an error. If not set, this is assumed to be false (the call was successful).
* @param Content[] $content The content of the tool result
* @param bool $isError Whether the tool execution resulted in an error. If not set, this is assumed to be false (the call was successful).
*/
public function __construct(
public readonly array $content,
@ -61,7 +55,7 @@ class CallToolResult implements ResultInterface
/**
* Create a new CallToolResult with success status.
*
* @param array<TextContent|ImageContent|AudioContent|EmbeddedResource> $content The content of the tool result
* @param Content[] $content The content of the tool result
*/
public static function success(array $content): self
{
@ -71,7 +65,7 @@ class CallToolResult implements ResultInterface
/**
* Create a new CallToolResult with error status.
*
* @param array<TextContent|ImageContent|AudioContent|EmbeddedResource> $content The content of the tool result
* @param Content[] $content The content of the tool result
*/
public static function error(array $content): self
{
@ -80,7 +74,7 @@ class CallToolResult implements ResultInterface
/**
* @param array{
* content: array<TextContentData|ImageContentData|AudioContentData|EmbeddedResourceData>,
* content: array<mixed>,
* isError?: bool,
* } $data
*/
@ -107,7 +101,7 @@ class CallToolResult implements ResultInterface
/**
* @return array{
* content: array<TextContent|ImageContent|AudioContent|EmbeddedResource>,
* content: array<mixed>,
* isError: bool,
* }
*/

View file

@ -20,27 +20,24 @@ use Mcp\Capability\Discovery\Discoverer;
use Mcp\Capability\Discovery\DocBlockParser;
use Mcp\Capability\Discovery\HandlerResolver;
use Mcp\Capability\Discovery\SchemaGenerator;
use Mcp\Capability\Prompt\PromptGetter;
use Mcp\Capability\Prompt\PromptGetterInterface;
use Mcp\Capability\Registry;
use Mcp\Capability\Registry\Container;
use Mcp\Capability\Registry\ElementReference;
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\MessageFactory;
use Mcp\Schema\Annotations;
use Mcp\Schema\Implementation;
use Mcp\Schema\Prompt;
use Mcp\Schema\PromptArgument;
use Mcp\Schema\Resource;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\ServerCapabilities;
use Mcp\Schema\Tool;
use Mcp\Schema\ToolAnnotations;
use Mcp\Server;
use Mcp\Server\Handler\JsonRpcHandler;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\InMemorySessionStore;
use Mcp\Server\Session\SessionFactory;
use Mcp\Server\Session\SessionFactoryInterface;
@ -64,12 +61,6 @@ final class Builder
private ?CacheInterface $discoveryCache = null;
private ?ToolCallerInterface $toolCaller = null;
private ?ResourceReaderInterface $resourceReader = null;
private ?PromptGetterInterface $promptGetter = null;
private ?EventDispatcherInterface $eventDispatcher = null;
private ?ContainerInterface $container = null;
@ -84,6 +75,13 @@ final class Builder
private ?string $instructions = null;
private ?ServerCapabilities $explicitCapabilities = null;
/**
* @var array<int, MethodHandlerInterface>
*/
private array $customMethodHandlers = [];
/**
* @var array{
* handler: Handler,
@ -174,6 +172,40 @@ final class Builder
return $this;
}
/**
* Explicitly set server capabilities. If set, this overrides automatic detection.
*/
public function setCapabilities(ServerCapabilities $capabilities): self
{
$this->explicitCapabilities = $capabilities;
return $this;
}
/**
* Register a single custom method handler.
*/
public function addMethodHandler(MethodHandlerInterface $handler): self
{
$this->customMethodHandlers[] = $handler;
return $this;
}
/**
* Register multiple custom method handlers.
*
* @param iterable<int, MethodHandlerInterface> $handlers
*/
public function addMethodHandlers(iterable $handlers): self
{
foreach ($handlers as $handler) {
$this->customMethodHandlers[] = $handler;
}
return $this;
}
/**
* Provides a PSR-3 logger instance. Defaults to NullLogger.
*/
@ -191,27 +223,6 @@ final class Builder
return $this;
}
public function setToolCaller(ToolCallerInterface $toolCaller): self
{
$this->toolCaller = $toolCaller;
return $this;
}
public function setResourceReader(ResourceReaderInterface $resourceReader): self
{
$this->resourceReader = $resourceReader;
return $this;
}
public function setPromptGetter(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.
@ -333,49 +344,60 @@ final class Builder
public function build(): Server
{
$logger = $this->logger ?? new NullLogger();
$container = $this->container ?? new Container();
$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->registerCapabilities($registry, $logger);
if (null !== $this->discoveryBasePath) {
$discovery = new Discoverer($registry, $logger);
if (null !== $this->discoveryCache) {
$discovery = new CachedDiscoverer($discovery, $this->discoveryCache, $logger);
}
$discovery->discover($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs);
$this->performDiscovery($registry, $logger);
}
$sessionTtl = $this->sessionTtl ?? 3600;
$sessionFactory = $this->sessionFactory ?? new SessionFactory();
$sessionStore = $this->sessionStore ?? new InMemorySessionStore($sessionTtl);
$messageFactory = MessageFactory::make();
return new Server(
jsonRpcHandler: JsonRpcHandler::make(
registry: $registry,
referenceProvider: $registry,
configuration: new Configuration(
$this->serverInfo,
$registry->getCapabilities(),
$this->paginationLimit, $this->instructions,
),
toolCaller: $toolCaller,
resourceReader: $resourceReader,
promptGetter: $promptGetter,
sessionStore: $sessionStore,
sessionFactory: $sessionFactory,
logger: $logger,
),
$capabilities = $this->explicitCapabilities ?? $registry->getCapabilities();
$configuration = new Configuration($this->serverInfo, $capabilities, $this->paginationLimit, $this->instructions);
$referenceHandler = new ReferenceHandler($container);
$methodHandlers = array_merge($this->customMethodHandlers, [
new Handler\Request\PingHandler(),
new Handler\Request\InitializeHandler($configuration),
new Handler\Request\ListToolsHandler($registry, $this->paginationLimit),
new Handler\Request\CallToolHandler($registry, $referenceHandler, $logger),
new Handler\Request\ListResourcesHandler($registry, $this->paginationLimit),
new Handler\Request\ListResourceTemplatesHandler($registry, $this->paginationLimit),
new Handler\Request\ReadResourceHandler($registry, $referenceHandler, $logger),
new Handler\Request\ListPromptsHandler($registry, $this->paginationLimit),
new Handler\Request\GetPromptHandler($registry, $referenceHandler, $logger),
new Handler\Notification\InitializedHandler(),
]);
$jsonRpcHandler = new JsonRpcHandler(
methodHandlers: $methodHandlers,
messageFactory: $messageFactory,
sessionFactory: $sessionFactory,
sessionStore: $sessionStore,
logger: $logger,
);
return new Server($jsonRpcHandler, $logger);
}
private function performDiscovery(
Registry\ReferenceRegistryInterface $registry,
LoggerInterface $logger,
): void {
$discovery = new Discoverer($registry, $logger);
if (null !== $this->discoveryCache) {
$discovery = new CachedDiscoverer($discovery, $this->discoveryCache, $logger);
}
$discovery->discover($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs);
}
/**

View file

@ -11,12 +11,6 @@
namespace Mcp\Server\Handler;
use Mcp\Capability\Completion\Completer;
use Mcp\Capability\Prompt\PromptGetterInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Capability\Registry\ReferenceRegistryInterface;
use Mcp\Capability\Resource\ResourceReaderInterface;
use Mcp\Capability\Tool\ToolCallerInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\HandlerNotFoundException;
use Mcp\Exception\InvalidInputMessageException;
@ -27,8 +21,6 @@ use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\InitializeRequest;
use Mcp\Server\Configuration;
use Mcp\Server\Handler;
use Mcp\Server\Session\SessionFactoryInterface;
use Mcp\Server\Session\SessionInterface;
use Mcp\Server\Session\SessionStoreInterface;
@ -44,56 +36,15 @@ use Symfony\Component\Uid\Uuid;
class JsonRpcHandler
{
/**
* @var array<int, MethodHandlerInterface>
*/
private readonly array $methodHandlers;
/**
* @param iterable<int, MethodHandlerInterface> $methodHandlers
* @param array<int, MethodHandlerInterface> $methodHandlers
*/
public function __construct(
private readonly array $methodHandlers,
private readonly MessageFactory $messageFactory,
private readonly SessionFactoryInterface $sessionFactory,
private readonly SessionStoreInterface $sessionStore,
iterable $methodHandlers,
private readonly LoggerInterface $logger = new NullLogger(),
) {
$this->methodHandlers = $methodHandlers instanceof \Traversable ? iterator_to_array(
$methodHandlers,
) : $methodHandlers;
}
public static function make(
ReferenceRegistryInterface $registry,
ReferenceProviderInterface $referenceProvider,
Configuration $configuration,
ToolCallerInterface $toolCaller,
ResourceReaderInterface $resourceReader,
PromptGetterInterface $promptGetter,
SessionStoreInterface $sessionStore,
SessionFactoryInterface $sessionFactory,
LoggerInterface $logger = new NullLogger(),
int $paginationLimit = 50,
): self {
return new self(
messageFactory: MessageFactory::make(),
sessionFactory: $sessionFactory,
sessionStore: $sessionStore,
methodHandlers: [
new Notification\InitializedHandler(),
new Handler\Request\InitializeHandler($configuration),
new Handler\Request\PingHandler(),
new Handler\Request\ListPromptsHandler($referenceProvider, $paginationLimit),
new Handler\Request\GetPromptHandler($promptGetter),
new Handler\Request\ListResourcesHandler($referenceProvider, $paginationLimit),
new Handler\Request\ReadResourceHandler($resourceReader),
new Handler\Request\ListResourceTemplatesHandler($referenceProvider, $paginationLimit),
new Handler\Request\CallToolHandler($toolCaller, $logger),
new Handler\Request\ListToolsHandler($referenceProvider, $paginationLimit),
new Handler\Request\CompletionCompleteHandler(new Completer($referenceProvider)),
],
logger: $logger,
);
}
/**

View file

@ -11,12 +11,16 @@
namespace Mcp\Server\Handler\Request;
use Mcp\Capability\Tool\ToolCallerInterface;
use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
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\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface;
use Psr\Log\LoggerInterface;
@ -29,7 +33,8 @@ use Psr\Log\NullLogger;
final class CallToolHandler implements MethodHandlerInterface
{
public function __construct(
private readonly ToolCallerInterface $toolCaller,
private readonly ReferenceProviderInterface $referenceProvider,
private readonly ReferenceHandlerInterface $referenceHandler,
private readonly LoggerInterface $logger = new NullLogger(),
) {
}
@ -43,20 +48,44 @@ final class CallToolHandler implements MethodHandlerInterface
{
\assert($message instanceof CallToolRequest);
$toolName = $message->name;
$arguments = $message->arguments ?? [];
$this->logger->debug('Executing tool', ['name' => $toolName, 'arguments' => $arguments]);
try {
$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,
],
);
$reference = $this->referenceProvider->getTool($toolName);
if (null === $reference) {
throw new ToolNotFoundException($message);
}
$result = $this->referenceHandler->handle($reference, $arguments);
$formatted = $reference->formatResult($result);
$this->logger->debug('Tool executed successfully', [
'name' => $toolName,
'result_type' => \gettype($result),
]);
return new Response($message->getId(), new CallToolResult($formatted));
} catch (ToolNotFoundException $e) {
$this->logger->error('Tool not found', ['name' => $toolName]);
return new Error($message->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
} catch (ToolCallException|ExceptionInterface $e) {
$this->logger->error(\sprintf('Error while executing tool "%s": "%s".', $toolName, $e->getMessage()), [
'tool' => $toolName,
'arguments' => $arguments,
]);
return Error::forInternalError('Error while executing tool', $message->getId());
} catch (\Throwable $e) {
$this->logger->error('Unhandled error during tool execution', [
'name' => $toolName,
'exception' => $e->getMessage(),
]);
return Error::forInternalError('Error while executing tool', $message->getId());
}
return new Response($message->getId(), $content);
}
}

View file

@ -11,14 +11,16 @@
namespace Mcp\Server\Handler\Request;
use Mcp\Capability\Completion\CompleterInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Capability\Completion\ProviderInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\CompletionCompleteRequest;
use Mcp\Schema\Result\CompletionCompleteResult;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface;
use Psr\Container\ContainerInterface;
/**
* Handles completion/complete requests.
@ -28,7 +30,8 @@ use Mcp\Server\Session\SessionInterface;
final class CompletionCompleteHandler implements MethodHandlerInterface
{
public function __construct(
private readonly CompleterInterface $completer,
private readonly ReferenceProviderInterface $referenceProvider,
private readonly ?ContainerInterface $container = null,
) {
}
@ -41,12 +44,45 @@ final class CompletionCompleteHandler implements MethodHandlerInterface
{
\assert($message instanceof CompletionCompleteRequest);
try {
$result = $this->completer->complete($message);
} catch (ExceptionInterface) {
return Error::forInternalError('Error while handling completion request', $message->getId());
$name = $message->argument['name'] ?? '';
$value = $message->argument['value'] ?? '';
$reference = match ($message->ref->type) {
'ref/prompt' => $this->referenceProvider->getPrompt($message->ref->name),
'ref/resource' => $this->referenceProvider->getResourceTemplate($message->ref->uri),
default => null,
};
if (null === $reference) {
return new Response($message->getId(), new CompletionCompleteResult([]));
}
return new Response($message->getId(), $result);
$providers = $reference->completionProviders;
$provider = $providers[$name] ?? null;
if (null === $provider) {
return new Response($message->getId(), new CompletionCompleteResult([]));
}
if (\is_string($provider)) {
if (!class_exists($provider)) {
return Error::forInternalError('Invalid completion provider', $message->getId());
}
$provider = $this->container?->has($provider) ? $this->container->get($provider) : new $provider();
}
if (!$provider instanceof ProviderInterface) {
return Error::forInternalError('Invalid completion provider type', $message->getId());
}
try {
$completions = $provider->getCompletions($value);
$total = \count($completions);
$hasMore = $total > 100;
$paged = \array_slice($completions, 0, 100);
return new Response($message->getId(), new CompletionCompleteResult($paged, $total, $hasMore));
} catch (\Throwable) {
return Error::forInternalError('Error while handling completion request', $message->getId());
}
}
}

View file

@ -11,14 +11,20 @@
namespace Mcp\Server\Handler\Request;
use Mcp\Capability\Prompt\PromptGetterInterface;
use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\PromptGetException;
use Mcp\Exception\PromptNotFoundException;
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\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
@ -26,7 +32,9 @@ use Mcp\Server\Session\SessionInterface;
final class GetPromptHandler implements MethodHandlerInterface
{
public function __construct(
private readonly PromptGetterInterface $promptGetter,
private readonly ReferenceProviderInterface $referenceProvider,
private readonly ReferenceHandlerInterface $referenceHandler,
private readonly LoggerInterface $logger = new NullLogger(),
) {
}
@ -39,12 +47,32 @@ final class GetPromptHandler implements MethodHandlerInterface
{
\assert($message instanceof GetPromptRequest);
$promptName = $message->name;
$arguments = $message->arguments ?? [];
try {
$messages = $this->promptGetter->get($message);
} catch (ExceptionInterface) {
$reference = $this->referenceProvider->getPrompt($promptName);
if (null === $reference) {
throw new PromptNotFoundException($message);
}
$result = $this->referenceHandler->handle($reference, $arguments);
$formatted = $reference->formatResult($result);
return new Response($message->getId(), new GetPromptResult($formatted));
} catch (PromptNotFoundException $e) {
$this->logger->error('Prompt not found', ['prompt_name' => $promptName]);
return new Error($message->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
} catch (PromptGetException|ExceptionInterface $e) {
$this->logger->error('Error while handling prompt', ['prompt_name' => $promptName]);
return Error::forInternalError('Error while handling prompt', $message->getId());
} catch (\Throwable $e) {
$this->logger->error('Error while handling prompt', ['prompt_name' => $promptName]);
return Error::forInternalError('Error while handling prompt', $message->getId());
}
return new Response($message->getId(), $messages);
}
}

View file

@ -11,15 +11,19 @@
namespace Mcp\Server\Handler\Request;
use Mcp\Capability\Resource\ResourceReaderInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Capability\Registry\ResourceTemplateReference;
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\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
@ -27,7 +31,9 @@ use Mcp\Server\Session\SessionInterface;
final class ReadResourceHandler implements MethodHandlerInterface
{
public function __construct(
private readonly ResourceReaderInterface $resourceReader,
private readonly ReferenceProviderInterface $referenceProvider,
private readonly ReferenceHandlerInterface $referenceHandler,
private readonly LoggerInterface $logger = new NullLogger(),
) {
}
@ -40,14 +46,33 @@ final class ReadResourceHandler implements MethodHandlerInterface
{
\assert($message instanceof ReadResourceRequest);
$uri = $message->uri;
$this->logger->debug('Reading resource', ['uri' => $uri]);
try {
$contents = $this->resourceReader->read($message);
$reference = $this->referenceProvider->getResource($uri);
if (null === $reference) {
throw new ResourceNotFoundException($message);
}
$result = $this->referenceHandler->handle($reference, ['uri' => $uri]);
if ($reference instanceof ResourceTemplateReference) {
$formatted = $reference->formatResult($result, $uri, $reference->resourceTemplate->mimeType);
} else {
$formatted = $reference->formatResult($result, $uri, $reference->schema->mimeType);
}
return new Response($message->getId(), new ReadResourceResult($formatted));
} catch (ResourceNotFoundException $e) {
$this->logger->error('Resource not found', ['uri' => $uri]);
return new Error($message->getId(), Error::RESOURCE_NOT_FOUND, $e->getMessage());
} catch (ExceptionInterface) {
} catch (\Throwable $e) {
$this->logger->error(\sprintf('Error while reading resource "%s": "%s".', $uri, $e->getMessage()));
return Error::forInternalError('Error while reading resource', $message->getId());
}
return new Response($message->getId(), $contents);
}
}