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