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\Elicitation;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Schema\Arrayable;
18:
19: /**
20: * Schema for a numeric elicitation field.
21: *
22: * @implements Arrayable<array{
23: * type: 'integer'|'number',
24: * title?: non-empty-string,
25: * description?: non-empty-string,
26: * minimum?: int,
27: * maximum?: int,
28: * default?: int,
29: * }>
30: *
31: * @see https://modelcontextprotocol.io/specification/2025-11-25/schema#numberschema
32: */
33: final readonly class NumberSchema implements Arrayable, PrimitiveSchemaDefinition
34: {
35: public const string TYPE = 'number';
36: public const string TYPE_INTEGER = 'integer';
37:
38: /**
39: * @var 'integer'|'number'
40: */
41: public string $type;
42:
43: /**
44: * @var null|non-empty-string
45: */
46: public ?string $title;
47:
48: /**
49: * @var null|non-empty-string
50: */
51: public ?string $description;
52:
53: public function __construct(
54: string $type = self::TYPE,
55: ?string $title = null,
56: ?string $description = null,
57: public ?int $minimum = null,
58: public ?int $maximum = null,
59: public ?int $default = null,
60: ) {
61: Assert::that($type)->isOneOf([self::TYPE, self::TYPE_INTEGER], 'number schema "type" must be one of {choices}.');
62: Assert::that($title)->nullOr()->isNonEmptyString('number schema "title" must be a non-empty string or null.');
63: Assert::that($description)->nullOr()->isNonEmptyString('number schema "description" must be a non-empty string or null.');
64:
65: $this->type = $type;
66: $this->title = $title;
67: $this->description = $description;
68: }
69:
70: /**
71: * @param array<string, mixed> $data
72: */
73: #[\Override]
74: public static function fromArray(array $data): static
75: {
76: Assert::that($data)->hasOffset('type', 'number schema missing the required "type" key.');
77: $type = $data['type'];
78: Assert::that($type)->isString('number schema "type" must be a string, {type} given.');
79:
80: $title = $data['title'] ?? null;
81: Assert::that($title)->nullOr()->isString('number schema "title" must be a string or null, {type} given.');
82:
83: $description = $data['description'] ?? null;
84: Assert::that($description)->nullOr()->isString('number schema "description" must be a string or null, {type} given.');
85:
86: $minimum = $data['minimum'] ?? null;
87: Assert::that($minimum)->nullOr()->isInt('number schema "minimum" must be an int or null, {type} given.');
88:
89: $maximum = $data['maximum'] ?? null;
90: Assert::that($maximum)->nullOr()->isInt('number schema "maximum" must be an int or null, {type} given.');
91:
92: $default = $data['default'] ?? null;
93: Assert::that($default)->nullOr()->isInt('number schema "default" must be an int or null, {type} given.');
94:
95: return new self($type, $title, $description, $minimum, $maximum, $default);
96: }
97:
98: #[\Override]
99: public function toArray(): array
100: {
101: $data = ['type' => $this->type];
102:
103: if (null !== $this->title) {
104: $data['title'] = $this->title;
105: }
106:
107: if (null !== $this->description) {
108: $data['description'] = $this->description;
109: }
110:
111: if (null !== $this->minimum) {
112: $data['minimum'] = $this->minimum;
113: }
114:
115: if (null !== $this->maximum) {
116: $data['maximum'] = $this->maximum;
117: }
118:
119: if (null !== $this->default) {
120: $data['default'] = $this->default;
121: }
122:
123: return $data;
124: }
125:
126: #[\Override]
127: public function jsonSerialize(): array
128: {
129: return $this->toArray();
130: }
131: }
132: