Skip to content

Commit cfbfff8

Browse files
committed
Added basic StructuredContent and error passing
1 parent 345b94d commit cfbfff8

File tree

4 files changed

+84
-26
lines changed

4 files changed

+84
-26
lines changed

src/Capability/Tool/ToolCaller.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Mcp\Schema\Content\AudioContent;
1919
use Mcp\Schema\Content\EmbeddedResource;
2020
use Mcp\Schema\Content\ImageContent;
21+
use Mcp\Schema\Content\StructuredContent;
2122
use Mcp\Schema\Content\TextContent;
2223
use Mcp\Schema\Request\CallToolRequest;
2324
use Mcp\Schema\Result\CallToolResult;
@@ -59,15 +60,24 @@ public function call(CallToolRequest $request): CallToolResult
5960

6061
try {
6162
$result = $this->referenceHandler->handle($toolReference, $arguments);
62-
/** @var TextContent[]|ImageContent[]|EmbeddedResource[]|AudioContent[] $formattedResult */
63+
64+
if ($result instanceof CallToolResult) {
65+
$this->logger->debug('Tool executed successfully', [
66+
'name' => $toolName,
67+
'result_type' => \gettype($result),
68+
]);
69+
70+
return $result;
71+
}
72+
/** @var array<int, TextContent|ImageContent|EmbeddedResource|AudioContent|StructuredContent> $formattedResult */
6373
$formattedResult = $toolReference->formatResult($result);
6474

6575
$this->logger->debug('Tool executed successfully', [
6676
'name' => $toolName,
6777
'result_type' => \gettype($result),
6878
]);
6979

70-
return new CallToolResult($formattedResult);
80+
return CallToolResult::fromArray($formattedResult);
7181
} catch (\Throwable $e) {
7282
$this->logger->error('Tool execution failed', [
7383
'name' => $toolName,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Schema\Content;
13+
14+
class StructuredContent extends Content
15+
{
16+
/**
17+
* @param mixed[] $data
18+
*/
19+
public function __construct(
20+
private array $data = [],
21+
) {
22+
parent::__construct('structured');
23+
}
24+
25+
public function jsonSerialize(): mixed
26+
{
27+
return $this->data;
28+
}
29+
}

src/Schema/Content/TextContent.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
* type: 'text',
2424
* text: string,
2525
* annotations?: AnnotationsData,
26+
* isError: bool
2627
* }
2728
*
2829
* @author Kyrian Obikwelu <[email protected]>
@@ -34,10 +35,12 @@ class TextContent extends Content
3435
*
3536
* @param mixed $text The value to convert to text
3637
* @param ?Annotations $annotations Optional annotations describing the content
38+
* @param bool $isError Optional, mark response as error
3739
*/
3840
public function __construct(
3941
public mixed $text,
4042
public readonly ?Annotations $annotations = null,
43+
public readonly bool $isError = false,
4144
) {
4245
$this->text = (\is_array($text) || \is_object($text))
4346
? json_encode($text, \JSON_PRETTY_PRINT) : (string) $text;

src/Schema/Result/CallToolResult.php

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Mcp\Schema\Content\Content;
1717
use Mcp\Schema\Content\EmbeddedResource;
1818
use Mcp\Schema\Content\ImageContent;
19+
use Mcp\Schema\Content\StructuredContent;
1920
use Mcp\Schema\Content\TextContent;
2021
use Mcp\Schema\JsonRpc\Response;
2122
use Mcp\Schema\JsonRpc\ResultInterface;
@@ -49,6 +50,7 @@ class CallToolResult implements ResultInterface
4950
*/
5051
public function __construct(
5152
public readonly array $content,
53+
public readonly ?StructuredContent $structuredContent = null,
5254
public readonly bool $isError = false,
5355
) {
5456
foreach ($this->content as $item) {
@@ -63,59 +65,73 @@ public function __construct(
6365
*
6466
* @param array<TextContent|ImageContent|AudioContent|EmbeddedResource> $content The content of the tool result
6567
*/
66-
public static function success(array $content): self
68+
public static function success(array $content, ?StructuredContent $structuredContent = null): self
6769
{
68-
return new self($content, false);
70+
return new self($content, $structuredContent, false);
6971
}
7072

7173
/**
7274
* Create a new CallToolResult with error status.
7375
*
7476
* @param array<TextContent|ImageContent|AudioContent|EmbeddedResource> $content The content of the tool result
7577
*/
76-
public static function error(array $content): self
78+
public static function error(array $content, ?StructuredContent $structuredContent = null): self
7779
{
78-
return new self($content, true);
80+
return new self($content, $structuredContent, true);
7981
}
8082

8183
/**
82-
* @param array{
83-
* content: array<TextContentData|ImageContentData|AudioContentData|EmbeddedResourceData>,
84-
* isError?: bool,
85-
* } $data
84+
* @param array<int, TextContent|ImageContent|AudioContent|EmbeddedResource|StructuredContent> $data
8685
*/
8786
public static function fromArray(array $data): self
8887
{
89-
if (!isset($data['content']) || !\is_array($data['content'])) {
90-
throw new InvalidArgumentException('Missing or invalid "content" array in CallToolResult data.');
91-
}
92-
9388
$contents = [];
89+
$structuredContent = null;
90+
$isError = false;
9491

95-
foreach ($data['content'] as $item) {
96-
$contents[] = match ($item['type'] ?? null) {
97-
'text' => TextContent::fromArray($item),
98-
'image' => ImageContent::fromArray($item),
99-
'audio' => AudioContent::fromArray($item),
100-
'resource' => EmbeddedResource::fromArray($item),
92+
foreach ($data as $item) {
93+
if (!$item instanceof Content) {
94+
throw new InvalidArgumentException('Provided array must be an array of Content objects.');
95+
}
96+
if ('structured' === $item->type) {
97+
$structuredContent = $item;
98+
continue;
99+
}
100+
$contents[] = match ($item->type) {
101+
// TODO this should be enum, also `resource_link` missing.
102+
// We shouldn't rely on user input for type and just use instanceof instead
103+
'text', 'audio', 'image', 'resource' => $item,
101104
default => throw new InvalidArgumentException(\sprintf('Invalid content type in CallToolResult data: "%s".', $item['type'] ?? null)),
102105
};
106+
107+
if ('text' === $item->type && $item instanceof TextContent) {
108+
$isError = $item->isError;
109+
}
103110
}
104111

105-
return new self($contents, $data['isError'] ?? false);
112+
return new self($contents, $structuredContent, $isError);
106113
}
107114

108115
/**
109116
* @return array{
110-
* content: array<TextContent|ImageContent|AudioContent|EmbeddedResource>,
117+
* content: array<TextContentData|ImageContentData|AudioContentData|EmbeddedResourceData>,
118+
* structuredContent?: mixed[],
111119
* isError: bool,
112120
* }
113121
*/
114122
public function jsonSerialize(): array
115123
{
116-
return [
117-
'content' => $this->content,
118-
'isError' => $this->isError,
119-
];
124+
$result['content'] = [];
125+
foreach ($this->content as $item) {
126+
$result['content'][] = $item->jsonSerialize();
127+
}
128+
129+
$result['isError'] = $this->isError;
130+
131+
if ($this->structuredContent) {
132+
$result['structuredContent'] = $this->structuredContent->jsonSerialize();
133+
}
134+
135+
return $result;
120136
}
121137
}

0 commit comments

Comments
 (0)