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 @@ -15,6 +15,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
* Fix `Psr16NotificationBus` subscribers missing the notifications published after the bus stayed quiet for longer than its TTL: the sequence counter no longer expires, and a subscriber whose cursor is ahead of a lost counter reads what the bus holds.
* 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.
* 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
5 changes: 0 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,7 +1,2 @@
parameters:
ignoreErrors:
-
message: '#^Method Mcp\\Schema\\Result\\ReadResourceResult\:\:jsonSerialize\(\) should return array\{contents\: array\<Mcp\\Schema\\Content\\BlobResourceContents\|Mcp\\Schema\\Content\\TextResourceContents\>, ttlMs\?\: int, cacheScope\?\: string\} but returns array\{contents\: array\<Mcp\\Schema\\Content\\ResourceContents\>, ttlMs\?\: int\<0, max\>, cacheScope\?\: ''private''\|''public''\}\.$#'
identifier: return.type
count: 1
path: src/Schema/Result/ReadResourceResult.php
8 changes: 2 additions & 6 deletions src/Capability/Formatter/PromptResultFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,8 @@ public function format(mixed $promptGenerationResult): array
if ($item instanceof PromptMessage) {
$result[] = $item;
} else {
foreach ($this->format($item) as $key => $message) {
if (\is_int($key)) {
$result[] = $message;
} else {
$result[$key] = $message;
}
foreach ($this->format($item) as $message) {
$result[] = $message;
}
}
}
Expand Down
8 changes: 2 additions & 6 deletions src/Capability/Formatter/ResourceResultFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,8 @@ public function format(mixed $readResult, string $uri, ?string $mimeType = null,
} elseif ($item instanceof EmbeddedResource) {
$result[] = $item->resource;
} else {
foreach ($this->format($item, $uri, $mimeType, $meta) as $key => $content) {
if (\is_int($key)) {
$result[] = $content;
} else {
$result[$key] = $content;
}
foreach ($this->format($item, $uri, $mimeType, $meta) as $content) {
$result[] = $content;
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/Capability/Formatter/ToolResultFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ public function format(mixed $toolExecutionResult): array
if ($item instanceof Content) {
$result[] = $item;
} else {
$result = array_merge($result, $this->format($item));
foreach ($this->format($item) as $content) {
$result[] = $content;
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/Schema/Result/CallToolResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public static function fromArray(array $data): self

/**
* @return array{
* content: array<mixed>,
* content: list<mixed>,
* isError: bool,
* structuredContent?: mixed,
* _meta?: array<string, mixed>,
Expand All @@ -139,7 +139,7 @@ public static function fromArray(array $data): self
public function jsonSerialize(): array
{
$result = [
'content' => $this->content,
'content' => array_values($this->content),
'isError' => $this->isError,
];

Expand Down
4 changes: 2 additions & 2 deletions src/Schema/Result/GetPromptResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ public static function fromArray(array $data): self

/**
* @return array{
* messages: array<PromptMessage>,
* messages: list<PromptMessage>,
* description?: string,
* }
*/
public function jsonSerialize(): array
{
$result = [
'messages' => $this->messages,
'messages' => array_values($this->messages),
];

if (null !== $this->description) {
Expand Down
4 changes: 2 additions & 2 deletions src/Schema/Result/ReadResourceResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,14 @@ public static function fromArray(array $data): self

/**
* @return array{
* contents: array<BlobResourceContents|TextResourceContents>,
* contents: list<ResourceContents>,
* ttlMs?: int,
* cacheScope?: string,
* }
*/
public function jsonSerialize(): array
{
$data = ['contents' => $this->contents];
$data = ['contents' => array_values($this->contents)];

// Only what this result actually decided; the wire codec fills the rest
// from policy, and an absent member is the signal for it to do so.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

class PromptResultFormatterTest extends TestCase
{
public function testMixedMessagesPreserveNestedKeysAndOrder(): void
public function testMixedMessagesAreFlattenedInOrder(): void
{
$first = new PromptMessage(Role::User, new TextContent('first'));
$second = new PromptMessage(Role::Assistant, new TextContent('second'));
Expand All @@ -36,11 +36,7 @@ public function testMixedMessagesPreserveNestedKeysAndOrder(): void
['named' => $third, 5 => $second],
]);

$this->assertSame([0, 'named', 1, 2], array_keys($result));
$this->assertSame($first, $result[0]);
$this->assertSame($third, $result['named']);
$this->assertSame($first, $result[1]);
$this->assertSame($second, $result[2]);
$this->assertSame([$first, $second, $first, $third, $second], $result);
}

public function testFormatPromptMessage(): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

class ResourceResultFormatterTest extends TestCase
{
public function testMixedResourcesPreserveNestedKeysAndOrder(): void
public function testMixedResourcesAreFlattenedInOrder(): void
{
$first = new TextResourceContents('file://test', 'text/plain', 'first');
$second = new TextResourceContents('file://test', 'text/plain', 'second');
Expand All @@ -29,11 +29,7 @@ public function testMixedResourcesPreserveNestedKeysAndOrder(): void
['named' => $third, 5 => $second],
], 'file://test');

$this->assertSame([0, 'named', 1, 2], array_keys($result));
$this->assertSame($first, $result[0]);
$this->assertSame($third, $result['named']);
$this->assertSame($first, $result[1]);
$this->assertSame($second, $result[2]);
$this->assertSame([$first, $second, $first, $third, $second], $result);
}

public function testFormatStringResult(): void
Expand Down
15 changes: 15 additions & 0 deletions tests/Unit/Capability/Formatter/ToolResultFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@

class ToolResultFormatterTest extends TestCase
{
public function testMixedContentIsFlattenedInOrder(): void
{
$first = new TextContent('first');
$second = new TextContent('second');
$third = new TextContent('third');

$result = (new ToolResultFormatter())->format([
$first,
['named' => $second, 8 => $first],
['named' => $third, 5 => $second],
]);

$this->assertSame([$first, $second, $first, $third, $second], $result);
}

public function testFormatStringResult(): void
{
$result = (new ToolResultFormatter())->format('hello');
Expand Down
16 changes: 16 additions & 0 deletions tests/Unit/Schema/Result/CallToolResultTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,26 @@
use Mcp\Schema\Content\ResourceLink;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Result\CallToolResult;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;

final class CallToolResultTest extends TestCase
{
/**
* @param array{int|string, int|string} $keys
*/
#[TestWith([['first', 'second']])]
#[TestWith([[1, 3]])]
public function testContentSerializesAsListForNonListKeys(array $keys): void
{
$first = new TextContent('first');
$second = new TextContent('second');

$result = new CallToolResult([$keys[0] => $first, $keys[1] => $second]);

$this->assertSame([$first, $second], $result->jsonSerialize()['content']);
}

public function testFromArrayDeserializesResourceLinkContent(): void
{
$result = CallToolResult::fromArray([
Expand Down
37 changes: 37 additions & 0 deletions tests/Unit/Schema/Result/GetPromptResultTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?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\Result;

use Mcp\Schema\Content\PromptMessage;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Enum\Role;
use Mcp\Schema\Result\GetPromptResult;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;

final class GetPromptResultTest extends TestCase
{
/**
* @param array{int|string, int|string} $keys
*/
#[TestWith([['first', 'second']])]
#[TestWith([[1, 3]])]
public function testMessagesSerializeAsListForNonListKeys(array $keys): void
{
$first = new PromptMessage(Role::User, new TextContent('first'));
$second = new PromptMessage(Role::Assistant, new TextContent('second'));

$result = new GetPromptResult([$keys[0] => $first, $keys[1] => $second]);

$this->assertSame([$first, $second], $result->jsonSerialize()['messages']);
}
}
35 changes: 35 additions & 0 deletions tests/Unit/Schema/Result/ReadResourceResultTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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\Result;

use Mcp\Schema\Content\TextResourceContents;
use Mcp\Schema\Result\ReadResourceResult;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;

final class ReadResourceResultTest extends TestCase
{
/**
* @param array{int|string, int|string} $keys
*/
#[TestWith([['first', 'second']])]
#[TestWith([[1, 3]])]
public function testContentsSerializeAsListForNonListKeys(array $keys): void
{
$first = new TextResourceContents('file://test', 'text/plain', 'first');
$second = new TextResourceContents('file://test', 'text/plain', 'second');

$result = new ReadResourceResult([$keys[0] => $first, $keys[1] => $second]);

$this->assertSame([$first, $second], $result->jsonSerialize()['contents']);
}
}
Loading