diff --git a/src/Capability/Registry/Loader/ArrayLoader.php b/src/Capability/Registry/Loader/ArrayLoader.php new file mode 100644 index 00000000..7420bc75 --- /dev/null +++ b/src/Capability/Registry/Loader/ArrayLoader.php @@ -0,0 +1,299 @@ + + * + * @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 + */ + 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; + } +} diff --git a/src/Capability/Registry/Loader/DiscoveryLoader.php b/src/Capability/Registry/Loader/DiscoveryLoader.php new file mode 100644 index 00000000..2fadaadf --- /dev/null +++ b/src/Capability/Registry/Loader/DiscoveryLoader.php @@ -0,0 +1,49 @@ + + */ +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); + } +} diff --git a/src/Capability/Registry/Loader/LoaderInterface.php b/src/Capability/Registry/Loader/LoaderInterface.php new file mode 100644 index 00000000..5b7ba6a9 --- /dev/null +++ b/src/Capability/Registry/Loader/LoaderInterface.php @@ -0,0 +1,22 @@ + + */ +interface LoaderInterface +{ + public function load(ReferenceRegistryInterface $registry): void; +} diff --git a/src/Server/Builder.php b/src/Server/Builder.php index 95ab9ea1..886e61a8 100644 --- a/src/Server/Builder.php +++ b/src/Server/Builder.php @@ -11,29 +11,16 @@ 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\Container; use Mcp\Capability\Registry\ElementReference; +use Mcp\Capability\Registry\Loader\ArrayLoader; +use Mcp\Capability\Registry\Loader\DiscoveryLoader; use Mcp\Capability\Registry\ReferenceHandler; -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\Notification\NotificationHandlerInterface; @@ -376,13 +363,20 @@ public function build(): Server $container = $this->container ?? new Container(); $registry = new Registry($this->eventDispatcher, $logger); - $this->registerCapabilities($registry, $logger); - if ($this->serverCapabilities) { - $registry->setServerCapabilities($this->serverCapabilities); - } + $loaders = [ + new ArrayLoader($this->tools, $this->resources, $this->resourceTemplates, $this->prompts, $logger), + ]; 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; @@ -421,246 +415,4 @@ public function build(): Server 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 - */ - 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; - } }