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: use Nexus\Mcp\Extension\Apps\Apps;
19: use Nexus\Mcp\Extension\Apps\Schema\Enum\ToolVisibility;
20:
21: /**
22: * The `_meta.ui` object on a tool, linking it to the UI resource that renders its results.
23: *
24: * @implements Arrayable<array{
25: * resourceUri?: non-empty-string,
26: * visibility?: list<'app'|'model'>,
27: * }>
28: *
29: * @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
30: */
31: final readonly class UiToolMeta implements Arrayable
32: {
33: /**
34: * @param null|non-empty-string $resourceUri
35: * @param null|list<ToolVisibility> $visibility Omission means the spec default `["model", "app"]`
36: */
37: public function __construct(
38: public ?string $resourceUri = null,
39: public ?array $visibility = null,
40: ) {
41: if (null !== $resourceUri) {
42: Assert::that($resourceUri)->startsWith(Apps::URI_PREFIX, \sprintf('"_meta.ui.resourceUri" must start with "%s".', Apps::URI_PREFIX));
43: }
44:
45: if (null !== $visibility) {
46: Assert::that($visibility)
47: ->isList('"_meta.ui.visibility" must be a list, {type} given.')
48: ->values()
49: ->isInstanceOf(ToolVisibility::class, 'each "_meta.ui.visibility" must be a tool visibility, {type} given.')
50: ;
51: }
52: }
53:
54: #[\Override]
55: public static function fromArray(array $data): static
56: {
57: $resourceUri = $data['resourceUri'] ?? null;
58: Assert::that($resourceUri)->nullOr()->isNonEmptyString('"_meta.ui.resourceUri" must be a non-empty string or null, {type} given.');
59:
60: $visibility = null;
61:
62: if (isset($data['visibility'])) {
63: Assert::that($data['visibility'])->isList('"_meta.ui.visibility" must be a list, {type} given.');
64: $visibility = array_map(
65: static function (mixed $entry): ToolVisibility {
66: Assert::that($entry)->isOneOf(array_column(ToolVisibility::cases(), 'value'), 'each "_meta.ui.visibility" must be one of {choices}, {value} given.');
67:
68: return ToolVisibility::from($entry);
69: },
70: $data['visibility'],
71: );
72: }
73:
74: return new self(resourceUri: $resourceUri, visibility: $visibility);
75: }
76:
77: #[\Override]
78: public function toArray(): array
79: {
80: $data = [];
81:
82: if (null !== $this->resourceUri) {
83: $data['resourceUri'] = $this->resourceUri;
84: }
85:
86: if (null !== $this->visibility) {
87: $data['visibility'] = array_map(static fn(ToolVisibility $visibility): string => $visibility->value, $this->visibility);
88: }
89:
90: return $data;
91: }
92:
93: #[\Override]
94: public function jsonSerialize(): array|\stdClass
95: {
96: $data = $this->toArray();
97:
98: return [] === $data ? new \stdClass() : $data;
99: }
100: }
101: