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