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\Arrayable;
19: use Nexus\Mcp\Core\Schema\Elicitation\ElicitRequest;
20: use Nexus\Mcp\Core\Schema\Enum\ResultType;
21: use Nexus\Mcp\Core\Schema\MetaObject;
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<MetaObject, Arrayable, 'T'>,
33: * resultType: non-empty-string,
34: * inputRequests?: array<string, array<string, mixed>>,
35: * requestState?: string,
36: * }>
37: *
38: * @see https://modelcontextprotocol.io/specification/draft/schema#inputrequiredresult
39: */
40: final readonly class InputRequiredResult extends Result implements ServerResult
41: {
42: /**
43: * @var null|array<string, InputRequest>
44: */
45: public ?array $inputRequests;
46:
47: /**
48: * @param null|array<string, InputRequest> $inputRequests
49: */
50: public function __construct(
51: ?array $inputRequests = null,
52: public ?string $requestState = null,
53: MetaObject $meta = new MetaObject(),
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: ->isMap('"result.inputRequests" must be a string-keyed object.')
64: ->values()
65: ->isInstanceOf(InputRequest::class, 'each "result.inputRequests" entry must be an InputRequest, {type} given.')
66: ;
67: }
68:
69: $this->inputRequests = $inputRequests;
70:
71: parent::__construct(meta: $meta);
72: }
73:
74: #[\Override]
75: public static function fromArray(array $data): static
76: {
77: $inputRequests = null;
78:
79: if (\array_key_exists('inputRequests', $data)) {
80: Assert::that($data['inputRequests'])
81: ->isArray('"result.inputRequests" must be an object, {type} given.')
82: ->isMap('"result.inputRequests" must be a string-keyed object.')
83: ->values()
84: ->isArray('each "result.inputRequests" entry must be an object, {type} given.')
85: ->isMap('each "result.inputRequests" entry must be a string-keyed object.')
86: ;
87: $inputRequests = array_map(self::decodeInputRequest(...), $data['inputRequests']);
88: }
89:
90: $requestState = null;
91:
92: if (\array_key_exists('requestState', $data)) {
93: Assert::that($data['requestState'])->isString('"result.requestState" must be a string, {type} given.');
94: $requestState = $data['requestState'];
95: }
96:
97: $meta = new MetaObject();
98:
99: if (\array_key_exists('_meta', $data)) {
100: Assert::that($data['_meta'])
101: ->isArray('"result._meta" must be an object, {type} given.')
102: ->isMap('"result._meta" must be a string-keyed object.')
103: ;
104: $meta = MetaObject::fromArray($data['_meta']);
105: }
106:
107: return new self(inputRequests: $inputRequests, requestState: $requestState, meta: $meta);
108: }
109:
110: #[\Override]
111: public function toArray(): array
112: {
113: $data = [];
114: $meta = $this->meta->toArray();
115:
116: if ([] !== $meta) {
117: $data['_meta'] = $meta;
118: }
119:
120: $data['resultType'] = self::getResultType();
121:
122: if (null !== $this->inputRequests) {
123: $data['inputRequests'] = array_map(
124: static fn(InputRequest $request): array => $request->toArray(),
125: $this->inputRequests,
126: );
127: }
128:
129: if (null !== $this->requestState) {
130: $data['requestState'] = $this->requestState;
131: }
132:
133: return $data;
134: }
135:
136: #[\Override]
137: protected function getResultType(): string
138: {
139: return ResultType::InputRequired->value;
140: }
141:
142: /**
143: * @param array<string, mixed> $request
144: */
145: private static function decodeInputRequest(array $request): InputRequest
146: {
147: Assert::that($request)->hasOffset('method', 'each "result.inputRequests" entry is missing the required "method" key.');
148:
149: return match ($request['method']) {
150: 'elicitation/create' => ElicitRequest::fromArray($request),
151: default => throw new \InvalidArgumentException(\sprintf(
152: 'each "result.inputRequests" entry must use a supported input-request method, %s given.',
153: SafeDisplay::sanitise(var_export($request['method'], true)),
154: )),
155: };
156: }
157: }
158: