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