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