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\ContentBlockDispatcher;
18: use Nexus\Mcp\Core\Schema\Arrayable;
19: use Nexus\Mcp\Core\Schema\ContentBlock\AudioContent;
20: use Nexus\Mcp\Core\Schema\ContentBlock\EmbeddedResource;
21: use Nexus\Mcp\Core\Schema\ContentBlock\ImageContent;
22: use Nexus\Mcp\Core\Schema\ContentBlock\ResourceLink;
23: use Nexus\Mcp\Core\Schema\ContentBlock\TextContent;
24: use Nexus\Mcp\Core\Schema\MetaObject;
25:
26: /**
27: * The result of a tool use, provided by the user back to the assistant.
28: *
29: * @implements Arrayable<array{
30: * content: list<
31: * template-type<TextContent, Arrayable, 'T'>
32: * | template-type<ImageContent, Arrayable, 'T'>
33: * | template-type<AudioContent, Arrayable, 'T'>
34: * | template-type<ResourceLink, Arrayable, 'T'>
35: * | template-type<EmbeddedResource, Arrayable, 'T'>
36: * >,
37: * toolUseId: non-empty-string,
38: * type: 'tool_result',
39: * isError?: bool,
40: * structuredContent?: array<string, mixed>,
41: * _meta?: template-type<MetaObject, Arrayable, 'T'>,
42: * }>
43: *
44: * @see https://modelcontextprotocol.io/specification/2025-11-25/schema#toolresultcontent
45: */
46: final readonly class ToolResultContent implements Arrayable, SamplingMessageContentBlock
47: {
48: public const string TYPE = 'tool_result';
49:
50: /**
51: * @var non-empty-string
52: */
53: public string $toolUseId;
54:
55: /**
56: * @param list<AudioContent|EmbeddedResource|ImageContent|ResourceLink|TextContent> $content
57: * @param null|array<string, mixed> $structuredContent
58: */
59: public function __construct(
60: string $toolUseId,
61: public array $content,
62: public ?bool $isError = null,
63: public ?array $structuredContent = null,
64: public MetaObject $meta = new MetaObject(),
65: ) {
66: Assert::that($toolUseId)->isNonEmptyString('"content.toolUseId" must be a non-empty string.');
67: Assert::that($content)->values()->isInstanceOf(Arrayable::class);
68:
69: $this->toolUseId = $toolUseId;
70: }
71:
72: /**
73: * @param array<string, mixed> $data
74: */
75: #[\Override]
76: public static function fromArray(array $data): static
77: {
78: Assert::that($data)->hasOffset('type', '"content" missing the required "type" key.');
79: $type = $data['type'];
80: Assert::that($type)->isIdentical(self::TYPE, '"content.type" must be {other}, {value} given.');
81:
82: Assert::that($data)->hasOffset('toolUseId', '"content" missing the required "toolUseId" key.');
83: $toolUseId = $data['toolUseId'];
84: Assert::that($toolUseId)->isString('"content.toolUseId" must be a string, {type} given.');
85:
86: Assert::that($data)->hasOffset('content', '"content" missing the required "content" key.');
87: Assert::that($data['content'])
88: ->isList('"content.content" must be a list, {type} given.')
89: ->values()
90: ->isArray('each "content.content" must be an object, {type} given.')
91: ->isMap('each "content.content" must be a string-keyed object.')
92: ;
93: $content = array_map(
94: static fn(array $entry): AudioContent|EmbeddedResource|ImageContent|ResourceLink|TextContent => ContentBlockDispatcher::fromArray($entry, 'ToolResultContent content'),
95: $data['content'],
96: );
97:
98: $isError = $data['isError'] ?? null;
99: Assert::that($isError)->nullOr()->isBool('"content.isError" must be a bool or null, {type} given.');
100:
101: $structuredContent = null;
102:
103: if (\array_key_exists('structuredContent', $data)) {
104: Assert::that($data['structuredContent'])
105: ->isArray('"content.structuredContent" must be an object, {type} given.')
106: ->isMap('"content.structuredContent" must be a string-keyed object.')
107: ;
108: $structuredContent = $data['structuredContent'];
109: }
110:
111: $meta = new MetaObject();
112:
113: if (\array_key_exists('_meta', $data)) {
114: Assert::that($data['_meta'])
115: ->isArray('"content._meta" must be an object, {type} given.')
116: ->isMap('"content._meta" must be a string-keyed object.')
117: ;
118: $meta = MetaObject::fromArray($data['_meta']);
119: }
120:
121: return new self($toolUseId, $content, $isError, $structuredContent, $meta);
122: }
123:
124: #[\Override]
125: public function toArray(): array
126: {
127: $data = [
128: 'content' => array_map(static fn(AudioContent|EmbeddedResource|ImageContent|ResourceLink|TextContent $block): array => $block->toArray(), $this->content),
129: 'toolUseId' => $this->toolUseId,
130: 'type' => self::TYPE,
131: ];
132:
133: if (null !== $this->isError) {
134: $data['isError'] = $this->isError;
135: }
136:
137: if (null !== $this->structuredContent) {
138: $data['structuredContent'] = $this->structuredContent;
139: }
140:
141: $meta = $this->meta->toArray();
142:
143: if ([] !== $meta) {
144: $data['_meta'] = $meta;
145: }
146:
147: return $data;
148: }
149:
150: #[\Override]
151: public function jsonSerialize(): array
152: {
153: $data = $this->toArray();
154:
155: if ([] === $this->structuredContent) {
156: $data['structuredContent'] = new \stdClass();
157: }
158:
159: return $data;
160: }
161: }
162: