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
36 changed files with 4656 additions and 334 deletions
|
|
@ -12,7 +12,7 @@
|
|||
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;
|
||||
|
|
@ -20,7 +20,7 @@ use Mcp\Schema\Result\CallToolResult;
|
|||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
class ExampleTool implements MetadataInterface, ToolExecutorInterface
|
||||
class ExampleTool implements MetadataInterface, ToolCallerInterface
|
||||
{
|
||||
public function call(CallToolRequest $request): CallToolResult
|
||||
{
|
||||
|
|
|
|||
|
|
@ -367,131 +367,23 @@ parameters:
|
|||
path: examples/08-schema-showcase-streamable/server.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\CallToolHandler constructor expects Mcp\\Capability\\Registry, Mcp\\Capability\\ToolChain given\.$#'
|
||||
message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\ListPromptsHandler constructor expects Mcp\\Capability\\Registry\\ReferenceProviderInterface, Mcp\\Capability\\PromptChain given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: examples/09-standalone-cli/src/Builder.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\GetPromptHandler constructor expects Mcp\\Capability\\Registry, Mcp\\Capability\\PromptChain given\.$#'
|
||||
message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\ListResourcesHandler constructor expects Mcp\\Capability\\Registry\\ReferenceProviderInterface, Mcp\\Capability\\ResourceChain given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: examples/09-standalone-cli/src/Builder.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\ListPromptsHandler constructor expects Mcp\\Capability\\Registry, Mcp\\Capability\\PromptChain given\.$#'
|
||||
message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\ListToolsHandler constructor expects Mcp\\Capability\\Registry\\ReferenceProviderInterface, Mcp\\Capability\\ToolChain given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: examples/09-standalone-cli/src/Builder.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\ListResourcesHandler constructor expects Mcp\\Capability\\Registry, Mcp\\Capability\\ResourceChain given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: examples/09-standalone-cli/src/Builder.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\ListToolsHandler constructor expects Mcp\\Capability\\Registry, Mcp\\Capability\\ToolChain given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: examples/09-standalone-cli/src/Builder.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\ReadResourceHandler constructor expects Mcp\\Capability\\Registry, Mcp\\Capability\\ResourceChain given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: examples/09-standalone-cli/src/Builder.php
|
||||
|
||||
-
|
||||
message: '#^Call to protected method formatResult\(\) of class Mcp\\Capability\\Registry\\ResourceReference\.$#'
|
||||
identifier: method.protected
|
||||
count: 1
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^Cannot import type alias CallableArray\: type alias does not exist in Mcp\\Capability\\Registry\\ElementReference\.$#'
|
||||
identifier: typeAlias.notFound
|
||||
count: 1
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^Method Mcp\\Capability\\Registry\:\:handleCallTool\(\) has parameter \$arguments with no value type specified in iterable type array\.$#'
|
||||
identifier: missingType.iterableValue
|
||||
count: 1
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^Method Mcp\\Capability\\Registry\:\:handleCallTool\(\) return type has no value type specified in iterable type array\.$#'
|
||||
identifier: missingType.iterableValue
|
||||
count: 1
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^Method Mcp\\Capability\\Registry\:\:handleGetPrompt\(\) has parameter \$arguments with no value type specified in iterable type array\.$#'
|
||||
identifier: missingType.iterableValue
|
||||
count: 1
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^Method Mcp\\Capability\\Registry\:\:registerPrompt\(\) has parameter \$handler with no value type specified in iterable type array\.$#'
|
||||
identifier: missingType.iterableValue
|
||||
count: 1
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^Method Mcp\\Capability\\Registry\:\:registerResource\(\) has parameter \$handler with no value type specified in iterable type array\.$#'
|
||||
identifier: missingType.iterableValue
|
||||
count: 1
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^Method Mcp\\Capability\\Registry\:\:registerResourceTemplate\(\) has parameter \$handler with no value type specified in iterable type array\.$#'
|
||||
identifier: missingType.iterableValue
|
||||
count: 1
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^Method Mcp\\Capability\\Registry\:\:registerTool\(\) has parameter \$handler with no value type specified in iterable type array\.$#'
|
||||
identifier: missingType.iterableValue
|
||||
count: 1
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param for parameter \$handler with type \(callable\)\|Mcp\\Capability\\CallableArray\|string is not subtype of native type array\|\(callable\)\|string\.$#'
|
||||
identifier: parameter.phpDocType
|
||||
count: 4
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \$handler of method Mcp\\Capability\\Registry\:\:registerPrompt\(\) has invalid type Mcp\\Capability\\CallableArray\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \$handler of method Mcp\\Capability\\Registry\:\:registerResource\(\) has invalid type Mcp\\Capability\\CallableArray\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \$handler of method Mcp\\Capability\\Registry\:\:registerResourceTemplate\(\) has invalid type Mcp\\Capability\\CallableArray\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \$handler of method Mcp\\Capability\\Registry\:\:registerTool\(\) has invalid type Mcp\\Capability\\CallableArray\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Capability/Registry.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Mcp\\Capability\\Registry\\ResourceTemplateReference\:\:handle\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: src/Capability/Registry/ResourceTemplateReference.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @return with type array is incompatible with native type object\.$#'
|
||||
identifier: return.phpDocType
|
||||
|
|
@ -505,11 +397,23 @@ parameters:
|
|||
path: src/Schema/Result/ReadResourceResult.php
|
||||
|
||||
-
|
||||
message: '#^Method Mcp\\Capability\\Registry\:\:getPrompts\(\) invoked with 2 parameters, 0 required\.$#'
|
||||
message: '#^Result of && is always false\.$#'
|
||||
identifier: booleanAnd.alwaysFalse
|
||||
count: 1
|
||||
path: src/Server/RequestHandler/ListResourcesHandler.php
|
||||
|
||||
-
|
||||
message: '#^Method Mcp\\Capability\\Registry\\ReferenceProviderInterface\:\:getPrompts\(\) invoked with 2 parameters, 0 required\.$#'
|
||||
identifier: arguments.count
|
||||
count: 1
|
||||
path: src/Server/RequestHandler/ListPromptsHandler.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Mcp\\Capability\\Registry\\ResourceTemplateReference\:\:handle\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: src/Capability/Registry/ResourceTemplateReference.php
|
||||
|
||||
-
|
||||
message: '#^Result of && is always false\.$#'
|
||||
identifier: booleanAnd.alwaysFalse
|
||||
|
|
@ -523,29 +427,23 @@ parameters:
|
|||
path: src/Server/RequestHandler/ListPromptsHandler.php
|
||||
|
||||
-
|
||||
message: '#^Method Mcp\\Capability\\Registry\:\:getResources\(\) invoked with 2 parameters, 0 required\.$#'
|
||||
message: '#^Method Mcp\\Capability\\Registry\\ReferenceProviderInterface\:\:getResources\(\) invoked with 2 parameters, 0 required\.$#'
|
||||
identifier: arguments.count
|
||||
count: 1
|
||||
path: src/Server/RequestHandler/ListResourcesHandler.php
|
||||
|
||||
-
|
||||
message: '#^Result of && is always false\.$#'
|
||||
identifier: booleanAnd.alwaysFalse
|
||||
count: 1
|
||||
path: src/Server/RequestHandler/ListResourcesHandler.php
|
||||
|
||||
-
|
||||
message: '#^Strict comparison using \!\=\= between null and null will always evaluate to false\.$#'
|
||||
identifier: notIdentical.alwaysFalse
|
||||
count: 1
|
||||
path: src/Server/RequestHandler/ListResourcesHandler.php
|
||||
|
||||
-
|
||||
message: '#^Method Mcp\\Capability\\Registry\:\:getTools\(\) invoked with 2 parameters, 0 required\.$#'
|
||||
message: '#^Method Mcp\\Capability\\Registry\\ReferenceProviderInterface\:\:getTools\(\) invoked with 2 parameters, 0 required\.$#'
|
||||
identifier: arguments.count
|
||||
count: 1
|
||||
path: src/Server/RequestHandler/ListToolsHandler.php
|
||||
|
||||
-
|
||||
message: '#^Strict comparison using \!\=\= between null and null will always evaluate to false\.$#'
|
||||
identifier: notIdentical.alwaysFalse
|
||||
count: 1
|
||||
path: src/Server/RequestHandler/ListResourcesHandler.php
|
||||
|
||||
-
|
||||
message: '#^Result of && is always false\.$#'
|
||||
identifier: booleanAnd.alwaysFalse
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use Mcp\Capability\Attribute\McpTool;
|
|||
use Mcp\Capability\Prompt\Completion\EnumCompletionProvider;
|
||||
use Mcp\Capability\Prompt\Completion\ListCompletionProvider;
|
||||
use Mcp\Capability\Prompt\Completion\ProviderInterface;
|
||||
use Mcp\Capability\Registry;
|
||||
use Mcp\Capability\Registry\ReferenceRegistryInterface;
|
||||
use Mcp\Exception\ExceptionInterface;
|
||||
use Mcp\Schema\Prompt;
|
||||
use Mcp\Schema\PromptArgument;
|
||||
|
|
@ -44,7 +44,7 @@ use Symfony\Component\Finder\SplFileInfo;
|
|||
class Discoverer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Registry $registry,
|
||||
private readonly ReferenceRegistryInterface $registry,
|
||||
private readonly LoggerInterface $logger = new NullLogger(),
|
||||
private ?DocBlockParser $docBlockParser = null,
|
||||
private ?SchemaGenerator $schemaGenerator = null,
|
||||
|
|
|
|||
69
src/Capability/Prompt/PromptGetter.php
Normal file
69
src/Capability/Prompt/PromptGetter.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,9 @@
|
|||
|
||||
namespace Mcp\Capability;
|
||||
|
||||
use Mcp\Capability\Registry\ElementReference;
|
||||
use Mcp\Capability\Registry\PromptReference;
|
||||
use Mcp\Capability\Registry\ReferenceHandler;
|
||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
||||
use Mcp\Capability\Registry\ReferenceRegistryInterface;
|
||||
use Mcp\Capability\Registry\ResourceReference;
|
||||
use Mcp\Capability\Registry\ResourceTemplateReference;
|
||||
use Mcp\Capability\Registry\ToolReference;
|
||||
|
|
@ -21,9 +21,6 @@ use Mcp\Event\PromptListChangedEvent;
|
|||
use Mcp\Event\ResourceListChangedEvent;
|
||||
use Mcp\Event\ResourceTemplateListChangedEvent;
|
||||
use Mcp\Event\ToolListChangedEvent;
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
use Mcp\Schema\Content\PromptMessage;
|
||||
use Mcp\Schema\Content\ResourceContents;
|
||||
use Mcp\Schema\Prompt;
|
||||
use Mcp\Schema\Resource;
|
||||
use Mcp\Schema\ResourceTemplate;
|
||||
|
|
@ -34,11 +31,14 @@ use Psr\Log\LoggerInterface;
|
|||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type CallableArray from ElementReference
|
||||
* Registry implementation that manages MCP element registration and access.
|
||||
* Implements both ReferenceProvider (for access) and ReferenceRegistry (for registration)
|
||||
* following the Interface Segregation Principle.
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
* @author Pavel Buchnev <butschster@gmail.com>
|
||||
*/
|
||||
class Registry
|
||||
final class Registry implements ReferenceProviderInterface, ReferenceRegistryInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, ToolReference>
|
||||
|
|
@ -61,7 +61,6 @@ class Registry
|
|||
private array $resourceTemplates = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly ReferenceHandler $referenceHandler = new ReferenceHandler(),
|
||||
private readonly ?EventDispatcherInterface $eventDispatcher = null,
|
||||
private readonly LoggerInterface $logger = new NullLogger(),
|
||||
) {
|
||||
|
|
@ -74,28 +73,27 @@ class Registry
|
|||
}
|
||||
|
||||
return new ServerCapabilities(
|
||||
tools: true, // [] !== $this->tools,
|
||||
tools: [] !== $this->tools,
|
||||
toolsListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
|
||||
resources: [] !== $this->resources || [] !== $this->resourceTemplates,
|
||||
resourcesSubscribe: false,
|
||||
resourcesListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
|
||||
prompts: [] !== $this->prompts,
|
||||
promptsListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
|
||||
logging: false, // true,
|
||||
logging: false,
|
||||
completions: true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable|CallableArray|string $handler
|
||||
*/
|
||||
public function registerTool(Tool $tool, callable|array|string $handler, bool $isManual = false): void
|
||||
{
|
||||
$toolName = $tool->name;
|
||||
$existing = $this->tools[$toolName] ?? null;
|
||||
|
||||
if ($existing && !$isManual && $existing->isManual) {
|
||||
$this->logger->debug("Ignoring discovered tool '{$toolName}' as it conflicts with a manually registered one.");
|
||||
$this->logger->debug(
|
||||
"Ignoring discovered tool '{$toolName}' as it conflicts with a manually registered one.",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -105,16 +103,15 @@ class Registry
|
|||
$this->eventDispatcher?->dispatch(new ToolListChangedEvent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable|CallableArray|string $handler
|
||||
*/
|
||||
public function registerResource(Resource $resource, callable|array|string $handler, bool $isManual = false): void
|
||||
{
|
||||
$uri = $resource->uri;
|
||||
$existing = $this->resources[$uri] ?? null;
|
||||
|
||||
if ($existing && !$isManual && $existing->isManual) {
|
||||
$this->logger->debug("Ignoring discovered resource '{$uri}' as it conflicts with a manually registered one.");
|
||||
$this->logger->debug(
|
||||
"Ignoring discovered resource '{$uri}' as it conflicts with a manually registered one.",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -124,10 +121,6 @@ class Registry
|
|||
$this->eventDispatcher?->dispatch(new ResourceListChangedEvent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable|CallableArray|string $handler
|
||||
* @param array<string, class-string|object> $completionProviders
|
||||
*/
|
||||
public function registerResourceTemplate(
|
||||
ResourceTemplate $template,
|
||||
callable|array|string $handler,
|
||||
|
|
@ -138,20 +131,23 @@ class Registry
|
|||
$existing = $this->resourceTemplates[$uriTemplate] ?? null;
|
||||
|
||||
if ($existing && !$isManual && $existing->isManual) {
|
||||
$this->logger->debug("Ignoring discovered template '{$uriTemplate}' as it conflicts with a manually registered one.");
|
||||
$this->logger->debug(
|
||||
"Ignoring discovered template '{$uriTemplate}' as it conflicts with a manually registered one.",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->resourceTemplates[$uriTemplate] = new ResourceTemplateReference($template, $handler, $isManual, $completionProviders);
|
||||
$this->resourceTemplates[$uriTemplate] = new ResourceTemplateReference(
|
||||
$template,
|
||||
$handler,
|
||||
$isManual,
|
||||
$completionProviders,
|
||||
);
|
||||
|
||||
$this->eventDispatcher?->dispatch(new ResourceTemplateListChangedEvent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable|CallableArray|string $handler
|
||||
* @param array<string, class-string|object> $completionProviders
|
||||
*/
|
||||
public function registerPrompt(
|
||||
Prompt $prompt,
|
||||
callable|array|string $handler,
|
||||
|
|
@ -162,7 +158,9 @@ class Registry
|
|||
$existing = $this->prompts[$promptName] ?? null;
|
||||
|
||||
if ($existing && !$isManual && $existing->isManual) {
|
||||
$this->logger->debug("Ignoring discovered prompt '{$promptName}' as it conflicts with a manually registered one.");
|
||||
$this->logger->debug(
|
||||
"Ignoring discovered prompt '{$promptName}' as it conflicts with a manually registered one.",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -172,20 +170,6 @@ class Registry
|
|||
$this->eventDispatcher?->dispatch(new PromptListChangedEvent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any elements (manual or discovered) are currently registered.
|
||||
*/
|
||||
public function hasElements(): bool
|
||||
{
|
||||
return !empty($this->tools)
|
||||
|| !empty($this->resources)
|
||||
|| !empty($this->prompts)
|
||||
|| !empty($this->resourceTemplates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear discovered elements from registry.
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$clearCount = 0;
|
||||
|
|
@ -220,43 +204,15 @@ class Registry
|
|||
}
|
||||
}
|
||||
|
||||
public function handleCallTool(string $name, array $arguments): array
|
||||
{
|
||||
$reference = $this->getTool($name);
|
||||
|
||||
if (null === $reference) {
|
||||
throw new InvalidArgumentException(\sprintf('Tool "%s" is not registered.', $name));
|
||||
}
|
||||
|
||||
return $reference->formatResult(
|
||||
$this->referenceHandler->handle($reference, $arguments)
|
||||
);
|
||||
}
|
||||
|
||||
public function getTool(string $name): ?ToolReference
|
||||
{
|
||||
return $this->tools[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ResourceContents[]
|
||||
*/
|
||||
public function handleReadResource(string $uri): array
|
||||
{
|
||||
$reference = $this->getResource($uri);
|
||||
|
||||
if (null === $reference) {
|
||||
throw new InvalidArgumentException(\sprintf('Resource "%s" is not registered.', $uri));
|
||||
}
|
||||
|
||||
return $reference->formatResult(
|
||||
$this->referenceHandler->handle($reference, ['uri' => $uri]),
|
||||
$uri,
|
||||
);
|
||||
}
|
||||
|
||||
public function getResource(string $uri, bool $includeTemplates = true): ResourceReference|ResourceTemplateReference|null
|
||||
{
|
||||
public function getResource(
|
||||
string $uri,
|
||||
bool $includeTemplates = true,
|
||||
): ResourceReference|ResourceTemplateReference|null {
|
||||
$registration = $this->resources[$uri] ?? null;
|
||||
if ($registration) {
|
||||
return $registration;
|
||||
|
|
@ -282,54 +238,37 @@ class Registry
|
|||
return $this->resourceTemplates[$uriTemplate] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PromptMessage[]
|
||||
*/
|
||||
public function handleGetPrompt(string $name, ?array $arguments): array
|
||||
{
|
||||
$reference = $this->getPrompt($name);
|
||||
|
||||
if (null === $reference) {
|
||||
throw new InvalidArgumentException(\sprintf('Prompt "%s" is not registered.', $name));
|
||||
}
|
||||
|
||||
return $reference->formatResult(
|
||||
$this->referenceHandler->handle($reference, $arguments)
|
||||
);
|
||||
}
|
||||
|
||||
public function getPrompt(string $name): ?PromptReference
|
||||
{
|
||||
return $this->prompts[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Tool>
|
||||
*/
|
||||
public function getTools(): array
|
||||
{
|
||||
return array_map(fn (ToolReference $tool) => $tool->tool, $this->tools);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, resource>
|
||||
*/
|
||||
public function getResources(): array
|
||||
{
|
||||
return array_map(fn (ResourceReference $resource) => $resource->schema, $this->resources);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Prompt>
|
||||
*/
|
||||
public function getPrompts(): array
|
||||
{
|
||||
return array_map(fn (PromptReference $prompt) => $prompt->prompt, $this->prompts);
|
||||
}
|
||||
|
||||
/** @return array<string, ResourceTemplate> */
|
||||
public function getResourceTemplates(): array
|
||||
{
|
||||
return array_map(fn ($template) => $template->resourceTemplate, $this->resourceTemplates);
|
||||
return array_map(fn (ResourceTemplateReference $template) => $template->resourceTemplate,
|
||||
$this->resourceTemplates);
|
||||
}
|
||||
|
||||
public function hasElements(): bool
|
||||
{
|
||||
return !empty($this->tools)
|
||||
|| !empty($this->resources)
|
||||
|| !empty($this->prompts)
|
||||
|| !empty($this->resourceTemplates);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use Psr\Container\ContainerInterface;
|
|||
/**
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class ReferenceHandler
|
||||
final class ReferenceHandler implements ReferenceHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ?ContainerInterface $container = null,
|
||||
|
|
|
|||
34
src/Capability/Registry/ReferenceHandlerInterface.php
Normal file
34
src/Capability/Registry/ReferenceHandlerInterface.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?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\Registry;
|
||||
|
||||
/**
|
||||
* Interface for handling execution of MCP elements.
|
||||
* Allows custom implementations of element execution logic.
|
||||
*
|
||||
* @author Pavel Buchnev <butschster@gmail.com>
|
||||
*/
|
||||
interface ReferenceHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Handles execution of an MCP element reference.
|
||||
*
|
||||
* @param ElementReference $reference the element reference to execute
|
||||
* @param array<string, mixed> $arguments arguments to pass to the handler
|
||||
*
|
||||
* @return mixed the result of the element execution
|
||||
*
|
||||
* @throws \Mcp\Exception\InvalidArgumentException if the handler is invalid
|
||||
* @throws \Mcp\Exception\RegistryException if execution fails
|
||||
*/
|
||||
public function handle(ElementReference $reference, array $arguments): mixed;
|
||||
}
|
||||
78
src/Capability/Registry/ReferenceProviderInterface.php
Normal file
78
src/Capability/Registry/ReferenceProviderInterface.php
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?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\Registry;
|
||||
|
||||
use Mcp\Schema\Prompt;
|
||||
use Mcp\Schema\ResourceTemplate;
|
||||
use Mcp\Schema\Tool;
|
||||
|
||||
/**
|
||||
* Interface for providing access to registered MCP elements.
|
||||
* Separates the concern of accessing elements from registering them.
|
||||
*
|
||||
* @author Pavel Buchnev <butschster@gmail.com>
|
||||
*/
|
||||
interface ReferenceProviderInterface
|
||||
{
|
||||
/**
|
||||
* Gets a tool reference by name.
|
||||
*/
|
||||
public function getTool(string $name): ?ToolReference;
|
||||
|
||||
/**
|
||||
* Gets a resource reference by URI (includes template matching if enabled).
|
||||
*/
|
||||
public function getResource(string $uri, bool $includeTemplates = true): ResourceReference|ResourceTemplateReference|null;
|
||||
|
||||
/**
|
||||
* Gets a resource template reference by URI template.
|
||||
*/
|
||||
public function getResourceTemplate(string $uriTemplate): ?ResourceTemplateReference;
|
||||
|
||||
/**
|
||||
* Gets a prompt reference by name.
|
||||
*/
|
||||
public function getPrompt(string $name): ?PromptReference;
|
||||
|
||||
/**
|
||||
* Gets all registered tools.
|
||||
*
|
||||
* @return array<string, Tool>
|
||||
*/
|
||||
public function getTools(): array;
|
||||
|
||||
/**
|
||||
* Gets all registered resources.
|
||||
*
|
||||
* @return array<string, \Mcp\Schema\Resource>
|
||||
*/
|
||||
public function getResources(): array;
|
||||
|
||||
/**
|
||||
* Gets all registered prompts.
|
||||
*
|
||||
* @return array<string, Prompt>
|
||||
*/
|
||||
public function getPrompts(): array;
|
||||
|
||||
/**
|
||||
* Gets all registered resource templates.
|
||||
*
|
||||
* @return array<string, ResourceTemplate>
|
||||
*/
|
||||
public function getResourceTemplates(): array;
|
||||
|
||||
/**
|
||||
* Checks if any elements (manual or discovered) are currently registered.
|
||||
*/
|
||||
public function hasElements(): bool;
|
||||
}
|
||||
79
src/Capability/Registry/ReferenceRegistryInterface.php
Normal file
79
src/Capability/Registry/ReferenceRegistryInterface.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?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\Registry;
|
||||
|
||||
use Mcp\Schema\Prompt;
|
||||
use Mcp\Schema\Resource;
|
||||
use Mcp\Schema\ResourceTemplate;
|
||||
use Mcp\Schema\ServerCapabilities;
|
||||
use Mcp\Schema\Tool;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type Handler from ElementReference
|
||||
*
|
||||
* Interface for registering MCP elements.
|
||||
* Separates the concern of registering elements from accessing them.
|
||||
*
|
||||
* @author Pavel Buchnev <butschster@gmail.com>
|
||||
*/
|
||||
interface ReferenceRegistryInterface
|
||||
{
|
||||
/**
|
||||
* Gets server capabilities based on registered elements.
|
||||
*/
|
||||
public function getCapabilities(): ServerCapabilities;
|
||||
|
||||
/**
|
||||
* Registers a tool with its handler.
|
||||
*
|
||||
* @param Handler $handler
|
||||
*/
|
||||
public function registerTool(Tool $tool, callable|array|string $handler, bool $isManual = false): void;
|
||||
|
||||
/**
|
||||
* Registers a resource with its handler.
|
||||
*
|
||||
* @param Handler $handler
|
||||
*/
|
||||
public function registerResource(Resource $resource, callable|array|string $handler, bool $isManual = false): void;
|
||||
|
||||
/**
|
||||
* Registers a resource template with its handler and completion providers.
|
||||
*
|
||||
* @param Handler $handler
|
||||
* @param array<string, class-string|object> $completionProviders
|
||||
*/
|
||||
public function registerResourceTemplate(
|
||||
ResourceTemplate $template,
|
||||
callable|array|string $handler,
|
||||
array $completionProviders = [],
|
||||
bool $isManual = false,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Registers a prompt with its handler and completion providers.
|
||||
*
|
||||
* @param Handler $handler
|
||||
* @param array<string, class-string|object> $completionProviders
|
||||
*/
|
||||
public function registerPrompt(
|
||||
Prompt $prompt,
|
||||
callable|array|string $handler,
|
||||
array $completionProviders = [],
|
||||
bool $isManual = false,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Clear discovered elements from registry.
|
||||
*/
|
||||
public function clear(): void;
|
||||
}
|
||||
|
|
@ -55,7 +55,8 @@ class ResourceTemplateReference extends ElementReference
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the resource template.
|
||||
* @deprecated
|
||||
* Gets the resource template
|
||||
*
|
||||
* @return array<int, ResourceContents> array of ResourceContents objects
|
||||
*/
|
||||
|
|
@ -162,7 +163,7 @@ class ResourceTemplateReference extends ElementReference
|
|||
* - array: Converted to JSON if MIME type is application/json or contains 'json'
|
||||
* For other MIME types, will try to convert to JSON with a warning
|
||||
*/
|
||||
protected function formatResult(mixed $readResult, string $uri, ?string $mimeType = null): array
|
||||
public function formatResult(mixed $readResult, string $uri, ?string $mimeType = null): array
|
||||
{
|
||||
if ($readResult instanceof ResourceContents) {
|
||||
return [$readResult];
|
||||
|
|
|
|||
|
|
@ -1,30 +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;
|
||||
|
||||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class ResourceReadResult
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $result,
|
||||
public readonly string $uri,
|
||||
|
||||
/**
|
||||
* @var "text"|"blob"
|
||||
*/
|
||||
public readonly string $type = 'text',
|
||||
public readonly string $mimeType = 'text/plain',
|
||||
) {
|
||||
}
|
||||
}
|
||||
68
src/Capability/Resource/ResourceReader.php
Normal file
68
src/Capability/Resource/ResourceReader.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/Capability/Tool/ToolCaller.php
Normal file
81
src/Capability/Tool/ToolCaller.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/Capability/Tool/ToolCallerInterface.php
Normal file
29
src/Capability/Tool/ToolCallerInterface.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?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;
|
||||
}
|
||||
|
|
@ -14,9 +14,9 @@ namespace Mcp\Capability;
|
|||
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;
|
||||
|
|
@ -26,7 +26,7 @@ use Mcp\Schema\Result\CallToolResult;
|
|||
*
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
class ToolChain implements ToolExecutorInterface, CollectionInterface
|
||||
class ToolChain implements ToolCallerInterface, CollectionInterface
|
||||
{
|
||||
public function __construct(
|
||||
/**
|
||||
|
|
@ -63,11 +63,11 @@ class ToolChain implements ToolExecutorInterface, CollectionInterface
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
src/Exception/ToolCallException.php
Normal file
27
src/Exception/ToolCallException.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?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\Exception;
|
||||
|
||||
use Mcp\Schema\Request\CallToolRequest;
|
||||
|
||||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class ToolCallException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
public function __construct(
|
||||
public readonly CallToolRequest $request,
|
||||
?\Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct(\sprintf('Tool call "%s" failed with error: "%s".', $request->name, $previous?->getMessage() ?? ''), previous: $previous);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,11 @@
|
|||
|
||||
namespace Mcp\JsonRpc;
|
||||
|
||||
use Mcp\Capability\Registry;
|
||||
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;
|
||||
|
|
@ -46,28 +50,34 @@ class Handler
|
|||
iterable $methodHandlers,
|
||||
private readonly LoggerInterface $logger = new NullLogger(),
|
||||
) {
|
||||
$this->methodHandlers = $methodHandlers instanceof \Traversable ? iterator_to_array($methodHandlers) : $methodHandlers;
|
||||
$this->methodHandlers = $methodHandlers instanceof \Traversable ? iterator_to_array(
|
||||
$methodHandlers,
|
||||
) : $methodHandlers;
|
||||
}
|
||||
|
||||
public static function make(
|
||||
Registry $registry,
|
||||
ReferenceRegistryInterface $registry,
|
||||
ReferenceProviderInterface $referenceProvider,
|
||||
Implementation $implementation,
|
||||
ToolCallerInterface $toolCaller,
|
||||
ResourceReaderInterface $resourceReader,
|
||||
PromptGetterInterface $promptGetter,
|
||||
LoggerInterface $logger = new NullLogger(),
|
||||
): self {
|
||||
return new self(
|
||||
MessageFactory::make(),
|
||||
[
|
||||
messageFactory: MessageFactory::make(),
|
||||
methodHandlers: [
|
||||
new NotificationHandler\InitializedHandler(),
|
||||
new RequestHandler\InitializeHandler($registry->getCapabilities(), $implementation),
|
||||
new RequestHandler\PingHandler(),
|
||||
new RequestHandler\ListPromptsHandler($registry),
|
||||
new RequestHandler\GetPromptHandler($registry),
|
||||
new RequestHandler\ListResourcesHandler($registry),
|
||||
new RequestHandler\ReadResourceHandler($registry),
|
||||
new RequestHandler\CallToolHandler($registry, $logger),
|
||||
new RequestHandler\ListToolsHandler($registry),
|
||||
new RequestHandler\ListPromptsHandler($referenceProvider),
|
||||
new RequestHandler\GetPromptHandler($promptGetter),
|
||||
new RequestHandler\ListResourcesHandler($referenceProvider),
|
||||
new RequestHandler\ReadResourceHandler($resourceReader),
|
||||
new RequestHandler\CallToolHandler($toolCaller, $logger),
|
||||
new RequestHandler\ListToolsHandler($referenceProvider),
|
||||
],
|
||||
$logger,
|
||||
logger: $logger,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +117,8 @@ class Handler
|
|||
} catch (\DomainException) {
|
||||
yield null;
|
||||
} catch (NotFoundExceptionInterface $e) {
|
||||
$this->logger->warning(\sprintf('Failed to create response: %s', $e->getMessage()), ['exception' => $e]);
|
||||
$this->logger->warning(\sprintf('Failed to create response: %s', $e->getMessage()), ['exception' => $e],
|
||||
);
|
||||
|
||||
yield $this->encodeResponse(Error::forMethodNotFound($e->getMessage()));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class ServerCapabilities implements \JsonSerializable
|
|||
* completions?: mixed,
|
||||
* prompts?: array{listChanged?: bool}|object,
|
||||
* resources?: array{listChanged?: bool, subscribe?: bool}|object,
|
||||
* tools?: object,
|
||||
* tools?: object|array{listChanged?: bool},
|
||||
* experimental?: array<string, mixed>,
|
||||
* } $data
|
||||
*/
|
||||
|
|
@ -106,7 +106,7 @@ class ServerCapabilities implements \JsonSerializable
|
|||
promptsListChanged: $promptsListChanged,
|
||||
logging: $loggingEnabled,
|
||||
completions: $completionsEnabled,
|
||||
experimental: $data['experimental'] ?? null
|
||||
experimental: $data['experimental'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue