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\ParsesNumber;
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?: float,
42: * icons?: list<template-type<Icon, Arrayable, 'T'>>,
43: * _meta?: template-type<MetaObject, Arrayable, 'T'>,
44: * }>
45: *
46: * @see https://modelcontextprotocol.io/specification/2025-11-25/schema#resourcelink
47: */
48: final readonly class ResourceLink extends BaseMetadata implements Arrayable, ContentBlock, Icons
49: {
50: use ParsesNumber;
51:
52: public const string TYPE = 'resource_link';
53:
54: /**
55: * @var non-empty-string
56: */
57: public string $uri;
58:
59: /**
60: * @var null|non-empty-string
61: */
62: public ?string $description;
63:
64: /**
65: * @var null|non-empty-string
66: */
67: public ?string $mimeType;
68:
69: /**
70: * @var null|list<Icon>
71: */
72: public ?array $icons;
73:
74: /**
75: * @param null|list<Icon> $icons
76: */
77: public function __construct(
78: string $name,
79: string $uri,
80: ?string $title = null,
81: ?string $description = null,
82: ?string $mimeType = null,
83: public Annotations $annotations = new Annotations(),
84: public ?float $size = null,
85: ?array $icons = null,
86: public MetaObject $meta = new MetaObject(),
87: ) {
88: parent::__construct($name, $title);
89:
90: IdentifierNameValidator::validate($name, 'resource link "name"');
91: Rfc3986UriValidator::validate($uri, 'resource link "uri"');
92:
93: Assert::that($description)->nullOr()->isNonEmptyString('resource link "description" must be a non-empty string or null.');
94: Assert::that($mimeType)->nullOr()->isNonEmptyString('resource link "mimeType" must be a non-empty string or null.');
95:
96: if (null !== $icons) {
97: Assert::that($icons)->values()->isInstanceOf(Icon::class);
98: }
99:
100: $this->uri = $uri;
101: $this->description = $description;
102: $this->mimeType = $mimeType;
103: $this->icons = $icons;
104: }
105:
106: /**
107: * @param array<string, mixed> $data
108: */
109: #[\Override]
110: public static function fromArray(array $data): static
111: {
112: Assert::that($data)->hasOffset('type', 'resource link 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 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 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:
145: if (null !== $size) {
146: $size = self::parseNumber($size, 'resource link "size" must be a number or null, {type} given.');
147: }
148:
149: $icons = null;
150:
151: if (isset($data['icons'])) {
152: Assert::that($data['icons'])
153: ->isList('resource link "icons" must be a list, {type} given.')
154: ->values()
155: ->isArray('each resource link "icon" must be an object, {type} given.')
156: ->isMap('each resource link "icon" must be a string-keyed object.')
157: ;
158: $icons = array_map(Icon::fromArray(...), $data['icons']);
159: }
160:
161: $meta = new MetaObject();
162:
163: if (\array_key_exists('_meta', $data)) {
164: Assert::that($data['_meta'])
165: ->isArray('resource link "_meta" must be an object, {type} given.')
166: ->isMap('resource link "_meta" must be a string-keyed object.')
167: ;
168: $meta = MetaObject::fromArray($data['_meta']);
169: }
170:
171: return new self($name, $uri, $title, $description, $mimeType, $annotations, $size, $icons, $meta);
172: }
173:
174: #[\Override]
175: public function toArray(): array
176: {
177: $data = [
178: 'name' => $this->name,
179: 'type' => self::TYPE,
180: 'uri' => $this->uri,
181: ];
182:
183: if (null !== $this->title) {
184: $data['title'] = $this->title;
185: }
186:
187: if (null !== $this->description) {
188: $data['description'] = $this->description;
189: }
190:
191: if (null !== $this->mimeType) {
192: $data['mimeType'] = $this->mimeType;
193: }
194:
195: $annotations = $this->annotations->toArray();
196:
197: if ([] !== $annotations) {
198: $data['annotations'] = $annotations;
199: }
200:
201: if (null !== $this->size) {
202: $data['size'] = $this->size;
203: }
204:
205: if (null !== $this->icons) {
206: $data['icons'] = array_map(static fn(Icon $icon): array => $icon->toArray(), $this->icons);
207: }
208:
209: $meta = $this->meta->toArray();
210:
211: if ([] !== $meta) {
212: $data['_meta'] = $meta;
213: }
214:
215: return $data;
216: }
217:
218: #[\Override]
219: public function jsonSerialize(): array
220: {
221: return $this->toArray();
222: }
223: }
224: