Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Capability/Formatter/PromptResultFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
chr-hertel marked this conversation as resolved.
Resolved

return new TextContent($jsonContent);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Capability/Formatter/ResourceResultFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)];
Expand Down
2 changes: 1 addition & 1 deletion src/Capability/Formatter/ToolResultFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)];
Expand Down
2 changes: 1 addition & 1 deletion src/Schema/Content/TextContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
12 changes: 11 additions & 1 deletion tests/Integration/ClientExamples/ClientExampleTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())));
}

/**
Expand All @@ -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);
Expand Down
16 changes: 15 additions & 1 deletion tests/Interop/Server/InspectorSnapshotTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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 [
Expand Down
27 changes: 27 additions & 0 deletions tests/Support/PrettyJson.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?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\Tests\Support;

/**
* Keeps JSON readable in snapshots; formatter tests pin the compact wire format.
*/
final class PrettyJson
{
public static function format(string $text): string
{
$decoded = json_decode($text);

return \is_array($decoded) || \is_object($decoded)
? json_encode($decoded, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR)
: $text;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ public function testFormatRoleContentArray(): void
$this->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([
Expand Down
11 changes: 11 additions & 0 deletions tests/Unit/Capability/Formatter/ResourceResultFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions tests/Unit/Schema/Content/TextContentTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?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\Tests\Unit\Schema\Content;

use Mcp\Schema\Content\TextContent;
use PHPUnit\Framework\TestCase;

final class TextContentTest extends TestCase
{
public function testArrayIsEncodedAsCompactJson(): void
{
$this->assertSame('{"key":{"nested":1}}', (new TextContent(['key' => ['nested' => 1]]))->text);
}

public function testObjectIsEncodedAsCompactJson(): void
{
$this->assertSame('{"key":"value"}', (new TextContent((object) ['key' => 'value']))->text);
}
}
Loading