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: * Content Security Policy allow-lists a UI resource declares for its sandbox.
21: *
22: * @implements Arrayable<array{
23: * connectDomains?: list<non-empty-string>,
24: * resourceDomains?: list<non-empty-string>,
25: * frameDomains?: list<non-empty-string>,
26: * baseUriDomains?: list<non-empty-string>,
27: * }>
28: *
29: * @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
30: */
31: final readonly class UiResourceCsp implements Arrayable
32: {
33: /**
34: * @param null|list<non-empty-string> $connectDomains Origins allowed for `connect-src`
35: * @param null|list<non-empty-string> $resourceDomains Origins allowed for static assets (`img-src`, `script-src`, and friends)
36: * @param null|list<non-empty-string> $frameDomains Origins allowed for `frame-src`
37: * @param null|list<non-empty-string> $baseUriDomains Origins allowed for `base-uri`
38: */
39: public function __construct(
40: public ?array $connectDomains = null,
41: public ?array $resourceDomains = null,
42: public ?array $frameDomains = null,
43: public ?array $baseUriDomains = null,
44: ) {
45: $this->assertDomainList($connectDomains, 'connectDomains');
46: $this->assertDomainList($resourceDomains, 'resourceDomains');
47: $this->assertDomainList($frameDomains, 'frameDomains');
48: $this->assertDomainList($baseUriDomains, 'baseUriDomains');
49: }
50:
51: #[\Override]
52: public static function fromArray(array $data): static
53: {
54: return new self(
55: connectDomains: self::parseDomainList($data, 'connectDomains'),
56: resourceDomains: self::parseDomainList($data, 'resourceDomains'),
57: frameDomains: self::parseDomainList($data, 'frameDomains'),
58: baseUriDomains: self::parseDomainList($data, 'baseUriDomains'),
59: );
60: }
61:
62: #[\Override]
63: public function toArray(): array
64: {
65: $data = [];
66:
67: if (null !== $this->connectDomains && [] !== $this->connectDomains) {
68: $data['connectDomains'] = $this->connectDomains;
69: }
70:
71: if (null !== $this->resourceDomains && [] !== $this->resourceDomains) {
72: $data['resourceDomains'] = $this->resourceDomains;
73: }
74:
75: if (null !== $this->frameDomains && [] !== $this->frameDomains) {
76: $data['frameDomains'] = $this->frameDomains;
77: }
78:
79: if (null !== $this->baseUriDomains && [] !== $this->baseUriDomains) {
80: $data['baseUriDomains'] = $this->baseUriDomains;
81: }
82:
83: return $data;
84: }
85:
86: #[\Override]
87: public function jsonSerialize(): array|\stdClass
88: {
89: $data = $this->toArray();
90:
91: return [] === $data ? new \stdClass() : $data;
92: }
93:
94: /**
95: * @param null|list<non-empty-string> $domains
96: * @param non-empty-string $slot
97: */
98: private function assertDomainList(?array $domains, string $slot): void
99: {
100: if (null !== $domains) {
101: Assert::that($domains)
102: ->isList(\sprintf('"_meta.ui.csp.%s" must be a list, {type} given.', $slot))
103: ->values()
104: ->isNonEmptyString(\sprintf('each "_meta.ui.csp.%s" must be a non-empty string, {type} given.', $slot))
105: ;
106: }
107: }
108:
109: /**
110: * @param array<string, mixed> $data
111: * @param non-empty-string $slot
112: *
113: * @return null|list<non-empty-string>
114: */
115: private static function parseDomainList(array $data, string $slot): ?array
116: {
117: if (! isset($data[$slot])) {
118: return null;
119: }
120:
121: $domains = $data[$slot];
122: Assert::that($domains)
123: ->isList(\sprintf('"_meta.ui.csp.%s" must be a list, {type} given.', $slot))
124: ->values()
125: ->isNonEmptyString(\sprintf('each "_meta.ui.csp.%s" must be a non-empty string, {type} given.', $slot))
126: ;
127:
128: return $domains;
129: }
130: }
131: