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\Rfc6570UriTemplateValidator;
26:
27: /**
28: * A template description for resources available on the server.
29: *
30: * @implements Arrayable<array{
31: * name: non-empty-string,
32: * uriTemplate: 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: * 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#resourcetemplate
42: */
43: final readonly class ResourceTemplate extends BaseMetadata implements Arrayable, Icons
44: {
45: /**
46: * @var non-empty-string
47: */
48: public string $uriTemplate;
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 $uriTemplate,
71: ?string $title = null,
72: ?string $description = null,
73: ?string $mimeType = null,
74: public Annotations $annotations = new Annotations(),
75: ?array $icons = null,
76: public PayloadMetaObject $meta = new PayloadMetaObject(),
77: ) {
78: parent::__construct(name: $name, title: $title);
79:
80: IdentifierNameValidator::validate($name, 'resource template "name"');
81: Rfc6570UriTemplateValidator::validate($uriTemplate, 'resource template "uriTemplate"');
82:
83: Assert::that($description)->nullOr()->isNonEmptyString('resource template "description" must be a non-empty string or null.');
84: Assert::that($mimeType)->nullOr()->isNonEmptyString('resource template "mimeType" must be a non-empty string or null.');
85:
86: if (null !== $icons) {
87: Assert::that($icons)->values()->isInstanceOf(Icon::class);
88: }
89:
90: $this->uriTemplate = $uriTemplate;
91: $this->description = $description;
92: $this->mimeType = $mimeType;
93: $this->icons = $icons;
94: }
95:
96: #[\Override]
97: public static function fromArray(array $data): static
98: {
99: Assert::that($data)->hasOffset('name', 'resource template is missing the required "name" key.');
100: $name = $data['name'];
101: Assert::that($name)->isString('resource template "name" must be a string, {type} given.');
102:
103: Assert::that($data)->hasOffset('uriTemplate', 'resource template is missing the required "uriTemplate" key.');
104: $uriTemplate = $data['uriTemplate'];
105: Assert::that($uriTemplate)->isString('resource template "uriTemplate" must be a string, {type} given.');
106:
107: $title = $data['title'] ?? null;
108: Assert::that($title)->nullOr()->isString('resource template "title" must be a string or null, {type} given.');
109:
110: $description = $data['description'] ?? null;
111: Assert::that($description)->nullOr()->isString('resource template "description" must be a string or null, {type} given.');
112:
113: $mimeType = $data['mimeType'] ?? null;
114: Assert::that($mimeType)->nullOr()->isString('resource template "mimeType" must be a string or null, {type} given.');
115:
116: $annotations = new Annotations();
117:
118: if (\array_key_exists('annotations', $data)) {
119: Assert::that($data['annotations'])
120: ->isArray('resource template "annotations" must be an object, {type} given.')
121: ->isMap('resource template "annotations" must be a string-keyed object.')
122: ;
123: $annotations = Annotations::fromArray($data['annotations']);
124: }
125:
126: $icons = null;
127:
128: if (isset($data['icons'])) {
129: Assert::that($data['icons'])
130: ->isList('resource template "icons" must be a list, {type} given.')
131: ->values()
132: ->isArray('each resource template "icons" must be an object, {type} given.')
133: ->isMap('each resource template "icons" must be a string-keyed object.')
134: ;
135: $icons = array_map(Icon::fromArray(...), $data['icons']);
136: }
137:
138: $meta = new PayloadMetaObject();
139:
140: if (\array_key_exists('_meta', $data)) {
141: Assert::that($data['_meta'])
142: ->isArray('resource template "_meta" must be an object, {type} given.')
143: ->isMap('resource template "_meta" must be a string-keyed object.')
144: ;
145: $meta = PayloadMetaObject::fromArray($data['_meta']);
146: }
147:
148: return new self(
149: name: $name,
150: uriTemplate: $uriTemplate,
151: title: $title,
152: description: $description,
153: mimeType: $mimeType,
154: annotations: $annotations,
155: icons: $icons,
156: meta: $meta,
157: );
158: }
159:
160: #[\Override]
161: public function toArray(): array
162: {
163: $data = [
164: 'name' => $this->name,
165: 'uriTemplate' => $this->uriTemplate,
166: ];
167:
168: if (null !== $this->title) {
169: $data['title'] = $this->title;
170: }
171:
172: if (null !== $this->description) {
173: $data['description'] = $this->description;
174: }
175:
176: if (null !== $this->mimeType) {
177: $data['mimeType'] = $this->mimeType;
178: }
179:
180: $annotations = $this->annotations->toArray();
181:
182: if ([] !== $annotations) {
183: $data['annotations'] = $annotations;
184: }
185:
186: if (null !== $this->icons) {
187: $data['icons'] = array_map(static fn(Icon $icon): array => $icon->toArray(), $this->icons);
188: }
189:
190: $meta = $this->meta->toArray();
191:
192: if ([] !== $meta) {
193: $data['_meta'] = $meta;
194: }
195:
196: return $data;
197: }
198:
199: #[\Override]
200: public function jsonSerialize(): array
201: {
202: return $this->toArray();
203: }
204: }
205: