Skip to content

Commit 0439d2d

Browse files
committed
Add support for icons and website url
1 parent 09596c0 commit 0439d2d

File tree

10 files changed

+289
-33
lines changed

10 files changed

+289
-33
lines changed

examples/schema-showcase/server.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,18 @@
1313
require_once dirname(__DIR__).'/bootstrap.php';
1414
chdir(__DIR__);
1515

16+
use Mcp\Schema\Icon;
1617
use Mcp\Server;
1718
use Mcp\Server\Session\FileSessionStore;
1819

1920
$server = Server::builder()
20-
->setServerInfo('Schema Showcase', '1.0.0')
21+
->setServerInfo(
22+
'Schema Showcase',
23+
'1.0.0',
24+
'A showcase server demonstrating MCP schema capabilities.',
25+
[new Icon('https://www.php.net/images/logos/php-logo-white.svg', 'image/svg+xml', ['any'])],
26+
'https://github.com/modelcontextprotocol/php-sdk',
27+
)
2128
->setContainer(container())
2229
->setLogger(logger())
2330
->setSession(new FileSessionStore(__DIR__.'/sessions'))

src/Schema/Enum/ProtocolVersion.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313

1414
/**
1515
* Available protocol versions for MCP.
16+
*
17+
* @author Illia Vasylevskyi<[email protected]>
1618
*/
1719
enum ProtocolVersion: string
1820
{
1921
case V2024_11_05 = '2024-11-05';
20-
2122
case V2025_03_26 = '2025-03-26';
22-
2323
case V2025_06_18 = '2025-06-18';
24+
case V2025_11_25 = '2025-11-25';
2425
}

src/Schema/Icon.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Schema;
13+
14+
use Mcp\Exception\InvalidArgumentException;
15+
16+
/**
17+
* A url pointing to an icon URL or a base64-encoded data URI.
18+
*
19+
* @phpstan-type IconData array{
20+
* src: string,
21+
* mimeType?: string,
22+
* sizes?: string[],
23+
* }
24+
*
25+
* @author Christopher Hertel <[email protected]>
26+
*/
27+
class Icon implements \JsonSerializable
28+
{
29+
/**
30+
* @param string $src a standard URI pointing to an icon resource
31+
* @param ?string $mimeType optional override if the server's MIME type is missing or generic
32+
* @param ?string[] $sizes optional array of strings that specify sizes at which the icon can be used.
33+
* Each string should be in WxH format (e.g., `"48x48"`, `"96x96"`) or `"any"` for
34+
* scalable formats like SVG.
35+
*/
36+
public function __construct(
37+
public readonly string $src,
38+
public readonly ?string $mimeType = null,
39+
public readonly ?array $sizes = null,
40+
) {
41+
if (empty($src)) {
42+
throw new InvalidArgumentException('Icon "src" must be a non-empty string.');
43+
}
44+
if (!preg_match('#^(https?://|data:)#', $src)) {
45+
throw new InvalidArgumentException('Icon "src" must be a valid URL or data URI.');
46+
}
47+
48+
if (null !== $sizes) {
49+
foreach ($sizes as $size) {
50+
if (!\is_string($size)) {
51+
throw new InvalidArgumentException('Each size in "sizes" must be a string.');
52+
}
53+
if (!preg_match('/^(any|\d+x\d+)$/', $size)) {
54+
throw new InvalidArgumentException(\sprintf('Invalid size format "%s" in "sizes". Expected "WxH" or "any".', $size));
55+
}
56+
}
57+
}
58+
}
59+
60+
/**
61+
* @param IconData $data
62+
*/
63+
public static function fromArray(array $data): self
64+
{
65+
if (empty($data['src']) || !\is_string($data['src'])) {
66+
throw new InvalidArgumentException('Invalid or missing "src" in Icon data.');
67+
}
68+
69+
return new self($data['src'], $data['mimeTypes'] ?? null, $data['sizes'] ?? null);
70+
}
71+
72+
/**
73+
* @return IconData
74+
*/
75+
public function jsonSerialize(): array
76+
{
77+
$data = [
78+
'src' => $this->src,
79+
];
80+
81+
if (null !== $this->mimeType) {
82+
$data['mimeType'] = $this->mimeType;
83+
}
84+
85+
if (null !== $this->sizes) {
86+
$data['sizes'] = $this->sizes;
87+
}
88+
89+
return $data;
90+
}
91+
}

src/Schema/Implementation.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,31 @@
1616
/**
1717
* Describes the name and version of an MCP implementation.
1818
*
19+
* @phpstan-import-type IconData from Icon
20+
*
1921
* @author Kyrian Obikwelu <[email protected]>
2022
*/
2123
class Implementation implements \JsonSerializable
2224
{
25+
/**
26+
* @param ?Icon[] $icons
27+
*/
2328
public function __construct(
2429
public readonly string $name = 'app',
2530
public readonly string $version = 'dev',
2631
public readonly ?string $description = null,
32+
public readonly ?array $icons = null,
33+
public readonly ?string $websiteUrl = null,
2734
) {
2835
}
2936

3037
/**
3138
* @param array{
3239
* name: string,
3340
* version: string,
41+
* description?: string,
42+
* icons?: IconData[],
43+
* websiteUrl?: string,
3444
* } $data
3545
*/
3646
public static function fromArray(array $data): self
@@ -42,13 +52,30 @@ public static function fromArray(array $data): self
4252
throw new InvalidArgumentException('Invalid or missing "version" in Implementation data.');
4353
}
4454

45-
return new self($data['name'], $data['version'], $data['description'] ?? null);
55+
if (isset($data['icons'])) {
56+
if (!\is_array($data['icons'])) {
57+
throw new InvalidArgumentException('Invalid "icons" in Implementation data; expected an array.');
58+
}
59+
60+
$data['icons'] = array_map(Icon::fromArray(...), $data['icons']);
61+
}
62+
63+
return new self(
64+
$data['name'],
65+
$data['version'],
66+
$data['description'] ?? null,
67+
$data['icons'] ?? null,
68+
$data['websiteUrl'] ?? null,
69+
);
4670
}
4771

4872
/**
4973
* @return array{
5074
* name: string,
5175
* version: string,
76+
* description?: string,
77+
* icons?: Icon[],
78+
* websiteUrl?: string,
5279
* }
5380
*/
5481
public function jsonSerialize(): array
@@ -62,6 +89,14 @@ public function jsonSerialize(): array
6289
$data['description'] = $this->description;
6390
}
6491

92+
if (null !== $this->icons) {
93+
$data['icons'] = $this->icons;
94+
}
95+
96+
if (null !== $this->websiteUrl) {
97+
$data['websiteUrl'] = $this->websiteUrl;
98+
}
99+
65100
return $data;
66101
}
67102
}

src/Schema/Prompt.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,30 @@
1717
* A prompt or prompt template that the server offers.
1818
*
1919
* @phpstan-import-type PromptArgumentData from PromptArgument
20+
* @phpstan-import-type IconData from Icon
2021
*
2122
* @phpstan-type PromptData array{
2223
* name: string,
2324
* description?: string,
2425
* arguments?: PromptArgumentData[],
26+
* icons?: IconData[],
2527
* }
2628
*
2729
* @author Kyrian Obikwelu <[email protected]>
2830
*/
2931
class Prompt implements \JsonSerializable
3032
{
3133
/**
32-
* @param string $name the name of the prompt or prompt template
33-
* @param string|null $description an optional description of what this prompt provides
34-
* @param PromptArgument[]|null $arguments A list of arguments for templating. Null if not a template.
34+
* @param string $name the name of the prompt or prompt template
35+
* @param ?string $description an optional description of what this prompt provides
36+
* @param ?PromptArgument[] $arguments A list of arguments for templating. Null if not a template.
37+
* @param ?Icon[] $icons optional icons representing the prompt
3538
*/
3639
public function __construct(
3740
public readonly string $name,
3841
public readonly ?string $description = null,
3942
public readonly ?array $arguments = null,
43+
public readonly ?array $icons = null,
4044
) {
4145
if (null !== $this->arguments) {
4246
foreach ($this->arguments as $arg) {
@@ -63,15 +67,17 @@ public static function fromArray(array $data): self
6367
return new self(
6468
name: $data['name'],
6569
description: $data['description'] ?? null,
66-
arguments: $arguments
70+
arguments: $arguments,
71+
icons: isset($data['icons']) && \is_array($data['icons']) ? array_map(Icon::fromArray(...), $data['icons']) : null,
6772
);
6873
}
6974

7075
/**
7176
* @return array{
7277
* name: string,
7378
* description?: string,
74-
* arguments?: array<PromptArgument>
79+
* arguments?: array<PromptArgument>,
80+
* icons?: Icon[],
7581
* }
7682
*/
7783
public function jsonSerialize(): array
@@ -83,6 +89,9 @@ public function jsonSerialize(): array
8389
if (null !== $this->arguments) {
8490
$data['arguments'] = $this->arguments;
8591
}
92+
if (null !== $this->icons) {
93+
$data['icons'] = $this->icons;
94+
}
8695

8796
return $data;
8897
}

src/Schema/Resource.php

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
* A known resource that the server is capable of reading.
1818
*
1919
* @phpstan-import-type AnnotationsData from Annotations
20+
* @phpstan-import-type IconData from Icon
2021
*
2122
* @phpstan-type ResourceData array{
2223
* uri: string,
2324
* name: string,
24-
* description?: string|null,
25-
* mimeType?: string|null,
26-
* annotations?: AnnotationsData|null,
27-
* size?: int|null,
25+
* description?: string,
26+
* mimeType?: string,
27+
* annotations?: AnnotationsData,
28+
* size?: int,
2829
* }
2930
*
3031
* @author Kyrian Obikwelu <[email protected]>
@@ -43,14 +44,15 @@ class Resource implements \JsonSerializable
4344
private const URI_PATTERN = '/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s]*$/';
4445

4546
/**
46-
* @param string $uri the URI of this resource
47-
* @param string $name A human-readable name for this resource. This can be used by clients to populate UI elements.
48-
* @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.
49-
* @param string|null $mimeType the MIME type of this resource, if known
50-
* @param Annotations|null $annotations optional annotations for the client
51-
* @param int|null $size The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
47+
* @param string $uri the URI of this resource
48+
* @param string $name A human-readable name for this resource. This can be used by clients to populate UI elements.
49+
* @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.
50+
* @param ?string $mimeType the MIME type of this resource, if known
51+
* @param ?Annotations $annotations optional annotations for the client
52+
* @param ?int $size The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
53+
* @param ?Icon[] $icons optional icons representing the resource
5254
*
53-
* This can be used by Hosts to display file sizes and estimate context window usage.
55+
* This can be used by Hosts to display file sizes and estimate context window usage
5456
*/
5557
public function __construct(
5658
public readonly string $uri,
@@ -59,6 +61,7 @@ public function __construct(
5961
public readonly ?string $mimeType = null,
6062
public readonly ?Annotations $annotations = null,
6163
public readonly ?int $size = null,
64+
public readonly ?array $icons = null,
6265
) {
6366
if (!preg_match(self::RESOURCE_NAME_PATTERN, $name)) {
6467
throw new InvalidArgumentException('Invalid resource name: must contain only alphanumeric characters, underscores, and hyphens.');
@@ -86,7 +89,8 @@ public static function fromArray(array $data): self
8689
description: $data['description'] ?? null,
8790
mimeType: $data['mimeType'] ?? null,
8891
annotations: isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null,
89-
size: isset($data['size']) ? (int) $data['size'] : null
92+
size: isset($data['size']) ? (int) $data['size'] : null,
93+
icons: isset($data['icons']) && \is_array($data['icons']) ? array_map(Icon::fromArray(...), $data['icons']) : null,
9094
);
9195
}
9296

@@ -98,6 +102,7 @@ public static function fromArray(array $data): self
98102
* mimeType?: string,
99103
* annotations?: Annotations,
100104
* size?: int,
105+
* icons?: Icon[],
101106
* }
102107
*/
103108
public function jsonSerialize(): array
@@ -118,6 +123,9 @@ public function jsonSerialize(): array
118123
if (null !== $this->size) {
119124
$data['size'] = $this->size;
120125
}
126+
if (null !== $this->icons) {
127+
$data['icons'] = $this->icons;
128+
}
121129

122130
return $data;
123131
}

0 commit comments

Comments
 (0)