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\RequestParams;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Schema\Enum\IncludeContext;
18: use Nexus\Mcp\Core\Schema\ParsesNumber;
19: use Nexus\Mcp\Core\Schema\RequestMetaObject;
20: use Nexus\Mcp\Core\Schema\RequestParams;
21: use Nexus\Mcp\Core\Schema\Sampling\ModelPreferences;
22: use Nexus\Mcp\Core\Schema\Sampling\SamplingMessage;
23: use Nexus\Mcp\Core\Schema\Sampling\ToolChoice;
24: use Nexus\Mcp\Core\Schema\Task\TaskMetadata;
25: use Nexus\Mcp\Core\Schema\Tool\Tool;
26: use Nexus\Mcp\Core\Validation\EnumValueValidator;
27:
28: /**
29: * Parameters for a `sampling/createMessage` request.
30: *
31: * @see https://modelcontextprotocol.io/specification/2025-11-25/schema#createmessagerequestparams
32: */
33: final readonly class CreateMessageRequestParams extends RequestParams
34: {
35: use ParsesNumber;
36:
37: /**
38: * @var list<SamplingMessage>
39: */
40: public array $messages;
41:
42: /**
43: * @var null|non-empty-string
44: */
45: public ?string $systemPrompt;
46:
47: /**
48: * @var null|list<string>
49: */
50: public ?array $stopSequences;
51:
52: /**
53: * @var null|array<string, mixed>
54: */
55: public ?array $metadata;
56:
57: /**
58: * @var null|list<Tool>
59: */
60: public ?array $tools;
61:
62: /**
63: * @param list<SamplingMessage> $messages
64: * @param null|list<string> $stopSequences
65: * @param null|list<Tool> $tools
66: * @param null|array<string, mixed> $metadata
67: */
68: public function __construct(
69: public int $maxTokens,
70: array $messages,
71: public ?IncludeContext $includeContext = null,
72: public ModelPreferences $modelPreferences = new ModelPreferences(),
73: ?array $stopSequences = null,
74: ?string $systemPrompt = null,
75: public ?TaskMetadata $task = null,
76: public ?float $temperature = null,
77: public ToolChoice $toolChoice = new ToolChoice(),
78: ?array $tools = null,
79: ?array $metadata = null,
80: RequestMetaObject $meta = new RequestMetaObject(),
81: ) {
82: Assert::that($messages)->values()->isInstanceOf(SamplingMessage::class);
83: Assert::that($systemPrompt)->nullOr()->isNonEmptyString('"params.systemPrompt" must be a non-empty string or null.');
84:
85: if (null !== $stopSequences) {
86: Assert::that($stopSequences)->values()->isString('each "params.stopSequences" entry must be a string, {type} given.');
87: }
88:
89: if (null !== $tools) {
90: Assert::that($tools)->values()->isInstanceOf(Tool::class);
91: }
92:
93: $this->messages = $messages;
94: $this->systemPrompt = $systemPrompt;
95: $this->stopSequences = $stopSequences;
96: $this->metadata = $metadata;
97: $this->tools = $tools;
98:
99: parent::__construct($meta);
100: }
101:
102: /**
103: * @param array<string, mixed> $data
104: */
105: #[\Override]
106: public static function fromArray(array $data): static
107: {
108: Assert::that($data)->hasOffset('maxTokens', 'missing the required "maxTokens" key.');
109: $maxTokens = $data['maxTokens'];
110: Assert::that($maxTokens)->isInt('"params.maxTokens" must be an int, {type} given.');
111:
112: Assert::that($data)->hasOffset('messages', 'missing the required "messages" key.');
113: Assert::that($data['messages'])
114: ->isList('"params.messages" must be a list, non-list array given.')
115: ->values()
116: ->isArray('each "params.message" must be an object, {type} given.')
117: ->isMap('each "params.message" must be a string-keyed object.')
118: ;
119: $messages = array_map(SamplingMessage::fromArray(...), $data['messages']);
120:
121: $includeContext = null;
122:
123: if (\array_key_exists('includeContext', $data)) {
124: $includeContext = EnumValueValidator::parse(IncludeContext::class, $data['includeContext'], '"params.includeContext"');
125: }
126:
127: $modelPreferences = new ModelPreferences();
128:
129: if (\array_key_exists('modelPreferences', $data)) {
130: Assert::that($data['modelPreferences'])
131: ->isArray('"params.modelPreferences" must be an object, {type} given.')
132: ->isMap('"params.modelPreferences" must be a string-keyed object.')
133: ;
134: $modelPreferences = ModelPreferences::fromArray($data['modelPreferences']);
135: }
136:
137: $stopSequences = null;
138:
139: if (\array_key_exists('stopSequences', $data)) {
140: Assert::that($data['stopSequences'])
141: ->isList('"params.stopSequences" must be a list, non-list array given.')
142: ->values()->isString('each "params.stopSequences" must be a string, {type} given.')
143: ;
144: $stopSequences = $data['stopSequences'];
145: }
146:
147: $systemPrompt = $data['systemPrompt'] ?? null;
148: Assert::that($systemPrompt)->nullOr()->isString('"params.systemPrompt" must be a string or null, {type} given.');
149:
150: $task = null;
151:
152: if (\array_key_exists('task', $data)) {
153: Assert::that($data['task'])
154: ->isArray('"params.task" must be an object, {type} given.')
155: ->isMap('"params.task" must be a string-keyed object.')
156: ;
157: $task = TaskMetadata::fromArray($data['task']);
158: }
159:
160: $temperature = $data['temperature'] ?? null;
161:
162: if (null !== $temperature) {
163: $temperature = self::parseNumber($temperature, '"params.temperature" must be a number or null, {type} given.');
164: }
165:
166: $toolChoice = new ToolChoice();
167:
168: if (\array_key_exists('toolChoice', $data)) {
169: Assert::that($data['toolChoice'])
170: ->isArray('"params.toolChoice" must be an object, {type} given.')
171: ->isMap('"params.toolChoice" must be a string-keyed object.')
172: ;
173: $toolChoice = ToolChoice::fromArray($data['toolChoice']);
174: }
175:
176: $tools = null;
177:
178: if (\array_key_exists('tools', $data)) {
179: Assert::that($data['tools'])
180: ->isList('"params.tools" must be a list, non-list array given.')
181: ->values()
182: ->isArray('each "params.tool" must be an object, {type} given.')
183: ->isMap('each "params.tool" must be a string-keyed object.')
184: ;
185: $tools = array_map(Tool::fromArray(...), $data['tools']);
186: }
187:
188: $metadata = null;
189:
190: if (\array_key_exists('metadata', $data)) {
191: Assert::that($data['metadata'])
192: ->isArray('"params.metadata" must be an object, {type} given.')
193: ->isMap('"params.metadata" must be a string-keyed object.')
194: ;
195: $metadata = $data['metadata'];
196: }
197:
198: $meta = new RequestMetaObject();
199:
200: if (\array_key_exists('_meta', $data)) {
201: Assert::that($data['_meta'])
202: ->isArray('"params._meta" must be an object, {type} given.')
203: ->isMap('"params._meta" must be a string-keyed object.')
204: ;
205: $meta = RequestMetaObject::fromArray($data['_meta']);
206: }
207:
208: return new self(
209: $maxTokens,
210: $messages,
211: $includeContext,
212: $modelPreferences,
213: $stopSequences,
214: $systemPrompt,
215: $task,
216: $temperature,
217: $toolChoice,
218: $tools,
219: $metadata,
220: $meta,
221: );
222: }
223:
224: #[\Override]
225: public function toArray(): array
226: {
227: $data = [
228: ...parent::toArray(),
229: 'maxTokens' => $this->maxTokens,
230: 'messages' => array_map(static fn(SamplingMessage $m): array => $m->toArray(), $this->messages),
231: ];
232:
233: if (null !== $this->includeContext) {
234: $data['includeContext'] = $this->includeContext->value;
235: }
236:
237: $modelPreferences = $this->modelPreferences->toArray();
238:
239: if ([] !== $modelPreferences) {
240: $data['modelPreferences'] = $modelPreferences;
241: }
242:
243: if (null !== $this->stopSequences) {
244: $data['stopSequences'] = $this->stopSequences;
245: }
246:
247: if (null !== $this->systemPrompt) {
248: $data['systemPrompt'] = $this->systemPrompt;
249: }
250:
251: if (null !== $this->task) {
252: $data['task'] = $this->task->toArray();
253: }
254:
255: if (null !== $this->temperature) {
256: $data['temperature'] = $this->temperature;
257: }
258:
259: $toolChoice = $this->toolChoice->toArray();
260:
261: if ([] !== $toolChoice) {
262: $data['toolChoice'] = $toolChoice;
263: }
264:
265: if (null !== $this->tools) {
266: $data['tools'] = array_map(static fn(Tool $t): array => $t->toArray(), $this->tools);
267: }
268:
269: if (null !== $this->metadata) {
270: $data['metadata'] = $this->metadata;
271: }
272:
273: return $data;
274: }
275:
276: #[\Override]
277: public function jsonSerialize(): array
278: {
279: $data = $this->toArray();
280:
281: $data['messages'] = array_map(
282: static fn(SamplingMessage $m): array => $m->jsonSerialize(),
283: $this->messages,
284: );
285:
286: if ([] !== $this->modelPreferences->toArray()) {
287: $data['modelPreferences'] = $this->modelPreferences->jsonSerialize();
288: }
289:
290: if (null !== $this->task) {
291: $data['task'] = $this->task->jsonSerialize();
292: }
293:
294: if ([] !== $this->toolChoice->toArray()) {
295: $data['toolChoice'] = $this->toolChoice->jsonSerialize();
296: }
297:
298: if ([] === $this->metadata) {
299: $data['metadata'] = new \stdClass();
300: }
301:
302: return $data;
303: }
304: }
305: