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\Sampling;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\JsonRpc\SamplingContentDispatcher;
18: use Nexus\Mcp\Core\Schema\Arrayable;
19: use Nexus\Mcp\Core\Schema\ContentBlock\AudioContent;
20: use Nexus\Mcp\Core\Schema\ContentBlock\ImageContent;
21: use Nexus\Mcp\Core\Schema\ContentBlock\TextContent;
22: use Nexus\Mcp\Core\Schema\Enum\Role;
23: use Nexus\Mcp\Core\Schema\MetaObject;
24: use Nexus\Mcp\Core\Validation\EnumValueValidator;
25:
26: /**
27: * Describes a message issued to or received from an LLM API.
28: *
29: * @phpstan-type ContentMember AudioContent|ImageContent|TextContent|ToolResultContent|ToolUseContent
30: *
31: * @implements Arrayable<array{
32: * content: array<string, mixed>|list<array<string, mixed>>,
33: * role: value-of<Role>,
34: * _meta?: template-type<MetaObject, Arrayable, 'T'>,
35: * }>
36: *
37: * @see https://modelcontextprotocol.io/specification/2025-11-25/schema#samplingmessage
38: */
39: final readonly class SamplingMessage implements Arrayable
40: {
41: /**
42: * @var ContentMember|list<ContentMember>
43: */
44: public array|AudioContent|ImageContent|TextContent|ToolResultContent|ToolUseContent $content;
45:
46: /**
47: * @param ContentMember|list<ContentMember> $content
48: */
49: public function __construct(
50: public Role $role,
51: array|AudioContent|ImageContent|TextContent|ToolResultContent|ToolUseContent $content,
52: public MetaObject $meta = new MetaObject(),
53: ) {
54: if (\is_array($content)) {
55: Assert::that($content)->values()->isInstanceOf(SamplingMessageContentBlock::class);
56: }
57:
58: $this->content = $content;
59: }
60:
61: /**
62: * @param array<string, mixed> $data
63: */
64: #[\Override]
65: public static function fromArray(array $data): static
66: {
67: Assert::that($data)->hasOffset('role', 'sampling message missing the required "role" key.');
68: $role = EnumValueValidator::parse(Role::class, $data['role'], 'sampling message "role"');
69:
70: Assert::that($data)->hasOffset('content', 'sampling message missing the required "content" key.');
71: Assert::that($data['content'])->isArray('sampling message "content" must be an object or array, {type} given.');
72:
73: if ([] === $data['content'] || array_is_list($data['content'])) {
74: Assert::that($data['content'])
75: ->values()
76: ->isArray('each sampling message "content" must be an object, {type} given.')
77: ->isMap('each sampling message "content" must be a string-keyed object.')
78: ;
79: $content = array_map(
80: static fn(array $entry): AudioContent|ImageContent|TextContent|ToolResultContent|ToolUseContent => SamplingContentDispatcher::fromArray($entry, 'sampling message "content"'),
81: $data['content'],
82: );
83: } else {
84: Assert::that($data['content'])->isMap('sampling message "content" must be a string-keyed object.');
85: $content = SamplingContentDispatcher::fromArray($data['content'], 'sampling message "content"');
86: }
87:
88: $meta = new MetaObject();
89:
90: if (\array_key_exists('_meta', $data)) {
91: Assert::that($data['_meta'])
92: ->isArray('sampling message "_meta" must be an object, {type} given.')
93: ->isMap('sampling message "_meta" must be a string-keyed object.')
94: ;
95: $meta = MetaObject::fromArray($data['_meta']);
96: }
97:
98: return new self($role, $content, $meta);
99: }
100:
101: #[\Override]
102: public function toArray(): array
103: {
104: $data = ['role' => $this->role->value];
105:
106: if (\is_array($this->content)) {
107: $data['content'] = array_map(
108: static fn(AudioContent|ImageContent|TextContent|ToolResultContent|ToolUseContent $block): array => $block->toArray(),
109: $this->content,
110: );
111: } else {
112: $data['content'] = $this->content->toArray();
113: }
114:
115: $meta = $this->meta->toArray();
116:
117: if ([] !== $meta) {
118: $data['_meta'] = $meta;
119: }
120:
121: return $data;
122: }
123:
124: #[\Override]
125: public function jsonSerialize(): array
126: {
127: $data = ['role' => $this->role->value];
128:
129: if (\is_array($this->content)) {
130: $data['content'] = array_map(
131: static fn(AudioContent|ImageContent|TextContent|ToolResultContent|ToolUseContent $block): array => $block->jsonSerialize(),
132: $this->content,
133: );
134: } else {
135: $data['content'] = $this->content->jsonSerialize();
136: }
137:
138: $meta = $this->meta->toArray();
139:
140: if ([] !== $meta) {
141: $data['_meta'] = $meta;
142: }
143:
144: return $data;
145: }
146: }
147: