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