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\Tool;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Schema\Arrayable;
18:
19: /**
20: * Additional properties describing a Tool to clients.
21: *
22: * NOTE: all properties in ToolAnnotations are **hints**.
23: * They are not guaranteed to provide a faithful description of
24: * tool behavior (including descriptive properties like `title`).
25: *
26: * Clients should never make tool use decisions based on ToolAnnotations
27: * received from untrusted servers.
28: *
29: * @implements Arrayable<array{
30: * title?: non-empty-string,
31: * readOnlyHint?: bool,
32: * destructiveHint?: bool,
33: * idempotentHint?: bool,
34: * openWorldHint?: bool,
35: * }>
36: *
37: * @see https://modelcontextprotocol.io/specification/2025-11-25/schema#toolannotations
38: */
39: final readonly class ToolAnnotations implements Arrayable
40: {
41: /**
42: * @var null|non-empty-string
43: */
44: public ?string $title;
45:
46: public function __construct(
47: ?string $title = null,
48: public ?bool $readOnlyHint = null,
49: public ?bool $destructiveHint = null,
50: public ?bool $idempotentHint = null,
51: public ?bool $openWorldHint = null,
52: ) {
53: Assert::that($title)->nullOr()->isNonEmptyString('"annotations.title" must be a non-empty string or null.');
54:
55: $this->title = $title;
56:
57: if (true === $this->readOnlyHint) {
58: Assert::that($this->destructiveHint)->isNull('"annotations.destructiveHint" must be null when "readOnlyHint" is true; the spec defines it only when readOnlyHint == false.');
59: Assert::that($this->idempotentHint)->isNull('"annotations.idempotentHint" must be null when "readOnlyHint" is true; the spec defines it only when readOnlyHint == false.');
60: }
61: }
62:
63: /**
64: * @param array<string, mixed> $data
65: */
66: #[\Override]
67: public static function fromArray(array $data): static
68: {
69: $title = $data['title'] ?? null;
70: Assert::that($title)->nullOr()->isString('"annotations.title" must be a string or null, {type} given.');
71:
72: $readOnlyHint = $data['readOnlyHint'] ?? null;
73: Assert::that($readOnlyHint)->nullOr()->isBool('"annotations.readOnlyHint" must be a bool or null, {type} given.');
74:
75: $destructiveHint = $data['destructiveHint'] ?? null;
76: Assert::that($destructiveHint)->nullOr()->isBool('"annotations.destructiveHint" must be a bool or null, {type} given.');
77:
78: $idempotentHint = $data['idempotentHint'] ?? null;
79: Assert::that($idempotentHint)->nullOr()->isBool('"annotations.idempotentHint" must be a bool or null, {type} given.');
80:
81: $openWorldHint = $data['openWorldHint'] ?? null;
82: Assert::that($openWorldHint)->nullOr()->isBool('"annotations.openWorldHint" must be a bool or null, {type} given.');
83:
84: return new self($title, $readOnlyHint, $destructiveHint, $idempotentHint, $openWorldHint);
85: }
86:
87: #[\Override]
88: public function toArray(): array
89: {
90: $data = [];
91:
92: if (null !== $this->title) {
93: $data['title'] = $this->title;
94: }
95:
96: if (null !== $this->readOnlyHint) {
97: $data['readOnlyHint'] = $this->readOnlyHint;
98: }
99:
100: if (null !== $this->destructiveHint) {
101: $data['destructiveHint'] = $this->destructiveHint;
102: }
103:
104: if (null !== $this->idempotentHint) {
105: $data['idempotentHint'] = $this->idempotentHint;
106: }
107:
108: if (null !== $this->openWorldHint) {
109: $data['openWorldHint'] = $this->openWorldHint;
110: }
111:
112: return $data;
113: }
114:
115: #[\Override]
116: public function jsonSerialize(): array|\stdClass
117: {
118: $data = $this->toArray();
119:
120: return [] === $data ? new \stdClass() : $data;
121: }
122: }
123: