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\Resource;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Schema\Annotations;
18: use Nexus\Mcp\Core\Schema\Arrayable;
19: use Nexus\Mcp\Core\Schema\BaseMetadata;
20: use Nexus\Mcp\Core\Schema\Icon;
21: use Nexus\Mcp\Core\Schema\Icons;
22: use Nexus\Mcp\Core\Schema\MetaObject;
23: use Nexus\Mcp\Core\Validation\IdentifierNameValidator;
24: use Nexus\Mcp\Core\Validation\Rfc3986UriValidator;
25:
26: /**
27: * A known resource that the server is capable of reading.
28: *
29: * @implements Arrayable<array{
30: * name: non-empty-string,
31: * uri: non-empty-string,
32: * title?: non-empty-string,
33: * description?: non-empty-string,
34: * mimeType?: non-empty-string,
35: * annotations?: template-type<Annotations, Arrayable, 'T'>,
36: * size?: int,
37: * icons?: list<template-type<Icon, Arrayable, 'T'>>,
38: * _meta?: template-type<MetaObject, Arrayable, 'T'>,
39: * }>
40: *
41: * @see https://modelcontextprotocol.io/specification/draft/schema#resource
42: */
43: final readonly class Resource extends BaseMetadata implements Arrayable, Icons
44: {
45: /**
46: * @var non-empty-string
47: */
48: public string $uri;
49:
50: /**
51: * @var null|non-empty-string
52: */
53: public ?string $description;
54:
55: /**
56: * @var null|non-empty-string
57: */
58: public ?string $mimeType;
59:
60: /**
61: * @var null|list<Icon>
62: */
63: public ?array $icons;
64:
65: /**
66: * @param null|list<Icon> $icons
67: */
68: public function __construct(
69: string $name,
70: string $uri,
71: ?string $title = null,
72: ?string $description = null,
73: ?string $mimeType = null,
74: public Annotations $annotations = new Annotations(),
75: public ?int $size = null,
76: ?array $icons = null,
77: public MetaObject $meta = new MetaObject(),
78: ) {
79: parent::__construct(name: $name, title: $title);
80:
81: IdentifierNameValidator::validate($name, 'resource "name"');
82: Rfc3986UriValidator::validate($uri, 'resource "uri"');
83:
84: Assert::that($description)
85: ->nullOr()
86: ->isNonEmptyString('resource "description" must be a non-empty string or null.')
87: ;
88: Assert::that($mimeType)
89: ->nullOr()
90: ->isNonEmptyString('resource "mimeType" must be a non-empty string or null.')
91: ;
92:
93: if (null !== $icons) {
94: Assert::that($icons)->values()->isInstanceOf(Icon::class);
95: }
96:
97: $this->uri = $uri;
98: $this->description = $description;
99: $this->mimeType = $mimeType;
100: $this->icons = $icons;
101: }
102:
103: #[\Override]
104: public static function fromArray(array $data): static
105: {
106: Assert::that($data)->hasOffset('name', 'resource is missing the required "name" key.');
107: $name = $data['name'];
108: Assert::that($name)->isString('resource "name" must be a string, {type} given.');
109:
110: Assert::that($data)->hasOffset('uri', 'resource is missing the required "uri" key.');
111: $uri = $data['uri'];
112: Assert::that($uri)->isString('resource "uri" must be a string, {type} given.');
113:
114: $title = $data['title'] ?? null;
115: Assert::that($title)->nullOr()->isString('resource "title" must be a string or null, {type} given.');
116:
117: $description = $data['description'] ?? null;
118: Assert::that($description)->nullOr()->isString('resource "description" must be a string or null, {type} given.');
119:
120: $mimeType = $data['mimeType'] ?? null;
121: Assert::that($mimeType)->nullOr()->isString('resource "mimeType" must be a string or null, {type} given.');
122:
123: $annotations = new Annotations();
124:
125: if (\array_key_exists('annotations', $data)) {
126: Assert::that($data['annotations'])
127: ->isArray('resource "annotations" must be an object, {type} given.')
128: ->isMap('resource "annotations" must be a string-keyed object.')
129: ;
130: $annotations = Annotations::fromArray($data['annotations']);
131: }
132:
133: $size = $data['size'] ?? null;
134: Assert::that($size)->nullOr()->isInt('resource "size" must be an integer or null, {type} given.');
135:
136: $icons = null;
137:
138: if (isset($data['icons'])) {
139: Assert::that($data['icons'])
140: ->isList('resource "icons" must be a list, {type} given.')
141: ->values()
142: ->isArray('each resource "icon" must be an object, {type} given.')
143: ->isMap('each resource "icon" must be a string-keyed object.')
144: ;
145: $icons = array_map(Icon::fromArray(...), $data['icons']);
146: }
147:
148: $meta = new MetaObject();
149:
150: if (\array_key_exists('_meta', $data)) {
151: Assert::that($data['_meta'])
152: ->isArray('resource "_meta" must be an object, {type} given.')
153: ->isMap('resource "_meta" must be a string-keyed object.')
154: ;
155: $meta = MetaObject::fromArray($data['_meta']);
156: }
157:
158: return new self(
159: name: $name,
160: uri: $uri,
161: title: $title,
162: description: $description,
163: mimeType: $mimeType,
164: annotations: $annotations,
165: size: $size,
166: icons: $icons,
167: meta: $meta,
168: );
169: }
170:
171: #[\Override]
172: public function toArray(): array
173: {
174: $data = [
175: 'name' => $this->name,
176: 'uri' => $this->uri,
177: ];
178:
179: if (null !== $this->title) {
180: $data['title'] = $this->title;
181: }
182:
183: if (null !== $this->description) {
184: $data['description'] = $this->description;
185: }
186:
187: if (null !== $this->mimeType) {
188: $data['mimeType'] = $this->mimeType;
189: }
190:
191: $annotations = $this->annotations->toArray();
192:
193: if ([] !== $annotations) {
194: $data['annotations'] = $annotations;
195: }
196:
197: if (null !== $this->size) {
198: $data['size'] = $this->size;
199: }
200:
201: if (null !== $this->icons) {
202: $data['icons'] = array_map(static fn(Icon $icon): array => $icon->toArray(), $this->icons);
203: }
204:
205: $meta = $this->meta->toArray();
206:
207: if ([] !== $meta) {
208: $data['_meta'] = $meta;
209: }
210:
211: return $data;
212: }
213:
214: #[\Override]
215: public function jsonSerialize(): array
216: {
217: return $this->toArray();
218: }
219: }
220: