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\ParsesNumber;
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?: float,
38: * icons?: list<template-type<Icon, Arrayable, 'T'>>,
39: * _meta?: template-type<MetaObject, Arrayable, 'T'>,
40: * }>
41: *
42: * @see https://modelcontextprotocol.io/specification/2025-11-25/schema#resource
43: */
44: final readonly class Resource extends BaseMetadata implements Arrayable, Icons
45: {
46: use ParsesNumber;
47:
48: /**
49: * @var non-empty-string
50: */
51: public string $uri;
52:
53: /**
54: * @var null|non-empty-string
55: */
56: public ?string $description;
57:
58: /**
59: * @var null|non-empty-string
60: */
61: public ?string $mimeType;
62:
63: /**
64: * @var null|list<Icon>
65: */
66: public ?array $icons;
67:
68: /**
69: * @param null|list<Icon> $icons
70: */
71: public function __construct(
72: string $name,
73: string $uri,
74: ?string $title = null,
75: ?string $description = null,
76: ?string $mimeType = null,
77: public Annotations $annotations = new Annotations(),
78: public ?float $size = null,
79: ?array $icons = null,
80: public MetaObject $meta = new MetaObject(),
81: ) {
82: parent::__construct($name, $title);
83:
84: IdentifierNameValidator::validate($name, 'resource "name"');
85: Rfc3986UriValidator::validate($uri, 'resource "uri"');
86:
87: Assert::that($description)->nullOr()->isNonEmptyString('resource "description" must be a non-empty string or null.');
88: Assert::that($mimeType)->nullOr()->isNonEmptyString('resource "mimeType" must be a non-empty string or null.');
89:
90: if (null !== $icons) {
91: Assert::that($icons)->values()->isInstanceOf(Icon::class);
92: }
93:
94: $this->uri = $uri;
95: $this->description = $description;
96: $this->mimeType = $mimeType;
97: $this->icons = $icons;
98: }
99:
100: /**
101: * @param array<string, mixed> $data
102: */
103: #[\Override]
104: public static function fromArray(array $data): static
105: {
106: Assert::that($data)->hasOffset('name', 'resource 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 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:
135: if (null !== $size) {
136: $size = self::parseNumber($size, 'resource "size" must be a number or null, {type} given.');
137: }
138:
139: $icons = null;
140:
141: if (isset($data['icons'])) {
142: Assert::that($data['icons'])
143: ->isList('resource "icons" must be a list, {type} given.')
144: ->values()
145: ->isArray('each resource "icon" must be an object, {type} given.')
146: ->isMap('each resource "icon" must be a string-keyed object.')
147: ;
148: $icons = array_map(Icon::fromArray(...), $data['icons']);
149: }
150:
151: $meta = new MetaObject();
152:
153: if (\array_key_exists('_meta', $data)) {
154: Assert::that($data['_meta'])
155: ->isArray('resource "_meta" must be an object, {type} given.')
156: ->isMap('resource "_meta" must be a string-keyed object.')
157: ;
158: $meta = MetaObject::fromArray($data['_meta']);
159: }
160:
161: return new self($name, $uri, $title, $description, $mimeType, $annotations, $size, $icons, $meta);
162: }
163:
164: #[\Override]
165: public function toArray(): array
166: {
167: $data = [
168: 'name' => $this->name,
169: 'uri' => $this->uri,
170: ];
171:
172: if (null !== $this->title) {
173: $data['title'] = $this->title;
174: }
175:
176: if (null !== $this->description) {
177: $data['description'] = $this->description;
178: }
179:
180: if (null !== $this->mimeType) {
181: $data['mimeType'] = $this->mimeType;
182: }
183:
184: $annotations = $this->annotations->toArray();
185:
186: if ([] !== $annotations) {
187: $data['annotations'] = $annotations;
188: }
189:
190: if (null !== $this->size) {
191: $data['size'] = $this->size;
192: }
193:
194: if (null !== $this->icons) {
195: $data['icons'] = array_map(static fn(Icon $icon): array => $icon->toArray(), $this->icons);
196: }
197:
198: $meta = $this->meta->toArray();
199:
200: if ([] !== $meta) {
201: $data['_meta'] = $meta;
202: }
203:
204: return $data;
205: }
206:
207: #[\Override]
208: public function jsonSerialize(): array
209: {
210: return $this->toArray();
211: }
212: }
213: