refactor: registry loader (#111)
This commit is contained in:
parent
d347c84472
commit
e4a82f7474
4 changed files with 384 additions and 262 deletions
299
src/Capability/Registry/Loader/ArrayLoader.php
Normal file
299
src/Capability/Registry/Loader/ArrayLoader.php
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
<?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\Loader;
|
||||||
|
|
||||||
|
use Mcp\Capability\Attribute\CompletionProvider;
|
||||||
|
use Mcp\Capability\Completion\EnumCompletionProvider;
|
||||||
|
use Mcp\Capability\Completion\ListCompletionProvider;
|
||||||
|
use Mcp\Capability\Completion\ProviderInterface;
|
||||||
|
use Mcp\Capability\Discovery\DocBlockParser;
|
||||||
|
use Mcp\Capability\Discovery\HandlerResolver;
|
||||||
|
use Mcp\Capability\Discovery\SchemaGenerator;
|
||||||
|
use Mcp\Capability\Registry\ElementReference;
|
||||||
|
use Mcp\Capability\Registry\ReferenceRegistryInterface;
|
||||||
|
use Mcp\Exception\ConfigurationException;
|
||||||
|
use Mcp\Schema\Annotations;
|
||||||
|
use Mcp\Schema\Prompt;
|
||||||
|
use Mcp\Schema\PromptArgument;
|
||||||
|
use Mcp\Schema\Resource;
|
||||||
|
use Mcp\Schema\ResourceTemplate;
|
||||||
|
use Mcp\Schema\Tool;
|
||||||
|
use Mcp\Schema\ToolAnnotations;
|
||||||
|
use Mcp\Server\Handler;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||||
|
*
|
||||||
|
* @phpstan-import-type Handler from ElementReference
|
||||||
|
*/
|
||||||
|
final class ArrayLoader implements LoaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array{
|
||||||
|
* handler: Handler,
|
||||||
|
* name: ?string,
|
||||||
|
* description: ?string,
|
||||||
|
* annotations: ?ToolAnnotations,
|
||||||
|
* }[] $tools
|
||||||
|
* @param array{
|
||||||
|
* handler: Handler,
|
||||||
|
* uri: string,
|
||||||
|
* name: ?string,
|
||||||
|
* description: ?string,
|
||||||
|
* mimeType: ?string,
|
||||||
|
* size: int|null,
|
||||||
|
* annotations: ?Annotations,
|
||||||
|
* }[] $resources
|
||||||
|
* @param array{
|
||||||
|
* handler: Handler,
|
||||||
|
* uriTemplate: string,
|
||||||
|
* name: ?string,
|
||||||
|
* description: ?string,
|
||||||
|
* mimeType: ?string,
|
||||||
|
* annotations: ?Annotations,
|
||||||
|
* }[] $resourceTemplates
|
||||||
|
* @param array{
|
||||||
|
* handler: Handler,
|
||||||
|
* name: ?string,
|
||||||
|
* description: ?string,
|
||||||
|
* }[] $prompts
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private array $tools = [],
|
||||||
|
private array $resources = [],
|
||||||
|
private array $resourceTemplates = [],
|
||||||
|
private array $prompts = [],
|
||||||
|
private LoggerInterface $logger = new NullLogger(),
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function load(ReferenceRegistryInterface $registry): void
|
||||||
|
{
|
||||||
|
$docBlockParser = new DocBlockParser(logger: $this->logger);
|
||||||
|
$schemaGenerator = new SchemaGenerator($docBlockParser);
|
||||||
|
|
||||||
|
// Register Tools
|
||||||
|
foreach ($this->tools as $data) {
|
||||||
|
try {
|
||||||
|
$reflection = HandlerResolver::resolve($data['handler']);
|
||||||
|
|
||||||
|
if ($reflection instanceof \ReflectionFunction) {
|
||||||
|
$name = $data['name'] ?? 'closure_tool_'.spl_object_id($data['handler']);
|
||||||
|
$description = $data['description'] ?? null;
|
||||||
|
} else {
|
||||||
|
$classShortName = $reflection->getDeclaringClass()->getShortName();
|
||||||
|
$methodName = $reflection->getName();
|
||||||
|
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
|
||||||
|
|
||||||
|
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
|
||||||
|
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$inputSchema = $data['inputSchema'] ?? $schemaGenerator->generate($reflection);
|
||||||
|
|
||||||
|
$tool = new Tool($name, $inputSchema, $description, $data['annotations']);
|
||||||
|
$registry->registerTool($tool, $data['handler'], true);
|
||||||
|
|
||||||
|
$handlerDesc = $this->getHandlerDescription($data['handler']);
|
||||||
|
$this->logger->debug("Registered manual tool {$name} from handler {$handlerDesc}");
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register Resources
|
||||||
|
foreach ($this->resources as $data) {
|
||||||
|
try {
|
||||||
|
$reflection = HandlerResolver::resolve($data['handler']);
|
||||||
|
|
||||||
|
if ($reflection instanceof \ReflectionFunction) {
|
||||||
|
$name = $data['name'] ?? 'closure_resource_'.spl_object_id($data['handler']);
|
||||||
|
$description = $data['description'] ?? null;
|
||||||
|
} else {
|
||||||
|
$classShortName = $reflection->getDeclaringClass()->getShortName();
|
||||||
|
$methodName = $reflection->getName();
|
||||||
|
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
|
||||||
|
|
||||||
|
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
|
||||||
|
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri = $data['uri'];
|
||||||
|
$mimeType = $data['mimeType'];
|
||||||
|
$size = $data['size'];
|
||||||
|
$annotations = $data['annotations'];
|
||||||
|
|
||||||
|
$resource = new Resource($uri, $name, $description, $mimeType, $annotations, $size);
|
||||||
|
$registry->registerResource($resource, $data['handler'], true);
|
||||||
|
|
||||||
|
$handlerDesc = $this->getHandlerDescription($data['handler']);
|
||||||
|
$this->logger->debug("Registered manual resource {$name} from handler {$handlerDesc}");
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register Templates
|
||||||
|
foreach ($this->resourceTemplates as $data) {
|
||||||
|
try {
|
||||||
|
$reflection = HandlerResolver::resolve($data['handler']);
|
||||||
|
|
||||||
|
if ($reflection instanceof \ReflectionFunction) {
|
||||||
|
$name = $data['name'] ?? 'closure_template_'.spl_object_id($data['handler']);
|
||||||
|
$description = $data['description'] ?? null;
|
||||||
|
} else {
|
||||||
|
$classShortName = $reflection->getDeclaringClass()->getShortName();
|
||||||
|
$methodName = $reflection->getName();
|
||||||
|
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
|
||||||
|
|
||||||
|
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
|
||||||
|
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uriTemplate = $data['uriTemplate'];
|
||||||
|
$mimeType = $data['mimeType'];
|
||||||
|
$annotations = $data['annotations'];
|
||||||
|
|
||||||
|
$template = new ResourceTemplate($uriTemplate, $name, $description, $mimeType, $annotations);
|
||||||
|
$completionProviders = $this->getCompletionProviders($reflection);
|
||||||
|
$registry->registerResourceTemplate($template, $data['handler'], $completionProviders, true);
|
||||||
|
|
||||||
|
$handlerDesc = $this->getHandlerDescription($data['handler']);
|
||||||
|
$this->logger->debug("Registered manual template {$name} from handler {$handlerDesc}");
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register Prompts
|
||||||
|
foreach ($this->prompts as $data) {
|
||||||
|
try {
|
||||||
|
$reflection = HandlerResolver::resolve($data['handler']);
|
||||||
|
|
||||||
|
if ($reflection instanceof \ReflectionFunction) {
|
||||||
|
$name = $data['name'] ?? 'closure_prompt_'.spl_object_id($data['handler']);
|
||||||
|
$description = $data['description'] ?? null;
|
||||||
|
} else {
|
||||||
|
$classShortName = $reflection->getDeclaringClass()->getShortName();
|
||||||
|
$methodName = $reflection->getName();
|
||||||
|
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
|
||||||
|
|
||||||
|
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
|
||||||
|
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arguments = [];
|
||||||
|
$paramTags = $reflection instanceof \ReflectionMethod ? $docBlockParser->getParamTags(
|
||||||
|
$docBlockParser->parseDocBlock($reflection->getDocComment() ?? null),
|
||||||
|
) : [];
|
||||||
|
foreach ($reflection->getParameters() as $param) {
|
||||||
|
$reflectionType = $param->getType();
|
||||||
|
|
||||||
|
// Basic DI check (heuristic)
|
||||||
|
if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$paramTag = $paramTags['$'.$param->getName()] ?? null;
|
||||||
|
$arguments[] = new PromptArgument(
|
||||||
|
$param->getName(),
|
||||||
|
$paramTag ? trim((string) $paramTag->getDescription()) : null,
|
||||||
|
!$param->isOptional() && !$param->isDefaultValueAvailable(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$prompt = new Prompt($name, $description, $arguments);
|
||||||
|
$completionProviders = $this->getCompletionProviders($reflection);
|
||||||
|
$registry->registerPrompt($prompt, $data['handler'], $completionProviders, true);
|
||||||
|
|
||||||
|
$handlerDesc = $this->getHandlerDescription($data['handler']);
|
||||||
|
$this->logger->debug("Registered manual prompt {$name} from handler {$handlerDesc}");
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->debug('Manual element registration complete.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Handler $handler
|
||||||
|
*/
|
||||||
|
private function getHandlerDescription(\Closure|array|string $handler): string
|
||||||
|
{
|
||||||
|
if ($handler instanceof \Closure) {
|
||||||
|
return 'Closure';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_array($handler)) {
|
||||||
|
return \sprintf(
|
||||||
|
'%s::%s',
|
||||||
|
\is_object($handler[0]) ? $handler[0]::class : $handler[0],
|
||||||
|
$handler[1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, ProviderInterface>
|
||||||
|
*/
|
||||||
|
private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $reflection): array
|
||||||
|
{
|
||||||
|
$completionProviders = [];
|
||||||
|
foreach ($reflection->getParameters() as $param) {
|
||||||
|
$reflectionType = $param->getType();
|
||||||
|
if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$completionAttributes = $param->getAttributes(
|
||||||
|
CompletionProvider::class,
|
||||||
|
\ReflectionAttribute::IS_INSTANCEOF,
|
||||||
|
);
|
||||||
|
if (!empty($completionAttributes)) {
|
||||||
|
$attributeInstance = $completionAttributes[0]->newInstance();
|
||||||
|
|
||||||
|
if ($attributeInstance->provider) {
|
||||||
|
$completionProviders[$param->getName()] = $attributeInstance->provider;
|
||||||
|
} elseif ($attributeInstance->providerClass) {
|
||||||
|
$completionProviders[$param->getName()] = $attributeInstance->providerClass;
|
||||||
|
} elseif ($attributeInstance->values) {
|
||||||
|
$completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
|
||||||
|
} elseif ($attributeInstance->enum) {
|
||||||
|
$completionProviders[$param->getName()] = new EnumCompletionProvider($attributeInstance->enum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $completionProviders;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/Capability/Registry/Loader/DiscoveryLoader.php
Normal file
49
src/Capability/Registry/Loader/DiscoveryLoader.php
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?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\Loader;
|
||||||
|
|
||||||
|
use Mcp\Capability\Discovery\CachedDiscoverer;
|
||||||
|
use Mcp\Capability\Discovery\Discoverer;
|
||||||
|
use Mcp\Capability\Registry\ReferenceRegistryInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Psr\SimpleCache\CacheInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||||
|
*/
|
||||||
|
final class DiscoveryLoader implements LoaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string[] $scanDirs
|
||||||
|
* @param array|string[] $excludeDirs
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private string $basePath,
|
||||||
|
private array $scanDirs,
|
||||||
|
private array $excludeDirs,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
private ?CacheInterface $cache = null,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function load(ReferenceRegistryInterface $registry): void
|
||||||
|
{
|
||||||
|
// This now encapsulates the discovery process
|
||||||
|
$discoverer = new Discoverer($registry, $this->logger);
|
||||||
|
|
||||||
|
$cachedDiscoverer = $this->cache
|
||||||
|
? new CachedDiscoverer($discoverer, $this->cache, $this->logger)
|
||||||
|
: $discoverer;
|
||||||
|
|
||||||
|
$cachedDiscoverer->discover($this->basePath, $this->scanDirs, $this->excludeDirs);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/Capability/Registry/Loader/LoaderInterface.php
Normal file
22
src/Capability/Registry/Loader/LoaderInterface.php
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?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\Loader;
|
||||||
|
|
||||||
|
use Mcp\Capability\Registry\ReferenceRegistryInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||||
|
*/
|
||||||
|
interface LoaderInterface
|
||||||
|
{
|
||||||
|
public function load(ReferenceRegistryInterface $registry): void;
|
||||||
|
}
|
||||||
|
|
@ -11,29 +11,16 @@
|
||||||
|
|
||||||
namespace Mcp\Server;
|
namespace Mcp\Server;
|
||||||
|
|
||||||
use Mcp\Capability\Attribute\CompletionProvider;
|
|
||||||
use Mcp\Capability\Completion\EnumCompletionProvider;
|
|
||||||
use Mcp\Capability\Completion\ListCompletionProvider;
|
|
||||||
use Mcp\Capability\Completion\ProviderInterface;
|
|
||||||
use Mcp\Capability\Discovery\CachedDiscoverer;
|
|
||||||
use Mcp\Capability\Discovery\Discoverer;
|
|
||||||
use Mcp\Capability\Discovery\DocBlockParser;
|
|
||||||
use Mcp\Capability\Discovery\HandlerResolver;
|
|
||||||
use Mcp\Capability\Discovery\SchemaGenerator;
|
|
||||||
use Mcp\Capability\Registry;
|
use Mcp\Capability\Registry;
|
||||||
use Mcp\Capability\Registry\Container;
|
use Mcp\Capability\Registry\Container;
|
||||||
use Mcp\Capability\Registry\ElementReference;
|
use Mcp\Capability\Registry\ElementReference;
|
||||||
|
use Mcp\Capability\Registry\Loader\ArrayLoader;
|
||||||
|
use Mcp\Capability\Registry\Loader\DiscoveryLoader;
|
||||||
use Mcp\Capability\Registry\ReferenceHandler;
|
use Mcp\Capability\Registry\ReferenceHandler;
|
||||||
use Mcp\Exception\ConfigurationException;
|
|
||||||
use Mcp\JsonRpc\MessageFactory;
|
use Mcp\JsonRpc\MessageFactory;
|
||||||
use Mcp\Schema\Annotations;
|
use Mcp\Schema\Annotations;
|
||||||
use Mcp\Schema\Implementation;
|
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\ServerCapabilities;
|
||||||
use Mcp\Schema\Tool;
|
|
||||||
use Mcp\Schema\ToolAnnotations;
|
use Mcp\Schema\ToolAnnotations;
|
||||||
use Mcp\Server;
|
use Mcp\Server;
|
||||||
use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
|
use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
|
||||||
|
|
@ -376,13 +363,20 @@ final class Builder
|
||||||
$container = $this->container ?? new Container();
|
$container = $this->container ?? new Container();
|
||||||
$registry = new Registry($this->eventDispatcher, $logger);
|
$registry = new Registry($this->eventDispatcher, $logger);
|
||||||
|
|
||||||
$this->registerCapabilities($registry, $logger);
|
$loaders = [
|
||||||
if ($this->serverCapabilities) {
|
new ArrayLoader($this->tools, $this->resources, $this->resourceTemplates, $this->prompts, $logger),
|
||||||
$registry->setServerCapabilities($this->serverCapabilities);
|
];
|
||||||
}
|
|
||||||
|
|
||||||
if (null !== $this->discoveryBasePath) {
|
if (null !== $this->discoveryBasePath) {
|
||||||
$this->performDiscovery($registry, $logger);
|
$loaders[] = new DiscoveryLoader($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs, $logger, $this->discoveryCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($loaders as $loader) {
|
||||||
|
$loader->load($registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->serverCapabilities) {
|
||||||
|
$registry->setServerCapabilities($this->serverCapabilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
$sessionTtl = $this->sessionTtl ?? 3600;
|
$sessionTtl = $this->sessionTtl ?? 3600;
|
||||||
|
|
@ -421,246 +415,4 @@ final class Builder
|
||||||
|
|
||||||
return new Server($protocol, $logger);
|
return new Server($protocol, $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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to perform the actual registration based on stored data.
|
|
||||||
* Moved into the builder.
|
|
||||||
*/
|
|
||||||
private function registerCapabilities(
|
|
||||||
Registry\ReferenceRegistryInterface $registry,
|
|
||||||
LoggerInterface $logger = new NullLogger(),
|
|
||||||
): void {
|
|
||||||
if (empty($this->tools) && empty($this->resources) && empty($this->resourceTemplates) && empty($this->prompts)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$docBlockParser = new DocBlockParser(logger: $logger);
|
|
||||||
$schemaGenerator = new SchemaGenerator($docBlockParser);
|
|
||||||
|
|
||||||
// Register Tools
|
|
||||||
foreach ($this->tools as $data) {
|
|
||||||
try {
|
|
||||||
$reflection = HandlerResolver::resolve($data['handler']);
|
|
||||||
|
|
||||||
if ($reflection instanceof \ReflectionFunction) {
|
|
||||||
$name = $data['name'] ?? 'closure_tool_'.spl_object_id($data['handler']);
|
|
||||||
$description = $data['description'] ?? null;
|
|
||||||
} else {
|
|
||||||
$classShortName = $reflection->getDeclaringClass()->getShortName();
|
|
||||||
$methodName = $reflection->getName();
|
|
||||||
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
|
|
||||||
|
|
||||||
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
|
|
||||||
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$inputSchema = $data['inputSchema'] ?? $schemaGenerator->generate($reflection);
|
|
||||||
|
|
||||||
$tool = new Tool($name, $inputSchema, $description, $data['annotations']);
|
|
||||||
$registry->registerTool($tool, $data['handler'], true);
|
|
||||||
|
|
||||||
$handlerDesc = $this->getHandlerDescription($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],
|
|
||||||
);
|
|
||||||
throw new ConfigurationException("Error registering manual tool '{$data['name']}': {$e->getMessage()}", 0, $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register Resources
|
|
||||||
foreach ($this->resources as $data) {
|
|
||||||
try {
|
|
||||||
$reflection = HandlerResolver::resolve($data['handler']);
|
|
||||||
|
|
||||||
if ($reflection instanceof \ReflectionFunction) {
|
|
||||||
$name = $data['name'] ?? 'closure_resource_'.spl_object_id($data['handler']);
|
|
||||||
$description = $data['description'] ?? null;
|
|
||||||
} else {
|
|
||||||
$classShortName = $reflection->getDeclaringClass()->getShortName();
|
|
||||||
$methodName = $reflection->getName();
|
|
||||||
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
|
|
||||||
|
|
||||||
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
|
|
||||||
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$uri = $data['uri'];
|
|
||||||
$mimeType = $data['mimeType'];
|
|
||||||
$size = $data['size'];
|
|
||||||
$annotations = $data['annotations'];
|
|
||||||
|
|
||||||
$resource = new Resource($uri, $name, $description, $mimeType, $annotations, $size);
|
|
||||||
$registry->registerResource($resource, $data['handler'], true);
|
|
||||||
|
|
||||||
$handlerDesc = $this->getHandlerDescription($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],
|
|
||||||
);
|
|
||||||
throw new ConfigurationException("Error registering manual resource '{$data['uri']}': {$e->getMessage()}", 0, $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register Templates
|
|
||||||
foreach ($this->resourceTemplates as $data) {
|
|
||||||
try {
|
|
||||||
$reflection = HandlerResolver::resolve($data['handler']);
|
|
||||||
|
|
||||||
if ($reflection instanceof \ReflectionFunction) {
|
|
||||||
$name = $data['name'] ?? 'closure_template_'.spl_object_id($data['handler']);
|
|
||||||
$description = $data['description'] ?? null;
|
|
||||||
} else {
|
|
||||||
$classShortName = $reflection->getDeclaringClass()->getShortName();
|
|
||||||
$methodName = $reflection->getName();
|
|
||||||
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
|
|
||||||
|
|
||||||
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
|
|
||||||
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$uriTemplate = $data['uriTemplate'];
|
|
||||||
$mimeType = $data['mimeType'];
|
|
||||||
$annotations = $data['annotations'];
|
|
||||||
|
|
||||||
$template = new ResourceTemplate($uriTemplate, $name, $description, $mimeType, $annotations);
|
|
||||||
$completionProviders = $this->getCompletionProviders($reflection);
|
|
||||||
$registry->registerResourceTemplate($template, $data['handler'], $completionProviders, true);
|
|
||||||
|
|
||||||
$handlerDesc = $this->getHandlerDescription($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],
|
|
||||||
);
|
|
||||||
throw new ConfigurationException("Error registering manual resource template '{$data['uriTemplate']}': {$e->getMessage()}", 0, $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register Prompts
|
|
||||||
foreach ($this->prompts as $data) {
|
|
||||||
try {
|
|
||||||
$reflection = HandlerResolver::resolve($data['handler']);
|
|
||||||
|
|
||||||
if ($reflection instanceof \ReflectionFunction) {
|
|
||||||
$name = $data['name'] ?? 'closure_prompt_'.spl_object_id($data['handler']);
|
|
||||||
$description = $data['description'] ?? null;
|
|
||||||
} else {
|
|
||||||
$classShortName = $reflection->getDeclaringClass()->getShortName();
|
|
||||||
$methodName = $reflection->getName();
|
|
||||||
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
|
|
||||||
|
|
||||||
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
|
|
||||||
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$arguments = [];
|
|
||||||
$paramTags = $reflection instanceof \ReflectionMethod ? $docBlockParser->getParamTags(
|
|
||||||
$docBlockParser->parseDocBlock($reflection->getDocComment() ?? null),
|
|
||||||
) : [];
|
|
||||||
foreach ($reflection->getParameters() as $param) {
|
|
||||||
$reflectionType = $param->getType();
|
|
||||||
|
|
||||||
// Basic DI check (heuristic)
|
|
||||||
if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$paramTag = $paramTags['$'.$param->getName()] ?? null;
|
|
||||||
$arguments[] = new PromptArgument(
|
|
||||||
$param->getName(),
|
|
||||||
$paramTag ? trim((string) $paramTag->getDescription()) : null,
|
|
||||||
!$param->isOptional() && !$param->isDefaultValueAvailable(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$prompt = new Prompt($name, $description, $arguments);
|
|
||||||
$completionProviders = $this->getCompletionProviders($reflection);
|
|
||||||
$registry->registerPrompt($prompt, $data['handler'], $completionProviders, true);
|
|
||||||
|
|
||||||
$handlerDesc = $this->getHandlerDescription($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],
|
|
||||||
);
|
|
||||||
throw new ConfigurationException("Error registering manual prompt '{$data['name']}': {$e->getMessage()}", 0, $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$logger->debug('Manual element registration complete.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Handler $handler
|
|
||||||
*/
|
|
||||||
private function getHandlerDescription(\Closure|array|string $handler): string
|
|
||||||
{
|
|
||||||
if ($handler instanceof \Closure) {
|
|
||||||
return 'Closure';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (\is_array($handler)) {
|
|
||||||
return \sprintf(
|
|
||||||
'%s::%s',
|
|
||||||
\is_object($handler[0]) ? $handler[0]::class : $handler[0],
|
|
||||||
$handler[1],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (string) $handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, ProviderInterface>
|
|
||||||
*/
|
|
||||||
private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $reflection): array
|
|
||||||
{
|
|
||||||
$completionProviders = [];
|
|
||||||
foreach ($reflection->getParameters() as $param) {
|
|
||||||
$reflectionType = $param->getType();
|
|
||||||
if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$completionAttributes = $param->getAttributes(
|
|
||||||
CompletionProvider::class,
|
|
||||||
\ReflectionAttribute::IS_INSTANCEOF,
|
|
||||||
);
|
|
||||||
if (!empty($completionAttributes)) {
|
|
||||||
$attributeInstance = $completionAttributes[0]->newInstance();
|
|
||||||
|
|
||||||
if ($attributeInstance->provider) {
|
|
||||||
$completionProviders[$param->getName()] = $attributeInstance->provider;
|
|
||||||
} elseif ($attributeInstance->providerClass) {
|
|
||||||
$completionProviders[$param->getName()] = $attributeInstance->providerClass;
|
|
||||||
} elseif ($attributeInstance->values) {
|
|
||||||
$completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
|
|
||||||
} elseif ($attributeInstance->enum) {
|
|
||||||
$completionProviders[$param->getName()] = new EnumCompletionProvider($attributeInstance->enum);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $completionProviders;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue