Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/Capability/Tool/ToolCaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Mcp\Schema\Content\AudioContent;
use Mcp\Schema\Content\EmbeddedResource;
use Mcp\Schema\Content\ImageContent;
use Mcp\Schema\Content\StructuredContent;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Request\CallToolRequest;
use Mcp\Schema\Result\CallToolResult;
Expand Down Expand Up @@ -59,15 +60,24 @@ public function call(CallToolRequest $request): CallToolResult

try {
$result = $this->referenceHandler->handle($toolReference, $arguments);
/** @var TextContent[]|ImageContent[]|EmbeddedResource[]|AudioContent[] $formattedResult */

if ($result instanceof CallToolResult) {
$this->logger->debug('Tool executed successfully', [
'name' => $toolName,
'result_type' => \gettype($result),
]);

return $result;
}
/** @var array<int, TextContent|ImageContent|EmbeddedResource|AudioContent|StructuredContent> $formattedResult */
$formattedResult = $toolReference->formatResult($result);

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

return new CallToolResult($formattedResult);
return CallToolResult::fromArray($formattedResult);
} catch (\Throwable $e) {
$this->logger->error('Tool execution failed', [
'name' => $toolName,
Expand Down
29 changes: 29 additions & 0 deletions src/Schema/Content/StructuredContent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Schema\Content;

class StructuredContent extends Content
{
/**
* @param mixed[] $data
*/
public function __construct(
private array $data = [],
) {
parent::__construct('structured');
}

public function jsonSerialize(): mixed
{
return $this->data;
}
}
3 changes: 3 additions & 0 deletions src/Schema/Content/TextContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* type: 'text',
* text: string,
* annotations?: AnnotationsData,
* isError: bool
* }
*
* @author Kyrian Obikwelu <[email protected]>
Expand All @@ -34,10 +35,12 @@ class TextContent extends Content
*
* @param mixed $text The value to convert to text
* @param ?Annotations $annotations Optional annotations describing the content
* @param bool $isError Optional, mark response as error
*/
public function __construct(
public mixed $text,
public readonly ?Annotations $annotations = null,
public readonly bool $isError = false,
) {
$this->text = (\is_array($text) || \is_object($text))
? json_encode($text, \JSON_PRETTY_PRINT) : (string) $text;
Expand Down
64 changes: 40 additions & 24 deletions src/Schema/Result/CallToolResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Mcp\Schema\Content\Content;
use Mcp\Schema\Content\EmbeddedResource;
use Mcp\Schema\Content\ImageContent;
use Mcp\Schema\Content\StructuredContent;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\JsonRpc\ResultInterface;
Expand Down Expand Up @@ -49,6 +50,7 @@ class CallToolResult implements ResultInterface
*/
public function __construct(
public readonly array $content,
public readonly ?StructuredContent $structuredContent = null,
public readonly bool $isError = false,
) {
foreach ($this->content as $item) {
Expand All @@ -63,59 +65,73 @@ public function __construct(
*
* @param array<TextContent|ImageContent|AudioContent|EmbeddedResource> $content The content of the tool result
*/
public static function success(array $content): self
public static function success(array $content, ?StructuredContent $structuredContent = null): self
{
return new self($content, false);
return new self($content, $structuredContent, false);
}

/**
* Create a new CallToolResult with error status.
*
* @param array<TextContent|ImageContent|AudioContent|EmbeddedResource> $content The content of the tool result
*/
public static function error(array $content): self
public static function error(array $content, ?StructuredContent $structuredContent = null): self
{
return new self($content, true);
return new self($content, $structuredContent, true);
}

/**
* @param array{
* content: array<TextContentData|ImageContentData|AudioContentData|EmbeddedResourceData>,
* isError?: bool,
* } $data
* @param array<int, TextContent|ImageContent|AudioContent|EmbeddedResource|StructuredContent> $data
*/
public static function fromArray(array $data): self
{
if (!isset($data['content']) || !\is_array($data['content'])) {
throw new InvalidArgumentException('Missing or invalid "content" array in CallToolResult data.');
}

$contents = [];
$structuredContent = null;
$isError = false;

foreach ($data['content'] as $item) {
$contents[] = match ($item['type'] ?? null) {
'text' => TextContent::fromArray($item),
'image' => ImageContent::fromArray($item),
'audio' => AudioContent::fromArray($item),
'resource' => EmbeddedResource::fromArray($item),
foreach ($data as $item) {
if (!$item instanceof Content) {
throw new InvalidArgumentException('Provided array must be an array of Content objects.');
}
if ('structured' === $item->type) {
$structuredContent = $item;
continue;
}
$contents[] = match ($item->type) {
// TODO this should be enum, also `resource_link` missing.
// We shouldn't rely on user input for type and just use instanceof instead
'text', 'audio', 'image', 'resource' => $item,
default => throw new InvalidArgumentException(\sprintf('Invalid content type in CallToolResult data: "%s".', $item['type'] ?? null)),
};

if ('text' === $item->type && $item instanceof TextContent) {
$isError = $item->isError;
}
}

return new self($contents, $data['isError'] ?? false);
return new self($contents, $structuredContent, $isError);
}

/**
* @return array{
* content: array<TextContent|ImageContent|AudioContent|EmbeddedResource>,
* content: array<TextContentData|ImageContentData|AudioContentData|EmbeddedResourceData>,
* structuredContent?: mixed[],
* isError: bool,
* }
*/
public function jsonSerialize(): array
{
return [
'content' => $this->content,
'isError' => $this->isError,
];
$result['content'] = [];
foreach ($this->content as $item) {
$result['content'][] = $item->jsonSerialize();
}

$result['isError'] = $this->isError;

if ($this->structuredContent) {
$result['structuredContent'] = $this->structuredContent->jsonSerialize();
}

return $result;
}
}