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