Skip to content

Commit 681df92

Browse files
committed
Remove duplicated content when emitting structured payloads
1 parent f283f60 commit 681df92

File tree

3 files changed

+36
-43
lines changed

3 files changed

+36
-43
lines changed

src/Server/Request/ToolsCallHandler.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public function execute(string $method, ?array $params = null): array
8282
}
8383

8484
try {
85-
$json = json_encode($preparedResult, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
85+
json_encode($preparedResult, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
8686
} catch (JsonException $exception) {
8787
throw new JsonRpcErrorException(
8888
message: 'Failed to encode tool result as JSON: '.$exception->getMessage(),
@@ -91,14 +91,6 @@ public function execute(string $method, ?array $params = null): array
9191
}
9292

9393
return [
94-
'content' => [
95-
[
96-
'type' => 'text',
97-
'text' => $json,
98-
],
99-
],
100-
// Provide structuredContent alongside text per MCP 2025-06-18 guidance.
101-
// @see https://modelcontextprotocol.io/specification/2025-06-18#structured-content
10294
'structuredContent' => $preparedResult,
10395
];
10496
}

src/Services/ToolService/ToolResponse.php

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,37 @@
33
namespace OPGG\LaravelMcpServer\Services\ToolService;
44

55
use InvalidArgumentException;
6-
use JsonException;
76

87
/**
98
* Value object describing a structured tool response.
109
*/
1110
final class ToolResponse
1211
{
12+
/**
13+
* @var array<int, array{type: string, text: string, source?: string}>
14+
*/
15+
private array $content;
16+
17+
/**
18+
* @var array<string, mixed>
19+
*/
20+
private array $metadata;
21+
1322
/**
1423
* @param array<int, array{type: string, text: string, source?: string}> $content
1524
* @param array<string, mixed> $metadata
1625
*/
17-
private function __construct(private array $content, private array $metadata = [])
26+
private bool $includeContent;
27+
28+
private function __construct(array $content, array $metadata = [], bool $includeContent = true)
1829
{
1930
if (array_key_exists('content', $metadata)) {
2031
throw new InvalidArgumentException('Metadata must not contain a content key.');
2132
}
2233

2334
$this->content = array_values($content);
35+
$this->metadata = $metadata;
36+
$this->includeContent = $includeContent && $this->content !== [];
2437

2538
foreach ($this->content as $index => $item) {
2639
if (! is_array($item) || ! isset($item['type'], $item['text'])) {
@@ -60,41 +73,26 @@ public static function text(string $text, string $type = 'text', array $metadata
6073
}
6174

6275
/**
63-
* Create a ToolResponse that includes structured content alongside serialised text.
76+
* Create a ToolResponse that includes structured content alongside optional serialised text.
6477
*
6578
* @param array<int, array{type: string, text: string, source?: string}>|null $content
6679
* @param array<string, mixed> $metadata
67-
*
68-
* @throws JsonException
6980
*/
7081
public static function structured(array $structuredContent, ?array $content = null, array $metadata = []): self
7182
{
72-
$json = json_encode($structuredContent, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
73-
7483
$contentItems = $content !== null ? array_values($content) : [];
7584

76-
$hasSerialisedText = false;
77-
foreach ($contentItems as $item) {
78-
if (isset($item['type'], $item['text']) && $item['type'] === 'text' && $item['text'] === $json) {
79-
$hasSerialisedText = true;
80-
break;
81-
}
82-
}
83-
84-
if (! $hasSerialisedText) {
85-
$contentItems[] = [
86-
'type' => 'text',
87-
'text' => $json,
88-
];
89-
}
90-
91-
return new self($contentItems, [
92-
...$metadata,
93-
// The MCP 2025-06-18 spec encourages servers to mirror structured payloads in the
94-
// `structuredContent` field for reliable client parsing.
95-
// @see https://modelcontextprotocol.io/specification/2025-06-18#structured-content
96-
'structuredContent' => $structuredContent,
97-
]);
85+
return new self(
86+
$contentItems,
87+
[
88+
...$metadata,
89+
// The MCP 2025-06-18 spec encourages servers to mirror structured payloads in the
90+
// `structuredContent` field for reliable client parsing.
91+
// @see https://modelcontextprotocol.io/specification/2025-06-18#structured-content
92+
'structuredContent' => $structuredContent,
93+
],
94+
$contentItems !== []
95+
);
9896
}
9997

10098
/**
@@ -124,9 +122,14 @@ public function metadata(): array
124122
*/
125123
public function toArray(): array
126124
{
127-
return [
125+
$payload = [
128126
...$this->metadata,
129-
'content' => $this->content,
130127
];
128+
129+
if ($this->includeContent) {
130+
$payload['content'] = $this->content;
131+
}
132+
133+
return $payload;
131134
}
132135
}

tests/Http/StreamableHttpTest.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@
3838
expect($data['result']['content'][0]['text'])
3939
->toContain('HelloWorld `Tester` developer');
4040

41-
expect($data['result']['content'][1]['type'])->toBe('text');
42-
$decoded = json_decode($data['result']['content'][1]['text'], true);
43-
expect($decoded['name'])->toBe('Tester');
41+
expect($data['result']['content'])->toHaveCount(1);
4442
expect($data['result']['structuredContent']['message'])
4543
->toContain('HelloWorld `Tester` developer');
4644
});

0 commit comments

Comments
 (0)