From 681df925d45e14abbd493d1c881e1db6af05bdf2 Mon Sep 17 00:00:00 2001 From: Sangrak Choi Date: Sun, 12 Oct 2025 16:54:10 +0900 Subject: [PATCH] Remove duplicated content when emitting structured payloads --- src/Server/Request/ToolsCallHandler.php | 10 +--- src/Services/ToolService/ToolResponse.php | 65 ++++++++++++----------- tests/Http/StreamableHttpTest.php | 4 +- 3 files changed, 36 insertions(+), 43 deletions(-) diff --git a/src/Server/Request/ToolsCallHandler.php b/src/Server/Request/ToolsCallHandler.php index 084a3e6..2ffa605 100644 --- a/src/Server/Request/ToolsCallHandler.php +++ b/src/Server/Request/ToolsCallHandler.php @@ -82,7 +82,7 @@ public function execute(string $method, ?array $params = null): array } try { - $json = json_encode($preparedResult, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); + json_encode($preparedResult, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); } catch (JsonException $exception) { throw new JsonRpcErrorException( message: 'Failed to encode tool result as JSON: '.$exception->getMessage(), @@ -91,14 +91,6 @@ public function execute(string $method, ?array $params = null): array } return [ - 'content' => [ - [ - 'type' => 'text', - 'text' => $json, - ], - ], - // Provide structuredContent alongside text per MCP 2025-06-18 guidance. - // @see https://modelcontextprotocol.io/specification/2025-06-18#structured-content 'structuredContent' => $preparedResult, ]; } diff --git a/src/Services/ToolService/ToolResponse.php b/src/Services/ToolService/ToolResponse.php index 51a02b1..34f9678 100644 --- a/src/Services/ToolService/ToolResponse.php +++ b/src/Services/ToolService/ToolResponse.php @@ -3,24 +3,37 @@ namespace OPGG\LaravelMcpServer\Services\ToolService; use InvalidArgumentException; -use JsonException; /** * Value object describing a structured tool response. */ final class ToolResponse { + /** + * @var array + */ + private array $content; + + /** + * @var array + */ + private array $metadata; + /** * @param array $content * @param array $metadata */ - private function __construct(private array $content, private array $metadata = []) + private bool $includeContent; + + private function __construct(array $content, array $metadata = [], bool $includeContent = true) { if (array_key_exists('content', $metadata)) { throw new InvalidArgumentException('Metadata must not contain a content key.'); } $this->content = array_values($content); + $this->metadata = $metadata; + $this->includeContent = $includeContent && $this->content !== []; foreach ($this->content as $index => $item) { if (! is_array($item) || ! isset($item['type'], $item['text'])) { @@ -60,41 +73,26 @@ public static function text(string $text, string $type = 'text', array $metadata } /** - * Create a ToolResponse that includes structured content alongside serialised text. + * Create a ToolResponse that includes structured content alongside optional serialised text. * * @param array|null $content * @param array $metadata - * - * @throws JsonException */ public static function structured(array $structuredContent, ?array $content = null, array $metadata = []): self { - $json = json_encode($structuredContent, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); - $contentItems = $content !== null ? array_values($content) : []; - $hasSerialisedText = false; - foreach ($contentItems as $item) { - if (isset($item['type'], $item['text']) && $item['type'] === 'text' && $item['text'] === $json) { - $hasSerialisedText = true; - break; - } - } - - if (! $hasSerialisedText) { - $contentItems[] = [ - 'type' => 'text', - 'text' => $json, - ]; - } - - return new self($contentItems, [ - ...$metadata, - // The MCP 2025-06-18 spec encourages servers to mirror structured payloads in the - // `structuredContent` field for reliable client parsing. - // @see https://modelcontextprotocol.io/specification/2025-06-18#structured-content - 'structuredContent' => $structuredContent, - ]); + return new self( + $contentItems, + [ + ...$metadata, + // The MCP 2025-06-18 spec encourages servers to mirror structured payloads in the + // `structuredContent` field for reliable client parsing. + // @see https://modelcontextprotocol.io/specification/2025-06-18#structured-content + 'structuredContent' => $structuredContent, + ], + $contentItems !== [] + ); } /** @@ -124,9 +122,14 @@ public function metadata(): array */ public function toArray(): array { - return [ + $payload = [ ...$this->metadata, - 'content' => $this->content, ]; + + if ($this->includeContent) { + $payload['content'] = $this->content; + } + + return $payload; } } diff --git a/tests/Http/StreamableHttpTest.php b/tests/Http/StreamableHttpTest.php index ebe4249..9982c7e 100644 --- a/tests/Http/StreamableHttpTest.php +++ b/tests/Http/StreamableHttpTest.php @@ -38,9 +38,7 @@ expect($data['result']['content'][0]['text']) ->toContain('HelloWorld `Tester` developer'); - expect($data['result']['content'][1]['type'])->toBe('text'); - $decoded = json_decode($data['result']['content'][1]['text'], true); - expect($decoded['name'])->toBe('Tester'); + expect($data['result']['content'])->toHaveCount(1); expect($data['result']['structuredContent']['message']) ->toContain('HelloWorld `Tester` developer'); });