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\SamplingContentDispatcher;
18: use Nexus\Mcp\Core\Schema\ContentBlock\AudioContent;
19: use Nexus\Mcp\Core\Schema\ContentBlock\ImageContent;
20: use Nexus\Mcp\Core\Schema\ContentBlock\TextContent;
21: use Nexus\Mcp\Core\Schema\Enum\Role;
22: use Nexus\Mcp\Core\Schema\MetaObject;
23: use Nexus\Mcp\Core\Schema\Result;
24: use Nexus\Mcp\Core\Schema\Sampling\SamplingMessage;
25: use Nexus\Mcp\Core\Schema\Sampling\ToolResultContent;
26: use Nexus\Mcp\Core\Schema\Sampling\ToolUseContent;
27: use Nexus\Mcp\Core\Validation\EnumValueValidator;
28:
29: /**
30: * The client's response to a sampling/createMessage request from the server. The client should
31: * inform the user before returning the sampled message, to allow them to inspect the response
32: * (human in the loop) and decide whether to allow the server to see it.
33: *
34: * @phpstan-import-type ContentMember from SamplingMessage
35: *
36: * @see https://modelcontextprotocol.io/specification/2025-11-25/schema#createmessageresult
37: */
38: final readonly class CreateMessageResult extends Result implements ClientResult
39: {
40: /**
41: * @var non-empty-string
42: */
43: public string $model;
44:
45: /**
46: * @var ContentMember|list<ContentMember>
47: */
48: public array|AudioContent|ImageContent|TextContent|ToolResultContent|ToolUseContent $content;
49:
50: /**
51: * @var null|non-empty-string
52: */
53: public ?string $stopReason;
54:
55: /**
56: * @param ContentMember|list<ContentMember> $content
57: */
58: public function __construct(
59: string $model,
60: public Role $role,
61: array|AudioContent|ImageContent|TextContent|ToolResultContent|ToolUseContent $content,
62: ?string $stopReason = null,
63: MetaObject $meta = new MetaObject(),
64: ) {
65: Assert::that($model)->isNonEmptyString('"result.model" must be a non-empty string.');
66: Assert::that($stopReason)->nullOr()->isNonEmptyString('"result.stopReason" must be a non-empty string or null.');
67:
68: $this->model = $model;
69: $this->content = $content;
70: $this->stopReason = $stopReason;
71:
72: parent::__construct($meta);
73: }
74:
75: /**
76: * @param array<string, mixed> $data
77: */
78: #[\Override]
79: public static function fromArray(array $data): static
80: {
81: Assert::that($data)->hasOffset('model', '"result" missing the required "model" key.');
82: $model = $data['model'];
83: Assert::that($model)->isString('"result.model" must be a string, {type} given.');
84:
85: Assert::that($data)->hasOffset('role', '"result" missing the required "role" key.');
86: $role = EnumValueValidator::parse(Role::class, $data['role'], '"result.role"');
87:
88: Assert::that($data)->hasOffset('content', '"result" missing the required "content" key.');
89: Assert::that($data['content'])->isArray('"result.content" must be an object or array, {type} given.');
90:
91: if ([] === $data['content'] || array_is_list($data['content'])) {
92: Assert::that($data['content'])
93: ->values()
94: ->isArray('each "result.content" must be an object, {type} given.')
95: ->isMap('each "result.content" must be a string-keyed object.')
96: ;
97: $content = array_map(
98: static fn(array $entry): AudioContent|ImageContent|TextContent|ToolResultContent|ToolUseContent => SamplingContentDispatcher::fromArray($entry, '"result" content'),
99: $data['content'],
100: );
101: } else {
102: Assert::that($data['content'])->isMap('"result.content" must be a string-keyed object.');
103: $content = SamplingContentDispatcher::fromArray($data['content'], '"result" content');
104: }
105:
106: $stopReason = $data['stopReason'] ?? null;
107: Assert::that($stopReason)->nullOr()->isString('"result.stopReason" must be a string or null, {type} given.');
108:
109: $meta = new MetaObject();
110:
111: if (\array_key_exists('_meta', $data)) {
112: Assert::that($data['_meta'])
113: ->isArray('"result._meta" must be an object, {type} given.')
114: ->isMap('"result._meta" must be a string-keyed object.')
115: ;
116: $meta = MetaObject::fromArray($data['_meta']);
117: }
118:
119: return new self($model, $role, $content, $stopReason, $meta);
120: }
121:
122: #[\Override]
123: public function toArray(): array
124: {
125: $data = [
126: ...parent::toArray(),
127: 'model' => $this->model,
128: 'role' => $this->role->value,
129: ];
130:
131: if (\is_array($this->content)) {
132: $data['content'] = array_map(
133: static fn(AudioContent|ImageContent|TextContent|ToolResultContent|ToolUseContent $block): array => $block->toArray(),
134: $this->content,
135: );
136: } else {
137: $data['content'] = $this->content->toArray();
138: }
139:
140: if (null !== $this->stopReason) {
141: $data['stopReason'] = $this->stopReason;
142: }
143:
144: return $data;
145: }
146:
147: #[\Override]
148: public function jsonSerialize(): array
149: {
150: $data = $this->toArray();
151:
152: if (\is_array($this->content)) {
153: $data['content'] = array_map(
154: static fn(AudioContent|ImageContent|TextContent|ToolResultContent|ToolUseContent $block): array => $block->jsonSerialize(),
155: $this->content,
156: );
157: } else {
158: $data['content'] = $this->content->jsonSerialize();
159: }
160:
161: return $data;
162: }
163: }
164: