1: <?php
2:
3: declare(strict_types=1);
4:
5: /**
6: * This file is part of the Nexus MCP SDK package.
7: *
8: * (c) 2026 John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9: *
10: * For the full copyright and license information, please view
11: * the LICENSE file that was distributed with this source code.
12: */
13:
14: namespace Nexus\Mcp\Core\Schema;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Validation\Rfc3986UriValidator;
18:
19: /**
20: * An optionally-sized icon that can be displayed in a user interface.
21: *
22: * @implements Arrayable<array{
23: * src: non-empty-string,
24: * mimeType?: non-empty-string,
25: * sizes?: list<non-empty-string>,
26: * theme?: 'dark'|'light',
27: * }>
28: *
29: * @see https://modelcontextprotocol.io/specification/2026-07-28/schema#icon
30: */
31: final readonly class Icon implements Arrayable
32: {
33: /**
34: * @param non-empty-string $src
35: * @param null|non-empty-string $mimeType
36: * @param null|list<non-empty-string> $sizes
37: * @param null|'dark'|'light' $theme
38: */
39: public function __construct(
40: public string $src,
41: public ?string $mimeType = null,
42: public ?array $sizes = null,
43: public ?string $theme = null,
44: ) {
45: Rfc3986UriValidator::validate($src, '"icons.src"');
46: Assert::that($mimeType)
47: ->nullOr()
48: ->isNonEmptyString('"icons.mimeType" must be a non-empty string or null.')
49: ->matchesRegularExpression(
50: '/\A[a-zA-Z][a-zA-Z!#$&^_.+-]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]*\z/',
51: '"icons.mimeType" must be a valid MIME type in the format "type/subtype".',
52: )
53: ;
54:
55: if (null !== $sizes) {
56: Assert::that($sizes)
57: ->isList('"icons.sizes" must be a list of strings or null, {type} given.')
58: ->values()
59: ->isNonEmptyString('each "icons.sizes" must be a non-empty string.')
60: ->matchesRegularExpression('/\A(\d+x\d+|any)\z/', 'each "icons.sizes" must be in the format "WIDTHxHEIGHT" or "any".')
61: ;
62: }
63:
64: Assert::that($theme)->nullOr()->isOneOf(['light', 'dark'], '"icons.theme" must be one of "light", "dark".');
65: }
66:
67: #[\Override]
68: public static function fromArray(array $data): static
69: {
70: Assert::that($data)->hasOffset('src', '"icons" is missing the required "src" key.');
71:
72: $src = $data['src'];
73: Assert::that($src)->isNonEmptyString('"icons.src" must be a non-empty string, {type} given.');
74:
75: $mimeType = $data['mimeType'] ?? null;
76: Assert::that($mimeType)->nullOr()->isNonEmptyString('"icons.mimeType" must be a non-empty string or null, {type} given.');
77:
78: $sizes = null;
79:
80: if (isset($data['sizes'])) {
81: Assert::that($data['sizes'])
82: ->isList('"icons.sizes" must be a list of strings or null, {type} given.')
83: ->values()->isNonEmptyString('each "icons.sizes" must be a non-empty string, {type} given.')
84: ;
85: $sizes = $data['sizes'];
86: }
87:
88: $theme = $data['theme'] ?? null;
89: Assert::that($theme)->nullOr()->isOneOf(['light', 'dark'], '"icons.theme" must be one of "light", "dark".');
90:
91: return new self(src: $src, mimeType: $mimeType, sizes: $sizes, theme: $theme);
92: }
93:
94: #[\Override]
95: public function toArray(): array
96: {
97: $data = ['src' => $this->src];
98:
99: if (null !== $this->mimeType) {
100: $data['mimeType'] = $this->mimeType;
101: }
102:
103: if (null !== $this->sizes) {
104: $data['sizes'] = $this->sizes;
105: }
106:
107: if (null !== $this->theme) {
108: $data['theme'] = $this->theme;
109: }
110:
111: return $data;
112: }
113:
114: #[\Override]
115: public function jsonSerialize(): array
116: {
117: return $this->toArray();
118: }
119: }
120: