diff --git a/CHANGELOG.md b/CHANGELOG.md index c3302570e..5b472b7f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to `mcp/sdk` will be documented in this file. * Fix `Session::save()` wiping the other keys of a session that the store failed to read, e.g. when `Psr16SessionStore` hits a cache error: it now returns `false` instead of writing the changed keys alone. * Fix `EnumCompletionProvider` and `ListCompletionProvider` offering all values for the prefix `"0"` instead of only the values that start with it. * Fix tools, prompts and resources sending `content`, `messages` and `contents` as a JSON object instead of an array when the handler returned a non-list array, e.g. the result of `array_filter()`. Named keys in nested results of mixed arrays no longer overwrite each other. +* Tool, prompt and resource results and `TextContent` encode arrays and objects as compact JSON instead of pretty-printed JSON. * Add `Psr6SessionStore` and `Psr6NotificationBus`, which work like `Psr16SessionStore` and `Psr16NotificationBus` over a PSR-6 cache pool. `Builder::setDiscovery()` also accepts a PSR-6 pool as `$cache`. ### Client diff --git a/src/Capability/Formatter/PromptResultFormatter.php b/src/Capability/Formatter/PromptResultFormatter.php index 30dc2b1c7..76bd5328c 100644 --- a/src/Capability/Formatter/PromptResultFormatter.php +++ b/src/Capability/Formatter/PromptResultFormatter.php @@ -173,7 +173,7 @@ private function formatContent(mixed $content, ?int $index = null): TextContent| return new TextContent($stringContent); } - $jsonContent = json_encode($content, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR); + $jsonContent = json_encode($content, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR); return new TextContent($jsonContent); } diff --git a/src/Capability/Formatter/ResourceResultFormatter.php b/src/Capability/Formatter/ResourceResultFormatter.php index 80d45d267..9167356f2 100644 --- a/src/Capability/Formatter/ResourceResultFormatter.php +++ b/src/Capability/Formatter/ResourceResultFormatter.php @@ -155,7 +155,7 @@ public function format(mixed $readResult, string $uri, ?string $mimeType = null, if ($mimeType && (str_contains(strtolower($mimeType), 'json') || 'application/json' === $mimeType)) { try { - $jsonString = json_encode($readResult, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT); + $jsonString = json_encode($readResult, \JSON_THROW_ON_ERROR); return [new TextResourceContents($uri, $mimeType, $jsonString, $meta)]; } catch (\JsonException $e) { @@ -164,7 +164,7 @@ public function format(mixed $readResult, string $uri, ?string $mimeType = null, } try { - $jsonString = json_encode($readResult, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT); + $jsonString = json_encode($readResult, \JSON_THROW_ON_ERROR); $mimeType = $mimeType ?? 'application/json'; return [new TextResourceContents($uri, $mimeType, $jsonString, $meta)]; diff --git a/src/Capability/Formatter/ToolResultFormatter.php b/src/Capability/Formatter/ToolResultFormatter.php index c2c0a5b06..e8aa92773 100644 --- a/src/Capability/Formatter/ToolResultFormatter.php +++ b/src/Capability/Formatter/ToolResultFormatter.php @@ -97,7 +97,7 @@ public function format(mixed $toolExecutionResult): array $jsonResult = json_encode( $toolExecutionResult, - \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR | \JSON_INVALID_UTF8_SUBSTITUTE + \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR | \JSON_INVALID_UTF8_SUBSTITUTE ); return [new TextContent($jsonResult)]; diff --git a/src/Schema/Content/TextContent.php b/src/Schema/Content/TextContent.php index 54b1cde2a..73a83b415 100644 --- a/src/Schema/Content/TextContent.php +++ b/src/Schema/Content/TextContent.php @@ -40,7 +40,7 @@ public function __construct( public readonly ?Annotations $annotations = null, ) { $this->text = (\is_array($text) || \is_object($text)) - ? json_encode($text, \JSON_PRETTY_PRINT) : (string) $text; + ? json_encode($text) : (string) $text; parent::__construct('text'); } diff --git a/tests/Integration/ClientExamples/ClientExampleTestCase.php b/tests/Integration/ClientExamples/ClientExampleTestCase.php index 28432b35d..3a26e2d06 100644 --- a/tests/Integration/ClientExamples/ClientExampleTestCase.php +++ b/tests/Integration/ClientExamples/ClientExampleTestCase.php @@ -11,6 +11,7 @@ namespace Mcp\Tests\Integration\ClientExamples; +use Mcp\Tests\Support\PrettyJson; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Process; @@ -66,7 +67,7 @@ public function testOutputMatchesSnapshot(string $input): void $process->getErrorOutput(), )); - $this->assertMatchesSnapshot($this->normalizeOutput($process->getOutput())); + $this->assertMatchesSnapshot($this->normalizeOutput($this->prettyPrintJson($process->getOutput()))); } /** @@ -82,6 +83,15 @@ protected function normalizeOutput(string $output): string return $output; } + private function prettyPrintJson(string $output): string + { + return preg_replace_callback( + '/^(\s*(?:Content: )?)(\{.*\})$/m', + static fn (array $match): string => $match[1].PrettyJson::format($match[2]), + $output, + ) ?? $output; + } + private function assertMatchesSnapshot(string $output): void { $className = substr(static::class, strrpos(static::class, '\\') + 1); diff --git a/tests/Interop/Server/InspectorSnapshotTestCase.php b/tests/Interop/Server/InspectorSnapshotTestCase.php index a4261fc3b..9595dadbc 100644 --- a/tests/Interop/Server/InspectorSnapshotTestCase.php +++ b/tests/Interop/Server/InspectorSnapshotTestCase.php @@ -13,6 +13,7 @@ use Mcp\Schema\Enum\LoggingLevel; use Mcp\Tests\Interop\NodeTool; +use Mcp\Tests\Support\PrettyJson; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Process; @@ -97,7 +98,7 @@ public function testOutputMatchesSnapshot( $snapshotFile = $this->getSnapshotFilePath($method, $testName); - $normalizedOutput = $this->normalizeTestOutput($output, $testName); + $normalizedOutput = $this->normalizeTestOutput($this->prettyPrintTextFields($output), $testName); if (!file_exists($snapshotFile)) { file_put_contents($snapshotFile, $normalizedOutput.\PHP_EOL); @@ -116,6 +117,19 @@ protected function normalizeTestOutput(string $output, ?string $testName = null) return $output; } + private function prettyPrintTextFields(string $output): string + { + $result = json_decode($output); + + foreach ([...$result->content ?? [], ...$result->contents ?? []] as $item) { + if (isset($item->text)) { + $item->text = PrettyJson::format($item->text); + } + } + + return json_encode($result, \JSON_THROW_ON_ERROR); + } + public static function provideMethods(): array { return [ diff --git a/tests/Support/PrettyJson.php b/tests/Support/PrettyJson.php new file mode 100644 index 000000000..2b9bf2a81 --- /dev/null +++ b/tests/Support/PrettyJson.php @@ -0,0 +1,27 @@ +assertSame(Role::User, $result[0]->role); } + public function testFormatArrayContentAsCompactJson(): void + { + $result = (new PromptResultFormatter())->format(['role' => 'user', 'content' => ['key' => ['nested' => 1]]]); + $this->assertInstanceOf(TextContent::class, $result[0]->content); + $this->assertSame('{"key":{"nested":1}}', $result[0]->content->text); + } + public function testFormatTypedTextContentPreservesAnnotations(): void { $result = (new PromptResultFormatter())->format([ diff --git a/tests/Unit/Capability/Formatter/ResourceResultFormatterTest.php b/tests/Unit/Capability/Formatter/ResourceResultFormatterTest.php index 172eff439..7a8847d8e 100644 --- a/tests/Unit/Capability/Formatter/ResourceResultFormatterTest.php +++ b/tests/Unit/Capability/Formatter/ResourceResultFormatterTest.php @@ -46,6 +46,17 @@ public function testFormatResourceContents(): void $this->assertSame([$contents], $result); } + public function testFormatArrayResultAsCompactJson(): void + { + $result = (new ResourceResultFormatter())->format(['key' => ['nested' => 1]], 'test://uri', 'application/json'); + $this->assertInstanceOf(TextResourceContents::class, $result[0]); + $this->assertSame('{"key":{"nested":1}}', $result[0]->text); + + $result = (new ResourceResultFormatter())->format(['key' => ['nested' => 1]], 'test://uri'); + $this->assertInstanceOf(TextResourceContents::class, $result[0]); + $this->assertSame('{"key":{"nested":1}}', $result[0]->text); + } + public function testFormatWithMimeType(): void { $result = (new ResourceResultFormatter())->format('content', 'file://test', 'text/html'); diff --git a/tests/Unit/Capability/Formatter/ToolResultFormatterTest.php b/tests/Unit/Capability/Formatter/ToolResultFormatterTest.php index 8b1c445b2..5efac21cc 100644 --- a/tests/Unit/Capability/Formatter/ToolResultFormatterTest.php +++ b/tests/Unit/Capability/Formatter/ToolResultFormatterTest.php @@ -52,7 +52,7 @@ public function testFormatArrayResult(): void $result = (new ToolResultFormatter())->format(['key' => 'value']); $this->assertCount(1, $result); $this->assertInstanceOf(TextContent::class, $result[0]); - $this->assertStringContainsString('value', $result[0]->text); + $this->assertSame('{"key":"value"}', $result[0]->text); } public function testFormatNullResult(): void diff --git a/tests/Unit/Schema/Content/TextContentTest.php b/tests/Unit/Schema/Content/TextContentTest.php new file mode 100644 index 000000000..8dd74df35 --- /dev/null +++ b/tests/Unit/Schema/Content/TextContentTest.php @@ -0,0 +1,28 @@ +assertSame('{"key":{"nested":1}}', (new TextContent(['key' => ['nested' => 1]]))->text); + } + + public function testObjectIsEncodedAsCompactJson(): void + { + $this->assertSame('{"key":"value"}', (new TextContent((object) ['key' => 'value']))->text); + } +}