From 6e0be3c67e5f1e8a072869b8a80cd92e5d2d6df8 Mon Sep 17 00:00:00 2001 From: Benoit Houdayer Date: Wed, 30 Jul 2025 17:56:42 +0200 Subject: [PATCH 1/2] allow discoverer to discover subclasses of annotations --- src/Utils/Discoverer.php | 98 ++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 53 deletions(-) diff --git a/src/Utils/Discoverer.php b/src/Utils/Discoverer.php index 61f3df4..fc7df98 100644 --- a/src/Utils/Discoverer.php +++ b/src/Utils/Discoverer.php @@ -191,60 +191,52 @@ private function processMethod(ReflectionMethod $method, array &$discoveredCount try { $instance = $attribute->newInstance(); - switch ($attributeClassName) { - case McpTool::class: - $docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null); - $name = $instance->name ?? ($methodName === '__invoke' ? $classShortName : $methodName); - $description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null; - $inputSchema = $this->schemaGenerator->generate($method); - $tool = Tool::make($name, $inputSchema, $description, $instance->annotations); - $this->registry->registerTool($tool, [$className, $methodName]); - $discoveredCount['tools']++; - break; - - case McpResource::class: - $docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null); - $name = $instance->name ?? ($methodName === '__invoke' ? $classShortName : $methodName); - $description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null; - $mimeType = $instance->mimeType; - $size = $instance->size; - $annotations = $instance->annotations; - $resource = Resource::make($instance->uri, $name, $description, $mimeType, $annotations, $size); - $this->registry->registerResource($resource, [$className, $methodName]); - $discoveredCount['resources']++; - break; - - case McpPrompt::class: - $docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null); - $name = $instance->name ?? ($methodName === '__invoke' ? $classShortName : $methodName); - $description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null; - $arguments = []; - $paramTags = $this->docBlockParser->getParamTags($docBlock); - foreach ($method->getParameters() as $param) { - $reflectionType = $param->getType(); - if ($reflectionType instanceof \ReflectionNamedType && ! $reflectionType->isBuiltin()) { - continue; - } - $paramTag = $paramTags['$' . $param->getName()] ?? null; - $arguments[] = PromptArgument::make($param->getName(), $paramTag ? trim((string) $paramTag->getDescription()) : null, ! $param->isOptional() && ! $param->isDefaultValueAvailable()); + if (is_a($instance, McpTool::class)) { + $docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null); + $name = $instance->name ?? ($methodName === '__invoke' ? $classShortName : $methodName); + $description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null; + $inputSchema = $this->schemaGenerator->generate($method); + $tool = Tool::make($name, $inputSchema, $description, $instance->annotations); + $this->registry->registerTool($tool, [$className, $methodName]); + $discoveredCount['tools']++; + } elseif (is_a($instance, McpResource::class)) { + $docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null); + $name = $instance->name ?? ($methodName === '__invoke' ? $classShortName : $methodName); + $description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null; + $mimeType = $instance->mimeType; + $size = $instance->size; + $annotations = $instance->annotations; + $resource = Resource::make($instance->uri, $name, $description, $mimeType, $annotations, $size); + $this->registry->registerResource($resource, [$className, $methodName]); + $discoveredCount['resources']++; + } elseif (is_a($instance, McpPrompt::class)) { + $docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null); + $name = $instance->name ?? ($methodName === '__invoke' ? $classShortName : $methodName); + $description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null; + $arguments = []; + $paramTags = $this->docBlockParser->getParamTags($docBlock); + foreach ($method->getParameters() as $param) { + $reflectionType = $param->getType(); + if ($reflectionType instanceof \ReflectionNamedType && ! $reflectionType->isBuiltin()) { + continue; } - $prompt = Prompt::make($name, $description, $arguments); - $completionProviders = $this->getCompletionProviders($method); - $this->registry->registerPrompt($prompt, [$className, $methodName], $completionProviders); - $discoveredCount['prompts']++; - break; - - case McpResourceTemplate::class: - $docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null); - $name = $instance->name ?? ($methodName === '__invoke' ? $classShortName : $methodName); - $description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null; - $mimeType = $instance->mimeType; - $annotations = $instance->annotations; - $resourceTemplate = ResourceTemplate::make($instance->uriTemplate, $name, $description, $mimeType, $annotations); - $completionProviders = $this->getCompletionProviders($method); - $this->registry->registerResourceTemplate($resourceTemplate, [$className, $methodName], $completionProviders); - $discoveredCount['resourceTemplates']++; - break; + $paramTag = $paramTags['$' . $param->getName()] ?? null; + $arguments[] = PromptArgument::make($param->getName(), $paramTag ? trim((string) $paramTag->getDescription()) : null, ! $param->isOptional() && ! $param->isDefaultValueAvailable()); + } + $prompt = Prompt::make($name, $description, $arguments); + $completionProviders = $this->getCompletionProviders($method); + $this->registry->registerPrompt($prompt, [$className, $methodName], $completionProviders); + $discoveredCount['prompts']++; + } elseif (is_a($instance, McpResourceTemplate::class)) { + $docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null); + $name = $instance->name ?? ($methodName === '__invoke' ? $classShortName : $methodName); + $description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null; + $mimeType = $instance->mimeType; + $annotations = $instance->annotations; + $resourceTemplate = ResourceTemplate::make($instance->uriTemplate, $name, $description, $mimeType, $annotations); + $completionProviders = $this->getCompletionProviders($method); + $this->registry->registerResourceTemplate($resourceTemplate, [$className, $methodName], $completionProviders); + $discoveredCount['resourceTemplates']++; } } catch (McpServerException $e) { $this->logger->error("Failed to process MCP attribute on {$className}::{$methodName}", ['attribute' => $attributeClassName, 'exception' => $e->getMessage(), 'trace' => $e->getPrevious() ? $e->getPrevious()->getTraceAsString() : $e->getTraceAsString()]); From 8d39b534ffe5bcb028e9bb00b9953bd2f5437597 Mon Sep 17 00:00:00 2001 From: Benoit Houdayer Date: Thu, 31 Jul 2025 12:01:52 +0200 Subject: [PATCH 2/2] make prompt, resource and template not final --- src/Attributes/McpPrompt.php | 2 +- src/Attributes/McpResource.php | 2 +- src/Attributes/McpResourceTemplate.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Attributes/McpPrompt.php b/src/Attributes/McpPrompt.php index 4e6d167..3f6dae5 100644 --- a/src/Attributes/McpPrompt.php +++ b/src/Attributes/McpPrompt.php @@ -9,7 +9,7 @@ * The method should return the prompt messages, potentially using arguments for templating. */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] -final class McpPrompt +class McpPrompt { /** * @param ?string $name Overrides the prompt name (defaults to method name). diff --git a/src/Attributes/McpResource.php b/src/Attributes/McpResource.php index f367301..7911578 100644 --- a/src/Attributes/McpResource.php +++ b/src/Attributes/McpResource.php @@ -10,7 +10,7 @@ * Used primarily for the 'resources/list' discovery. */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] -final class McpResource +class McpResource { /** * @param string $uri The specific URI identifying this resource instance. Must be unique within the server. diff --git a/src/Attributes/McpResourceTemplate.php b/src/Attributes/McpResourceTemplate.php index 9448d26..3a5d6ac 100644 --- a/src/Attributes/McpResourceTemplate.php +++ b/src/Attributes/McpResourceTemplate.php @@ -10,7 +10,7 @@ * This is informational, used for 'resources/templates/list'. */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] -final class McpResourceTemplate +class McpResourceTemplate { /** * @param string $uriTemplate The URI template string (RFC 6570).