From f8121bc038d5c408a981f46b6610d237a379ce93 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Sun, 9 Nov 2025 17:40:06 +0100 Subject: [PATCH 1/2] Add support for icons on explicit builder registration and attributes --- docs/mcp-elements.md | 7 +++ docs/server-builder.md | 51 ++++++++++++++++++- examples/discovery-calculator/McpElements.php | 7 ++- src/Capability/Attribute/McpPrompt.php | 4 ++ src/Capability/Attribute/McpResource.php | 3 ++ src/Capability/Attribute/McpTool.php | 3 ++ src/Capability/Discovery/Discoverer.php | 28 ++++++---- .../Registry/Loader/ArrayLoader.php | 47 +++++++++-------- src/Server/Builder.php | 41 ++++++++++++--- tests/Inspector/InspectorSnapshotTestCase.php | 3 +- ...iscoveryCalculatorTest-resources_list.json | 9 ++++ ...dioDiscoveryCalculatorTest-tools_list.json | 9 ++++ 12 files changed, 173 insertions(+), 39 deletions(-) diff --git a/docs/mcp-elements.md b/docs/mcp-elements.md index 1bb2690d..1846a7dd 100644 --- a/docs/mcp-elements.md +++ b/docs/mcp-elements.md @@ -72,6 +72,8 @@ class Calculator - **`name`** (optional): Tool identifier. Defaults to method name if not provided. - **`description`** (optional): Tool description. Defaults to docblock summary if not provided, otherwise uses method name. - **`annotations`** (optional): `ToolAnnotations` object for additional metadata. +- **`icons`** (optional): Array of `Icon` objects for visual representation. +- **`meta`** (optional): Arbitrary key-value pairs for custom metadata. **Priority for name/description**: Attribute parameters → DocBlock content → Method name @@ -217,6 +219,9 @@ class ConfigProvider - **`description`** (optional): Resource description. Defaults to docblock summary if not provided. - **`mimeType`** (optional): MIME type of the resource content. - **`size`** (optional): Size in bytes if known. +- **`annotations`** (optional): Additional metadata. +- **`icons`** (optional): Array of `Icon` objects for visual representation. +- **`meta`** (optional): Arbitrary key-value pairs for custom metadata. **Standard Protocol URI Schemes**: `https://` (web resources), `file://` (filesystem), `git://` (version control). **Custom schemes**: `config://`, `data://`, `db://`, `api://` or any RFC 3986 compliant scheme. @@ -399,6 +404,8 @@ class PromptGenerator - **`name`** (optional): Prompt identifier. Defaults to method name if not provided. - **`description`** (optional): Prompt description. Defaults to docblock summary if not provided. +- **`icons`** (optional): Array of `Icon` objects for visual representation. +- **`meta`** (optional): Arbitrary key-value pairs for custom metadata. ### Prompt Return Values diff --git a/docs/server-builder.md b/docs/server-builder.md index ac131374..ad0204df 100644 --- a/docs/server-builder.md +++ b/docs/server-builder.md @@ -52,14 +52,25 @@ final `Server` instance ready for use. Set the server's identity with name, version, and optional description: ```php +use Mcp\Schema\Icon; +use Mcp\Server; + $server = Server::builder() - ->setServerInfo('Calculator Server', '1.2.0', 'Advanced mathematical calculations'); + ->setServerInfo( + name: 'Calculator Server', + version: '1.2.0', + description: 'Advanced mathematical calculations', + icons: [new Icon('https://example.com/icon.png', 'image/png', ['64x64'])], + websiteUrl: 'https://example.com + '); ``` **Parameters:** - `$name` (string): The server name - `$version` (string): Version string (semantic versioning recommended) - `$description` (string|null): Optional description +- `$icons` (Icon[]|null): Optional array of server icons +- `$websiteUrl` (string|null): Optional server website URL ### Pagination Limit @@ -263,6 +274,16 @@ $server = Server::builder() ); ``` +#### Parameters + +- `handler` (callable|string): The tool handler +- `name` (string|null): Optional tool name +- `description` (string|null): Optional tool description +- `annotations` (ToolAnnotations|null): Optional annotations for the tool +- `inputSchema` (array|null): Optional input schema for the tool +- `icons` (Icon[]|null): Optional array of icons for the tool +- `meta` (array|null): Optional metadata for the tool + ### Manual Resource Registration Register static resources: @@ -278,6 +299,18 @@ $server = Server::builder() ); ``` +#### Parameters + +- `handler` (callable|string): The resource handler +- `uri` (string): The resource URI +- `name` (string|null): Optional resource name +- `description` (string|null): Optional resource description +- `mimeType` (string|null): Optional MIME type of the resource +- `size` (int|null): Optional size of the resource in bytes +- `annotations` (Annotations|null): Optional annotations for the resource +- `icons` (Icon[]|null): Optional array of icons for the resource +- `meta` (array|null): Optional metadata for the resource + ### Manual Resource Template Registration Register dynamic resources with URI templates: @@ -293,6 +326,15 @@ $server = Server::builder() ); ``` +#### Parameters + +- `handler` (callable|string): The resource template handler +- `uriTemplate` (string): The resource URI template +- `name` (string|null): Optional resource template name +- `description` (string|null): Optional resource template description +- `mimeType` (string|null): Optional MIME type of the resource +- `annotations` (Annotations|null): Optional annotations for the resource template + ### Manual Prompt Registration Register prompt generators: @@ -306,6 +348,13 @@ $server = Server::builder() ); ``` +#### Parameters + +- `handler` (callable|string): The prompt handler +- `name` (string|null): Optional prompt name +- `description` (string|null): Optional prompt description +- `icons` (Icon[]|null): Optional array of icons for the prompt + **Note:** `name` and `description` are optional for all manual registrations. If not provided, they will be derived from the handler's method name and docblock. diff --git a/examples/discovery-calculator/McpElements.php b/examples/discovery-calculator/McpElements.php index 6276de57..972534d1 100644 --- a/examples/discovery-calculator/McpElements.php +++ b/examples/discovery-calculator/McpElements.php @@ -14,6 +14,7 @@ use Mcp\Capability\Attribute\McpResource; use Mcp\Capability\Attribute\McpTool; use Mcp\Exception\ToolCallException; +use Mcp\Schema\Icon; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; @@ -47,7 +48,10 @@ public function __construct( * * @return float the result of the calculation */ - #[McpTool(name: 'calculate')] + #[McpTool( + name: 'calculate', + icons: [new Icon('https://www.svgrepo.com/show/530644/calculator.svg', 'image/svg+xml', ['any'])], + )] public function calculate(float $a, float $b, string $operation): float { $this->logger->info(\sprintf('Calculating: %f %s %f', $a, $operation, $b)); @@ -92,6 +96,7 @@ public function calculate(float $a, float $b, string $operation): float name: 'calculator_config', description: 'Current settings for the calculator tool (precision, allow_negative).', mimeType: 'application/json', + icons: [new Icon('https://www.svgrepo.com/show/529867/settings.svg', 'image/svg+xml', ['any'])], )] public function getConfiguration(): array { diff --git a/src/Capability/Attribute/McpPrompt.php b/src/Capability/Attribute/McpPrompt.php index 8488e608..0737c18d 100644 --- a/src/Capability/Attribute/McpPrompt.php +++ b/src/Capability/Attribute/McpPrompt.php @@ -11,6 +11,8 @@ namespace Mcp\Capability\Attribute; +use Mcp\Schema\Icon; + /** * Marks a PHP method as an MCP Prompt generator. * The method should return the prompt messages, potentially using arguments for templating. @@ -23,11 +25,13 @@ final class McpPrompt /** * @param ?string $name overrides the prompt name (defaults to method name) * @param ?string $description Optional description of the prompt. Defaults to method DocBlock summary. + * @param ?Icon[] $icons Optional list of icon URLs representing the prompt * @param ?array $meta Optional metadata */ public function __construct( public ?string $name = null, public ?string $description = null, + public ?array $icons = null, public ?array $meta = null, ) { } diff --git a/src/Capability/Attribute/McpResource.php b/src/Capability/Attribute/McpResource.php index 86b33078..2dcf85e6 100644 --- a/src/Capability/Attribute/McpResource.php +++ b/src/Capability/Attribute/McpResource.php @@ -12,6 +12,7 @@ namespace Mcp\Capability\Attribute; use Mcp\Schema\Annotations; +use Mcp\Schema\Icon; /** * Marks a PHP class as representing or handling a specific MCP Resource instance. @@ -29,6 +30,7 @@ final class McpResource * @param ?string $mimeType the MIME type, if known and constant for this resource * @param ?int $size the size in bytes, if known and constant * @param Annotations|null $annotations optional annotations describing the resource + * @param ?Icon[] $icons Optional list of icon URLs representing the resource * @param ?array $meta Optional metadata */ public function __construct( @@ -38,6 +40,7 @@ public function __construct( public ?string $mimeType = null, public ?int $size = null, public ?Annotations $annotations = null, + public ?array $icons = null, public ?array $meta = null, ) { } diff --git a/src/Capability/Attribute/McpTool.php b/src/Capability/Attribute/McpTool.php index e5cab26b..85dbc225 100644 --- a/src/Capability/Attribute/McpTool.php +++ b/src/Capability/Attribute/McpTool.php @@ -11,6 +11,7 @@ namespace Mcp\Capability\Attribute; +use Mcp\Schema\Icon; use Mcp\Schema\ToolAnnotations; /** @@ -23,12 +24,14 @@ class McpTool * @param string|null $name The name of the tool (defaults to the method name) * @param string|null $description The description of the tool (defaults to the DocBlock/inferred) * @param ToolAnnotations|null $annotations Optional annotations describing tool behavior + * @param ?Icon[] $icons Optional list of icon URLs representing the tool * @param ?array $meta Optional metadata */ public function __construct( public ?string $name = null, public ?string $description = null, public ?ToolAnnotations $annotations = null, + public ?array $icons = null, public ?array $meta = null, ) { } diff --git a/src/Capability/Discovery/Discoverer.php b/src/Capability/Discovery/Discoverer.php index 2ad632c2..88ec4117 100644 --- a/src/Capability/Discovery/Discoverer.php +++ b/src/Capability/Discovery/Discoverer.php @@ -222,8 +222,14 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun $name = $instance->name ?? ('__invoke' === $methodName ? $classShortName : $methodName); $description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null; $inputSchema = $this->schemaGenerator->generate($method); - $meta = $instance->meta ?? null; - $tool = new Tool($name, $inputSchema, $description, $instance->annotations, meta: $meta); + $tool = new Tool( + $name, + $inputSchema, + $description, + $instance->annotations, + $instance->icons, + $instance->meta, + ); $tools[$name] = new ToolReference($tool, [$className, $methodName], false); ++$discoveredCount['tools']; break; @@ -232,11 +238,16 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun $docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null); $name = $instance->name ?? ('__invoke' === $methodName ? $classShortName : $methodName); $description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null; - $mimeType = $instance->mimeType; - $size = $instance->size; - $annotations = $instance->annotations; - $meta = $instance->meta; - $resource = new Resource($instance->uri, $name, $description, $mimeType, $annotations, $size, $meta); + $resource = new Resource( + $instance->uri, + $name, + $description, + $instance->mimeType, + $instance->annotations, + $instance->size, + $instance->icons, + $instance->meta, + ); $resources[$instance->uri] = new ResourceReference($resource, [$className, $methodName], false); ++$discoveredCount['resources']; @@ -256,8 +267,7 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun $paramTag = $paramTags['$'.$param->getName()] ?? null; $arguments[] = new PromptArgument($param->getName(), $paramTag ? trim((string) $paramTag->getDescription()) : null, !$param->isOptional() && !$param->isDefaultValueAvailable()); } - $meta = $instance->meta ?? null; - $prompt = new Prompt($name, $description, $arguments, $meta); + $prompt = new Prompt($name, $description, $arguments, $instance->icons, $instance->meta); $completionProviders = $this->getCompletionProviders($method); $prompts[$name] = new PromptReference($prompt, [$className, $methodName], false, $completionProviders); ++$discoveredCount['prompts']; diff --git a/src/Capability/Registry/Loader/ArrayLoader.php b/src/Capability/Registry/Loader/ArrayLoader.php index b784fd47..03b2f852 100644 --- a/src/Capability/Registry/Loader/ArrayLoader.php +++ b/src/Capability/Registry/Loader/ArrayLoader.php @@ -22,6 +22,7 @@ use Mcp\Capability\Registry\ReferenceRegistryInterface; use Mcp\Exception\ConfigurationException; use Mcp\Schema\Annotations; +use Mcp\Schema\Icon; use Mcp\Schema\Prompt; use Mcp\Schema\PromptArgument; use Mcp\Schema\Resource; @@ -45,7 +46,7 @@ final class ArrayLoader implements LoaderInterface * name: ?string, * description: ?string, * annotations: ?ToolAnnotations, - * icons: ?array, + * icons: ?Icon[], * meta: ?array * }[] $tools * @param array{ @@ -56,6 +57,7 @@ final class ArrayLoader implements LoaderInterface * mimeType: ?string, * size: int|null, * annotations: ?Annotations, + * icons: ?Icon[], * meta: ?array * }[] $resources * @param array{ @@ -71,14 +73,15 @@ final class ArrayLoader implements LoaderInterface * handler: Handler, * name: ?string, * description: ?string, + * icons: ?Icon[], * meta: ?array * }[] $prompts */ public function __construct( - private array $tools = [], - private array $resources = [], - private array $resourceTemplates = [], - private array $prompts = [], + private readonly array $tools = [], + private readonly array $resources = [], + private readonly array $resourceTemplates = [], + private readonly array $prompts = [], private LoggerInterface $logger = new NullLogger(), ) { } @@ -145,13 +148,16 @@ public function load(ReferenceRegistryInterface $registry): void $description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null; } - $uri = $data['uri']; - $mimeType = $data['mimeType']; - $size = $data['size']; - $annotations = $data['annotations']; - $meta = $data['meta']; - - $resource = new Resource($uri, $name, $description, $mimeType, $annotations, $size, $meta); + $resource = new Resource( + $data['uri'], + $name, + $description, + $data['mimeType'], + $data['annotations'], + $data['size'], + $data['icons'], + $data['meta'], + ); $registry->registerResource($resource, $data['handler'], true); $handlerDesc = $this->getHandlerDescription($data['handler']); @@ -182,12 +188,14 @@ public function load(ReferenceRegistryInterface $registry): void $description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null; } - $uriTemplate = $data['uriTemplate']; - $mimeType = $data['mimeType']; - $annotations = $data['annotations']; - $meta = $data['meta']; - - $template = new ResourceTemplate($uriTemplate, $name, $description, $mimeType, $annotations, $meta); + $template = new ResourceTemplate( + $data['uriTemplate'], + $name, + $description, + $data['mimeType'], + $data['annotations'], + $data['meta'], + ); $completionProviders = $this->getCompletionProviders($reflection); $registry->registerResourceTemplate($template, $data['handler'], $completionProviders, true); @@ -238,8 +246,7 @@ public function load(ReferenceRegistryInterface $registry): void !$param->isOptional() && !$param->isDefaultValueAvailable(), ); } - $meta = $data['meta']; - $prompt = new Prompt($name, $description, $arguments, $meta); + $prompt = new Prompt($name, $description, $arguments, $data['icons'], $data['meta']); $completionProviders = $this->getCompletionProviders($reflection); $registry->registerPrompt($prompt, $data['handler'], $completionProviders, true); diff --git a/src/Server/Builder.php b/src/Server/Builder.php index ac8ec578..a0ab63e9 100644 --- a/src/Server/Builder.php +++ b/src/Server/Builder.php @@ -83,7 +83,7 @@ final class Builder * name: ?string, * description: ?string, * annotations: ?ToolAnnotations, - * icons: ?array, + * icons: ?Icon[], * meta: ?array * }[] */ @@ -98,6 +98,7 @@ final class Builder * mimeType: ?string, * size: int|null, * annotations: ?Annotations, + * icons: ?Icon[], * meta: ?array * }[] */ @@ -121,6 +122,7 @@ final class Builder * handler: Handler, * name: ?string, * description: ?string, + * icons: ?Icon[], * meta: ?array * }[] */ @@ -316,7 +318,7 @@ public function setProtocolVersion(ProtocolVersion $protocolVersion): self * * @param Handler $handler * @param array|null $inputSchema - * @param Icon[]|null $icons + * @param ?Icon[] $icons * @param array|null $meta */ public function addTool( @@ -328,7 +330,15 @@ public function addTool( ?array $icons = null, ?array $meta = null, ): self { - $this->tools[] = compact('handler', 'name', 'description', 'annotations', 'inputSchema', 'icons', 'meta'); + $this->tools[] = compact( + 'handler', + 'name', + 'description', + 'annotations', + 'inputSchema', + 'icons', + 'meta', + ); return $this; } @@ -337,6 +347,7 @@ public function addTool( * Manually registers a resource handler. * * @param Handler $handler + * @param ?Icon[] $icons * @param array|null $meta */ public function addResource( @@ -347,9 +358,20 @@ public function addResource( ?string $mimeType = null, ?int $size = null, ?Annotations $annotations = null, + ?array $icons = null, ?array $meta = null, ): self { - $this->resources[] = compact('handler', 'uri', 'name', 'description', 'mimeType', 'size', 'annotations', 'meta'); + $this->resources[] = compact( + 'handler', + 'uri', + 'name', + 'description', + 'mimeType', + 'size', + 'annotations', + 'icons', + 'meta', + ); return $this; } @@ -383,10 +405,15 @@ public function addResourceTemplate( * Manually registers a prompt handler. * * @param Handler $handler + * @param ?Icon[] $icons */ - public function addPrompt(\Closure|array|string $handler, ?string $name = null, ?string $description = null): self - { - $this->prompts[] = compact('handler', 'name', 'description'); + public function addPrompt( + \Closure|array|string $handler, + ?string $name = null, + ?string $description = null, + ?array $icons = null, + ): self { + $this->prompts[] = compact('handler', 'name', 'description', 'icons'); return $this; } diff --git a/tests/Inspector/InspectorSnapshotTestCase.php b/tests/Inspector/InspectorSnapshotTestCase.php index b28a9c1b..444c5164 100644 --- a/tests/Inspector/InspectorSnapshotTestCase.php +++ b/tests/Inspector/InspectorSnapshotTestCase.php @@ -109,7 +109,8 @@ public function testOutputMatchesSnapshot( $expected = file_get_contents($snapshotFile); - $this->assertJsonStringEqualsJsonString($expected, $normalizedOutput); + $message = \sprintf('Output does not match snapshot "%s".', $snapshotFile); + $this->assertJsonStringEqualsJsonString($expected, $normalizedOutput, $message); } protected function normalizeTestOutput(string $output, ?string $testName = null): string diff --git a/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-resources_list.json b/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-resources_list.json index a40f3489..e72548b5 100644 --- a/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-resources_list.json +++ b/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-resources_list.json @@ -4,6 +4,15 @@ "name": "calculator_config", "uri": "config://calculator/settings", "description": "Current settings for the calculator tool (precision, allow_negative).", + "icons": [ + { + "mimeType": "image/svg+xml", + "sizes": [ + "any" + ], + "src": "https://www.svgrepo.com/show/529867/settings.svg" + } + ], "mimeType": "application/json" } ] diff --git a/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_list.json b/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_list.json index d4a4531a..5f184117 100644 --- a/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_list.json +++ b/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_list.json @@ -3,6 +3,15 @@ { "name": "calculate", "description": "Performs a calculation based on the operation.", + "icons": [ + { + "mimeType": "image/svg+xml", + "sizes": [ + "any" + ], + "src": "https://www.svgrepo.com/show/530644/calculator.svg" + } + ], "inputSchema": { "type": "object", "properties": { From b0d0e61930248d6a08c08c838388697b664cc013 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Thu, 13 Nov 2025 22:24:41 +0100 Subject: [PATCH 2/2] Update docs/server-builder.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/server-builder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/server-builder.md b/docs/server-builder.md index ad0204df..5b00f902 100644 --- a/docs/server-builder.md +++ b/docs/server-builder.md @@ -61,7 +61,7 @@ $server = Server::builder() version: '1.2.0', description: 'Advanced mathematical calculations', icons: [new Icon('https://example.com/icon.png', 'image/png', ['64x64'])], - websiteUrl: 'https://example.com + websiteUrl: 'https://example.com' '); ```