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