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\Extension\Apps\Schema;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Schema\Arrayable;
18:
19: /**
20: * The `_meta.ui` object on a UI resource, carried by both its `resources/list`
21: * descriptor and its `resources/read` contents.
22: *
23: * @implements Arrayable<array{
24: * csp?: array{
25: * connectDomains?: list<non-empty-string>,
26: * resourceDomains?: list<non-empty-string>,
27: * frameDomains?: list<non-empty-string>,
28: * baseUriDomains?: list<non-empty-string>,
29: * },
30: * permissions?: array{
31: * camera?: \stdClass,
32: * microphone?: \stdClass,
33: * geolocation?: \stdClass,
34: * clipboardWrite?: \stdClass,
35: * },
36: * domain?: non-empty-string,
37: * prefersBorder?: bool,
38: * }>
39: *
40: * @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
41: */
42: final readonly class UiResourceMeta implements Arrayable
43: {
44: public ?UiResourceCsp $csp;
45: public ?UiResourcePermissions $permissions;
46:
47: /**
48: * @param null|non-empty-string $domain Host-defined dedicated origin for the view sandbox
49: * @param null|bool $prefersBorder Whether the host should draw a visible border and background
50: */
51: public function __construct(
52: ?UiResourceCsp $csp = null,
53: ?UiResourcePermissions $permissions = null,
54: public ?string $domain = null,
55: public ?bool $prefersBorder = null,
56: ) {
57: Assert::that($domain)->nullOr()->isNonEmptyString('"_meta.ui.domain" must be a non-empty string or null.');
58:
59: $this->csp = null === $csp || [] === $csp->toArray() ? null : $csp;
60: $this->permissions = null === $permissions || [] === $permissions->toArray() ? null : $permissions;
61: }
62:
63: #[\Override]
64: public static function fromArray(array $data): static
65: {
66: $csp = null;
67:
68: if (isset($data['csp'])) {
69: Assert::that($data['csp'])
70: ->isArray('"_meta.ui.csp" must be an array, {type} given.')
71: ->isMap('"_meta.ui.csp" must be a string-keyed object.')
72: ;
73: $csp = UiResourceCsp::fromArray($data['csp']);
74: }
75:
76: $permissions = null;
77:
78: if (isset($data['permissions'])) {
79: Assert::that($data['permissions'])
80: ->isArray('"_meta.ui.permissions" must be an array, {type} given.')
81: ->isMap('"_meta.ui.permissions" must be a string-keyed object.')
82: ;
83: $permissions = UiResourcePermissions::fromArray($data['permissions']);
84: }
85:
86: $domain = $data['domain'] ?? null;
87: Assert::that($domain)->nullOr()->isNonEmptyString('"_meta.ui.domain" must be a non-empty string or null.');
88:
89: $prefersBorder = $data['prefersBorder'] ?? null;
90: Assert::that($prefersBorder)->nullOr()->isBool('"_meta.ui.prefersBorder" must be a bool or null, {type} given.');
91:
92: return new self(csp: $csp, permissions: $permissions, domain: $domain, prefersBorder: $prefersBorder);
93: }
94:
95: #[\Override]
96: public function toArray(): array
97: {
98: $data = [];
99:
100: if (null !== $this->csp) {
101: $data['csp'] = $this->csp->toArray();
102: }
103:
104: if (null !== $this->permissions) {
105: $data['permissions'] = $this->permissions->toArray();
106: }
107:
108: if (null !== $this->domain) {
109: $data['domain'] = $this->domain;
110: }
111:
112: if (null !== $this->prefersBorder) {
113: $data['prefersBorder'] = $this->prefersBorder;
114: }
115:
116: return $data;
117: }
118:
119: #[\Override]
120: public function jsonSerialize(): array|\stdClass
121: {
122: $data = $this->toArray();
123:
124: return [] === $data ? new \stdClass() : $data;
125: }
126: }
127: