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\Result;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\JsonRpc\InputRequestDispatcher;
18: use Nexus\Mcp\Core\Schema\Enum\ResultType;
19: use Nexus\Mcp\Core\Schema\MetaObject;
20: use Nexus\Mcp\Core\Schema\MetaObject\GenericResultMetaObject;
21: use Nexus\Mcp\Core\Schema\MetaObject\ResultMetaObject;
22: use Nexus\Mcp\Core\Schema\Request\InputRequest;
23: use Nexus\Mcp\Core\Schema\Result;
24:
25: /**
26: * An InputRequiredResult sent by the server to indicate that additional input is needed
27: * before the request can be completed.
28: *
29: * At least one of `inputRequests` or `requestState` MUST be present.
30: *
31: * @extends Result<array{
32: * _meta?: template-type<ResultMetaObject, MetaObject, 'T'>,
33: * resultType: non-empty-string,
34: * inputRequests?: array<int|non-empty-string, array<string, mixed>>,
35: * requestState?: string,
36: * }>
37: *
38: * @see https://modelcontextprotocol.io/specification/2026-07-28/schema#inputrequiredresult
39: */
40: final readonly class InputRequiredResult extends Result implements ServerResult
41: {
42: /**
43: * @var null|array<int|non-empty-string, InputRequest>
44: */
45: public ?array $inputRequests;
46:
47: /**
48: * @param null|array<int|non-empty-string, InputRequest> $inputRequests
49: */
50: public function __construct(
51: ?array $inputRequests = null,
52: public ?string $requestState = null,
53: ResultMetaObject $meta = new GenericResultMetaObject(),
54: ) {
55: $inputRequests = [] === $inputRequests ? null : $inputRequests;
56:
57: if (null === $inputRequests && null === $requestState) {
58: throw new \InvalidArgumentException('"result" must carry at least one of "inputRequests" or "requestState".');
59: }
60:
61: if (null !== $inputRequests) {
62: Assert::that($inputRequests)
63: ->keys()
64: ->isIntOrNonEmptyString('each "result.inputRequests" key must be an int or non-empty string.')
65: ;
66: Assert::that($inputRequests)
67: ->values()
68: ->isInstanceOf(InputRequest::class, 'each "result.inputRequests" entry must be an InputRequest, {type} given.')
69: ;
70: }
71:
72: $this->inputRequests = $inputRequests;
73:
74: parent::__construct(meta: $meta);
75: }
76:
77: #[\Override]
78: public static function fromArray(array $data): static
79: {
80: $inputRequests = null;
81:
82: if (\array_key_exists('inputRequests', $data)) {
83: Assert::that($data['inputRequests'])
84: ->isArray('"result.inputRequests" must be an object, {type} given.')
85: ->keys()->isIntOrNonEmptyString('each "result.inputRequests" key must be an int or non-empty string.')
86: ;
87: Assert::that($data['inputRequests'])
88: ->values()
89: ->isArray('each "result.inputRequests" entry must be an object, {type} given.')
90: ->isMap('each "result.inputRequests" entry must be a string-keyed object.')
91: ;
92: $inputRequests = array_map(InputRequestDispatcher::decode(...), $data['inputRequests']);
93: }
94:
95: $requestState = null;
96:
97: if (\array_key_exists('requestState', $data)) {
98: Assert::that($data['requestState'])->isString('"result.requestState" must be a string, {type} given.');
99: $requestState = $data['requestState'];
100: }
101:
102: $meta = new GenericResultMetaObject();
103:
104: if (\array_key_exists('_meta', $data)) {
105: Assert::that($data['_meta'])
106: ->isArray('"result._meta" must be an object, {type} given.')
107: ->not()->isNonEmptyList('"result._meta" must be a string-keyed object.')
108: ;
109: $meta = GenericResultMetaObject::fromArray($data['_meta']);
110: }
111:
112: return new self(inputRequests: $inputRequests, requestState: $requestState, meta: $meta);
113: }
114:
115: #[\Override]
116: public function toArray(): array
117: {
118: $data = [];
119: $meta = $this->meta->toArray();
120:
121: if ([] !== $meta) {
122: $data['_meta'] = $meta;
123: }
124:
125: $data['resultType'] = self::getResultType();
126:
127: if (null !== $this->inputRequests) {
128: $data['inputRequests'] = array_map(
129: static fn(InputRequest $request): array => $request->toArray(),
130: $this->inputRequests,
131: );
132: }
133:
134: if (null !== $this->requestState) {
135: $data['requestState'] = $this->requestState;
136: }
137:
138: return $data;
139: }
140:
141: #[\Override]
142: public function jsonSerialize(): array
143: {
144: $data = $this->toArray();
145:
146: if (null !== $this->inputRequests) {
147: $data['inputRequests'] = array_map(
148: static fn(InputRequest $request): array|\stdClass => $request->jsonSerialize(),
149: $this->inputRequests,
150: );
151:
152: if (array_is_list($this->inputRequests)) {
153: $data['inputRequests'] = (object) $data['inputRequests'];
154: }
155: }
156:
157: return $data;
158: }
159:
160: #[\Override]
161: public function rebuildWithMeta(ResultMetaObject $meta): static
162: {
163: return new self(
164: inputRequests: $this->inputRequests,
165: requestState: $this->requestState,
166: meta: $meta,
167: );
168: }
169:
170: #[\Override]
171: protected function getResultType(): string
172: {
173: return ResultType::InputRequired->value;
174: }
175: }
176: