Add support for icons and website url (#141)
This commit is contained in:
parent
660f150361
commit
5804ba9773
10 changed files with 279 additions and 22 deletions
|
|
@ -13,11 +13,18 @@
|
|||
require_once dirname(__DIR__).'/bootstrap.php';
|
||||
chdir(__DIR__);
|
||||
|
||||
use Mcp\Schema\Icon;
|
||||
use Mcp\Server;
|
||||
use Mcp\Server\Session\FileSessionStore;
|
||||
|
||||
$server = Server::builder()
|
||||
->setServerInfo('Schema Showcase', '1.0.0')
|
||||
->setServerInfo(
|
||||
'Schema Showcase',
|
||||
'1.0.0',
|
||||
'A showcase server demonstrating MCP schema capabilities.',
|
||||
[new Icon('https://www.php.net/images/logos/php-logo-white.svg', 'image/svg+xml', ['any'])],
|
||||
'https://github.com/modelcontextprotocol/php-sdk',
|
||||
)
|
||||
->setContainer(container())
|
||||
->setLogger(logger())
|
||||
->setSession(new FileSessionStore(__DIR__.'/sessions'))
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@ namespace Mcp\Schema\Enum;
|
|||
|
||||
/**
|
||||
* Available protocol versions for MCP.
|
||||
*
|
||||
* @author Illia Vasylevskyi<ineersa@gmail.com>
|
||||
*/
|
||||
enum ProtocolVersion: string
|
||||
{
|
||||
case V2024_11_05 = '2024-11-05';
|
||||
|
||||
case V2025_03_26 = '2025-03-26';
|
||||
|
||||
case V2025_06_18 = '2025-06-18';
|
||||
case V2025_11_25 = '2025-11-25';
|
||||
}
|
||||
|
|
|
|||
91
src/Schema/Icon.php
Normal file
91
src/Schema/Icon.php
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<?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\Schema;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* A url pointing to an icon URL or a base64-encoded data URI.
|
||||
*
|
||||
* @phpstan-type IconData array{
|
||||
* src: string,
|
||||
* mimeType?: string,
|
||||
* sizes?: string[],
|
||||
* }
|
||||
*
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
class Icon implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @param string $src a standard URI pointing to an icon resource
|
||||
* @param ?string $mimeType optional override if the server's MIME type is missing or generic
|
||||
* @param ?string[] $sizes optional array of strings that specify sizes at which the icon can be used.
|
||||
* Each string should be in WxH format (e.g., `"48x48"`, `"96x96"`) or `"any"` for
|
||||
* scalable formats like SVG.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $src,
|
||||
public readonly ?string $mimeType = null,
|
||||
public readonly ?array $sizes = null,
|
||||
) {
|
||||
if (empty($src)) {
|
||||
throw new InvalidArgumentException('Icon "src" must be a non-empty string.');
|
||||
}
|
||||
if (!preg_match('#^(https?://|data:)#', $src)) {
|
||||
throw new InvalidArgumentException('Icon "src" must be a valid URL or data URI.');
|
||||
}
|
||||
|
||||
if (null !== $sizes) {
|
||||
foreach ($sizes as $size) {
|
||||
if (!\is_string($size)) {
|
||||
throw new InvalidArgumentException('Each size in "sizes" must be a string.');
|
||||
}
|
||||
if (!preg_match('/^(any|\d+x\d+)$/', $size)) {
|
||||
throw new InvalidArgumentException(\sprintf('Invalid size format "%s" in "sizes". Expected "WxH" or "any".', $size));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IconData $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
if (empty($data['src']) || !\is_string($data['src'])) {
|
||||
throw new InvalidArgumentException('Invalid or missing "src" in Icon data.');
|
||||
}
|
||||
|
||||
return new self($data['src'], $data['mimeTypes'] ?? null, $data['sizes'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IconData
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$data = [
|
||||
'src' => $this->src,
|
||||
];
|
||||
|
||||
if (null !== $this->mimeType) {
|
||||
$data['mimeType'] = $this->mimeType;
|
||||
}
|
||||
|
||||
if (null !== $this->sizes) {
|
||||
$data['sizes'] = $this->sizes;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,14 +16,21 @@ use Mcp\Exception\InvalidArgumentException;
|
|||
/**
|
||||
* Describes the name and version of an MCP implementation.
|
||||
*
|
||||
* @phpstan-import-type IconData from Icon
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class Implementation implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @param ?Icon[] $icons
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $name = 'app',
|
||||
public readonly string $version = 'dev',
|
||||
public readonly ?string $description = null,
|
||||
public readonly ?array $icons = null,
|
||||
public readonly ?string $websiteUrl = null,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +38,9 @@ class Implementation implements \JsonSerializable
|
|||
* @param array{
|
||||
* name: string,
|
||||
* version: string,
|
||||
* description?: string,
|
||||
* icons?: IconData[],
|
||||
* websiteUrl?: string,
|
||||
* } $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
|
|
@ -42,13 +52,30 @@ class Implementation implements \JsonSerializable
|
|||
throw new InvalidArgumentException('Invalid or missing "version" in Implementation data.');
|
||||
}
|
||||
|
||||
return new self($data['name'], $data['version'], $data['description'] ?? null);
|
||||
if (isset($data['icons'])) {
|
||||
if (!\is_array($data['icons'])) {
|
||||
throw new InvalidArgumentException('Invalid "icons" in Implementation data; expected an array.');
|
||||
}
|
||||
|
||||
$data['icons'] = array_map(Icon::fromArray(...), $data['icons']);
|
||||
}
|
||||
|
||||
return new self(
|
||||
$data['name'],
|
||||
$data['version'],
|
||||
$data['description'] ?? null,
|
||||
$data['icons'] ?? null,
|
||||
$data['websiteUrl'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* name: string,
|
||||
* version: string,
|
||||
* description?: string,
|
||||
* icons?: Icon[],
|
||||
* websiteUrl?: string,
|
||||
* }
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
|
|
@ -62,6 +89,14 @@ class Implementation implements \JsonSerializable
|
|||
$data['description'] = $this->description;
|
||||
}
|
||||
|
||||
if (null !== $this->icons) {
|
||||
$data['icons'] = $this->icons;
|
||||
}
|
||||
|
||||
if (null !== $this->websiteUrl) {
|
||||
$data['websiteUrl'] = $this->websiteUrl;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,13 @@ use Mcp\Exception\InvalidArgumentException;
|
|||
* A prompt or prompt template that the server offers.
|
||||
*
|
||||
* @phpstan-import-type PromptArgumentData from PromptArgument
|
||||
* @phpstan-import-type IconData from Icon
|
||||
*
|
||||
* @phpstan-type PromptData array{
|
||||
* name: string,
|
||||
* description?: string,
|
||||
* arguments?: PromptArgumentData[],
|
||||
* icons?: IconData[],
|
||||
* _meta?: array<string, mixed>
|
||||
* }
|
||||
*
|
||||
|
|
@ -31,14 +33,16 @@ class Prompt implements \JsonSerializable
|
|||
{
|
||||
/**
|
||||
* @param string $name the name of the prompt or prompt template
|
||||
* @param string|null $description an optional description of what this prompt provides
|
||||
* @param PromptArgument[]|null $arguments A list of arguments for templating. Null if not a template.
|
||||
* @param ?string $description an optional description of what this prompt provides
|
||||
* @param ?PromptArgument[] $arguments A list of arguments for templating. Null if not a template.
|
||||
* @param ?Icon[] $icons optional icons representing the prompt
|
||||
* @param ?array<string, mixed> $meta Optional metadata
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly ?string $description = null,
|
||||
public readonly ?array $arguments = null,
|
||||
public readonly ?array $icons = null,
|
||||
public readonly ?array $meta = null,
|
||||
) {
|
||||
if (null !== $this->arguments) {
|
||||
|
|
@ -71,6 +75,7 @@ class Prompt implements \JsonSerializable
|
|||
name: $data['name'],
|
||||
description: $data['description'] ?? null,
|
||||
arguments: $arguments,
|
||||
icons: isset($data['icons']) && \is_array($data['icons']) ? array_map(Icon::fromArray(...), $data['icons']) : null,
|
||||
meta: isset($data['_meta']) ? $data['_meta'] : null
|
||||
);
|
||||
}
|
||||
|
|
@ -80,6 +85,7 @@ class Prompt implements \JsonSerializable
|
|||
* name: string,
|
||||
* description?: string,
|
||||
* arguments?: array<PromptArgument>,
|
||||
* icons?: Icon[],
|
||||
* _meta?: array<string, mixed>
|
||||
* }
|
||||
*/
|
||||
|
|
@ -92,6 +98,9 @@ class Prompt implements \JsonSerializable
|
|||
if (null !== $this->arguments) {
|
||||
$data['arguments'] = $this->arguments;
|
||||
}
|
||||
if (null !== $this->icons) {
|
||||
$data['icons'] = $this->icons;
|
||||
}
|
||||
if (null !== $this->meta) {
|
||||
$data['_meta'] = $this->meta;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,15 +17,17 @@ use Mcp\Exception\InvalidArgumentException;
|
|||
* A known resource that the server is capable of reading.
|
||||
*
|
||||
* @phpstan-import-type AnnotationsData from Annotations
|
||||
* @phpstan-import-type IconData from Icon
|
||||
*
|
||||
* @phpstan-type ResourceData array{
|
||||
* uri: string,
|
||||
* name: string,
|
||||
* description?: string|null,
|
||||
* mimeType?: string|null,
|
||||
* annotations?: AnnotationsData|null,
|
||||
* size?: int|null,
|
||||
* _meta?: array<string, mixed>
|
||||
* description?: string,
|
||||
* mimeType?: string,
|
||||
* annotations?: AnnotationsData,
|
||||
* size?: int,
|
||||
* icons?: IconData[],
|
||||
* _meta?: array<string, mixed>,
|
||||
* }
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
|
|
@ -46,10 +48,11 @@ class Resource implements \JsonSerializable
|
|||
/**
|
||||
* @param string $uri the URI of this resource
|
||||
* @param string $name A human-readable name for this resource. This can be used by clients to populate UI elements.
|
||||
* @param string|null $description A description of what this resource represents. This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model.
|
||||
* @param string|null $mimeType the MIME type of this resource, if known
|
||||
* @param Annotations|null $annotations optional annotations for the client
|
||||
* @param int|null $size The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
|
||||
* @param ?string $description A description of what this resource represents. This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model.
|
||||
* @param ?string $mimeType the MIME type of this resource, if known
|
||||
* @param ?Annotations $annotations optional annotations for the client
|
||||
* @param ?int $size The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
|
||||
* @param ?Icon[] $icons optional icons representing the resource
|
||||
* @param ?array<string, mixed> $meta Optional metadata
|
||||
*
|
||||
* This can be used by Hosts to display file sizes and estimate context window usage
|
||||
|
|
@ -61,6 +64,7 @@ class Resource implements \JsonSerializable
|
|||
public readonly ?string $mimeType = null,
|
||||
public readonly ?Annotations $annotations = null,
|
||||
public readonly ?int $size = null,
|
||||
public readonly ?array $icons = null,
|
||||
public readonly ?array $meta = null,
|
||||
) {
|
||||
if (!preg_match(self::RESOURCE_NAME_PATTERN, $name)) {
|
||||
|
|
@ -94,6 +98,7 @@ class Resource implements \JsonSerializable
|
|||
mimeType: $data['mimeType'] ?? null,
|
||||
annotations: isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null,
|
||||
size: isset($data['size']) ? (int) $data['size'] : null,
|
||||
icons: isset($data['icons']) && \is_array($data['icons']) ? array_map(Icon::fromArray(...), $data['icons']) : null,
|
||||
meta: isset($data['_meta']) ? $data['_meta'] : null
|
||||
);
|
||||
}
|
||||
|
|
@ -106,6 +111,7 @@ class Resource implements \JsonSerializable
|
|||
* mimeType?: string,
|
||||
* annotations?: Annotations,
|
||||
* size?: int,
|
||||
* icons?: Icon[],
|
||||
* _meta?: array<string, mixed>
|
||||
* }
|
||||
*/
|
||||
|
|
@ -127,6 +133,9 @@ class Resource implements \JsonSerializable
|
|||
if (null !== $this->size) {
|
||||
$data['size'] = $this->size;
|
||||
}
|
||||
if (null !== $this->icons) {
|
||||
$data['icons'] = $this->icons;
|
||||
}
|
||||
if (null !== $this->meta) {
|
||||
$data['_meta'] = $this->meta;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use Mcp\Exception\InvalidArgumentException;
|
|||
* Definition for a tool the client can call.
|
||||
*
|
||||
* @phpstan-import-type ToolAnnotationsData from ToolAnnotations
|
||||
* @phpstan-import-type IconData from Icon
|
||||
*
|
||||
* @phpstan-type ToolInputSchema array{
|
||||
* type: 'object',
|
||||
|
|
@ -28,6 +29,7 @@ use Mcp\Exception\InvalidArgumentException;
|
|||
* inputSchema: ToolInputSchema,
|
||||
* description?: string|null,
|
||||
* annotations?: ToolAnnotationsData,
|
||||
* icons?: IconData[],
|
||||
* _meta?: array<string, mixed>
|
||||
* }
|
||||
*
|
||||
|
|
@ -37,11 +39,12 @@ class Tool implements \JsonSerializable
|
|||
{
|
||||
/**
|
||||
* @param string $name the name of the tool
|
||||
* @param string|null $description A human-readable description of the tool.
|
||||
* @param ?string $description A human-readable description of the tool.
|
||||
* This can be used by clients to improve the LLM's understanding of
|
||||
* available tools. It can be thought of like a "hint" to the model.
|
||||
* @param ToolInputSchema $inputSchema a JSON Schema object (as a PHP array) defining the expected 'arguments' for the tool
|
||||
* @param ToolAnnotations|null $annotations optional additional tool information
|
||||
* @param ?ToolAnnotations $annotations optional additional tool information
|
||||
* @param ?Icon[] $icons optional icons representing the tool
|
||||
* @param ?array<string, mixed> $meta Optional metadata
|
||||
*/
|
||||
public function __construct(
|
||||
|
|
@ -49,6 +52,7 @@ class Tool implements \JsonSerializable
|
|||
public readonly array $inputSchema,
|
||||
public readonly ?string $description,
|
||||
public readonly ?ToolAnnotations $annotations,
|
||||
public readonly ?array $icons = null,
|
||||
public readonly ?array $meta = null,
|
||||
) {
|
||||
if (!isset($inputSchema['type']) || 'object' !== $inputSchema['type']) {
|
||||
|
|
@ -79,6 +83,7 @@ class Tool implements \JsonSerializable
|
|||
$data['inputSchema'],
|
||||
isset($data['description']) && \is_string($data['description']) ? $data['description'] : null,
|
||||
isset($data['annotations']) && \is_array($data['annotations']) ? ToolAnnotations::fromArray($data['annotations']) : null,
|
||||
isset($data['icons']) && \is_array($data['icons']) ? array_map(Icon::fromArray(...), $data['icons']) : null,
|
||||
isset($data['_meta']) && \is_array($data['_meta']) ? $data['_meta'] : null
|
||||
);
|
||||
}
|
||||
|
|
@ -89,6 +94,7 @@ class Tool implements \JsonSerializable
|
|||
* inputSchema: ToolInputSchema,
|
||||
* description?: string,
|
||||
* annotations?: ToolAnnotations,
|
||||
* icons?: Icon[],
|
||||
* _meta?: array<string, mixed>
|
||||
* }
|
||||
*/
|
||||
|
|
@ -104,6 +110,9 @@ class Tool implements \JsonSerializable
|
|||
if (null !== $this->annotations) {
|
||||
$data['annotations'] = $this->annotations;
|
||||
}
|
||||
if (null !== $this->icons) {
|
||||
$data['icons'] = $this->icons;
|
||||
}
|
||||
if (null !== $this->meta) {
|
||||
$data['_meta'] = $this->meta;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ use Mcp\Capability\Registry\ReferenceHandler;
|
|||
use Mcp\JsonRpc\MessageFactory;
|
||||
use Mcp\Schema\Annotations;
|
||||
use Mcp\Schema\Enum\ProtocolVersion;
|
||||
use Mcp\Schema\Icon;
|
||||
use Mcp\Schema\Implementation;
|
||||
use Mcp\Schema\ServerCapabilities;
|
||||
use Mcp\Schema\ToolAnnotations;
|
||||
|
|
@ -145,10 +146,17 @@ final class Builder
|
|||
|
||||
/**
|
||||
* Sets the server's identity. Required.
|
||||
*
|
||||
* @param ?Icon[] $icons
|
||||
*/
|
||||
public function setServerInfo(string $name, string $version, ?string $description = null): self
|
||||
{
|
||||
$this->serverInfo = new Implementation(trim($name), trim($version), $description);
|
||||
public function setServerInfo(
|
||||
string $name,
|
||||
string $version,
|
||||
?string $description = null,
|
||||
?array $icons = null,
|
||||
?string $websiteUrl = null,
|
||||
): self {
|
||||
$this->serverInfo = new Implementation(trim($name), trim($version), $description, $icons, $websiteUrl);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
|
@ -295,7 +303,7 @@ final class Builder
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function setProtocolVersion(?ProtocolVersion $protocolVersion): self
|
||||
public function setProtocolVersion(ProtocolVersion $protocolVersion): self
|
||||
{
|
||||
$this->protocolVersion = $protocolVersion;
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use Symfony\Component\Process\Process;
|
|||
|
||||
abstract class InspectorSnapshotTestCase extends TestCase
|
||||
{
|
||||
private const INSPECTOR_VERSION = '0.16.8';
|
||||
private const INSPECTOR_VERSION = '0.17.2';
|
||||
|
||||
/** @param array<string, mixed> $options */
|
||||
#[DataProvider('provideMethods')]
|
||||
|
|
|
|||
88
tests/Unit/Schema/IconTest.php
Normal file
88
tests/Unit/Schema/IconTest.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?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\Tests\Unit\Schema;
|
||||
|
||||
use Mcp\Exception\InvalidArgumentException;
|
||||
use Mcp\Schema\Icon;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class IconTest extends TestCase
|
||||
{
|
||||
public function testValidConstructor()
|
||||
{
|
||||
$icon = new Icon('https://www.php.net/images/logos/php-logo-white.svg', 'image/svg+xml', ['any']);
|
||||
|
||||
$this->assertSame('https://www.php.net/images/logos/php-logo-white.svg', $icon->src);
|
||||
$this->assertSame('image/svg+xml', $icon->mimeType);
|
||||
$this->assertSame('any', $icon->sizes[0]);
|
||||
}
|
||||
|
||||
public function testConstructorWithMultipleSizes()
|
||||
{
|
||||
$icon = new Icon('https://example.com/icon.png', 'image/png', ['48x48', '96x96']);
|
||||
|
||||
$this->assertCount(2, $icon->sizes);
|
||||
$this->assertSame(['48x48', '96x96'], $icon->sizes);
|
||||
}
|
||||
|
||||
public function testConstructorWithAnySizes()
|
||||
{
|
||||
$icon = new Icon('https://example.com/icon.svg', 'image/png', ['any']);
|
||||
|
||||
$this->assertSame(['any'], $icon->sizes);
|
||||
}
|
||||
|
||||
public function testConstructorWithNullOptionalFields()
|
||||
{
|
||||
$icon = new Icon('https://example.com/icon.png');
|
||||
|
||||
$this->assertSame('https://example.com/icon.png', $icon->src);
|
||||
$this->assertNull($icon->mimeType);
|
||||
$this->assertNull($icon->sizes);
|
||||
}
|
||||
|
||||
public function testInvalidSizesFormatThrowsException()
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
new Icon('https://example.com/icon.png', 'image/png', ['invalid-size']);
|
||||
}
|
||||
|
||||
public function testInvalidPixelSizesFormatThrowsException()
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
new Icon('https://example.com/icon.png', 'image/png', ['180x48x48']);
|
||||
}
|
||||
|
||||
public function testEmptySrcThrowsException()
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
new Icon('', 'image/png', ['48x48']);
|
||||
}
|
||||
|
||||
public function testInvalidSrcThrowsException()
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
new Icon('not-a-url', 'image/png', ['48x48']);
|
||||
}
|
||||
|
||||
public function testValidDataUriSrc()
|
||||
{
|
||||
$dataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA';
|
||||
$icon = new Icon($dataUri, 'image/png', ['48x48']);
|
||||
|
||||
$this->assertSame($dataUri, $icon->src);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue