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