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