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