diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4dca0b25..7a4071ba 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -78,30 +78,12 @@ parameters: count: 1 path: src/Server/ServerBuilder.php - - - message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$discoveryExcludeDirs type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/Server/ServerBuilder.php - - message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$instructions is never read, only written\.$#' identifier: property.onlyWritten count: 1 path: src/Server/ServerBuilder.php - - - message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$paginationLimit \(int\|null\) is never assigned null so it can be removed from the property type\.$#' - identifier: property.unusedType - count: 1 - path: src/Server/ServerBuilder.php - - - - message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$paginationLimit is never read, only written\.$#' - identifier: property.onlyWritten - count: 1 - path: src/Server/ServerBuilder.php - - message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$prompts type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue diff --git a/src/JsonRpc/Handler.php b/src/JsonRpc/Handler.php index a16f7347..b73f7769 100644 --- a/src/JsonRpc/Handler.php +++ b/src/JsonRpc/Handler.php @@ -73,6 +73,7 @@ public static function make( SessionStoreInterface $sessionStore, SessionFactoryInterface $sessionFactory, LoggerInterface $logger = new NullLogger(), + int $paginationLimit = 50, ): self { return new self( messageFactory: MessageFactory::make(), @@ -82,12 +83,12 @@ public static function make( new NotificationHandler\InitializedHandler(), new RequestHandler\InitializeHandler($registry->getCapabilities(), $implementation), new RequestHandler\PingHandler(), - new RequestHandler\ListPromptsHandler($referenceProvider), + new RequestHandler\ListPromptsHandler($referenceProvider, $paginationLimit), new RequestHandler\GetPromptHandler($promptGetter), - new RequestHandler\ListResourcesHandler($referenceProvider), + new RequestHandler\ListResourcesHandler($referenceProvider, $paginationLimit), new RequestHandler\ReadResourceHandler($resourceReader), new RequestHandler\CallToolHandler($toolCaller, $logger), - new RequestHandler\ListToolsHandler($referenceProvider), + new RequestHandler\ListToolsHandler($referenceProvider, $paginationLimit), ], logger: $logger, ); diff --git a/src/Server/RequestHandler/Reference/Page.php b/src/Server/RequestHandler/Reference/Page.php index b76aa0ba..b5b31863 100644 --- a/src/Server/RequestHandler/Reference/Page.php +++ b/src/Server/RequestHandler/Reference/Page.php @@ -12,9 +12,9 @@ namespace Mcp\Server\RequestHandler\Reference; /** - * @implements \ArrayAccess + * @extends \ArrayObject */ -final class Page implements \Countable, \ArrayAccess +final class Page extends \ArrayObject { /** * @param array $references Items can be Tool, Prompt, ResourceTemplate, or Resource @@ -23,30 +23,11 @@ public function __construct( public readonly array $references, public readonly ?string $nextCursor, ) { + parent::__construct($references, \ArrayObject::ARRAY_AS_PROPS); } public function count(): int { return \count($this->references); } - - public function offsetExists(mixed $offset): bool - { - return isset($this->references[$offset]); - } - - public function offsetGet(mixed $offset): mixed - { - return $this->references[$offset] ?? null; - } - - public function offsetSet(mixed $offset, mixed $value): void - { - return; - } - - public function offsetUnset(mixed $offset): void - { - return; - } } diff --git a/src/Server/ServerBuilder.php b/src/Server/ServerBuilder.php index 40fd5647..f22cd9bc 100644 --- a/src/Server/ServerBuilder.php +++ b/src/Server/ServerBuilder.php @@ -76,7 +76,7 @@ final class ServerBuilder private int $sessionTtl = 3600; - private ?int $paginationLimit = 50; + private int $paginationLimit = 50; private ?string $instructions = null; @@ -119,6 +119,9 @@ final class ServerBuilder * @var array|string[] */ private array $discoveryScanDirs = []; + /** + * @var array|string[] + */ private array $discoveryExcludeDirs = []; /** @@ -337,6 +340,7 @@ public function build(): Server sessionStore: $sessionStore, sessionFactory: $sessionFactory, logger: $logger, + paginationLimit: $this->paginationLimit, ), logger: $logger, ); diff --git a/tests/Unit/Server/RequestHandler/ListToolsHandlerTest.php b/tests/Unit/Server/RequestHandler/ListToolsHandlerTest.php index a7e4902c..d85c2bf7 100644 --- a/tests/Unit/Server/RequestHandler/ListToolsHandlerTest.php +++ b/tests/Unit/Server/RequestHandler/ListToolsHandlerTest.php @@ -137,7 +137,7 @@ public function testReturnsLastPageWithNullCursor(): void public function testReturnsAllToolsWhenCountIsLessThanPageSize(): void { // Arrange - $this->addToolsToRegistry(2); // Less than page size (3) + $this->addToolsToRegistry(2); // Less than page size 3 $request = $this->createListToolsRequest(); // Act @@ -239,6 +239,51 @@ public function testMaintainsStableCursorsAcrossCalls(): void $this->assertEquals($result1->tools, $result2->tools); } + #[TestDox('Uses custom page size when provided')] + public function testUsesCustomPageSizeWhenProvided(): void + { + // Arrange + $customPageSize = 5; + $customHandler = new ListToolsHandler($this->registry, pageSize: $customPageSize); + $this->addToolsToRegistry(10); + $request = $this->createListToolsRequest(); + + // Act + $response = $customHandler->handle($request, $this->session); + + // Assert + /** @var ListToolsResult $result */ + $result = $response->result; + $this->assertInstanceOf(ListToolsResult::class, $result); + $this->assertCount($customPageSize, $result->tools); + $this->assertNotNull($result->nextCursor); + } + + #[TestDox('Different page sizes produce different pagination results')] + public function testDifferentPageSizesProduceDifferentPaginationResults(): void + { + // Arrange + $this->addToolsToRegistry(10); + $smallPageHandler = new ListToolsHandler($this->registry, pageSize: 2); + $largePageHandler = new ListToolsHandler($this->registry, pageSize: 7); + $request = $this->createListToolsRequest(); + + // Act + $smallPageResponse = $smallPageHandler->handle($request, $this->session); + $largePageResponse = $largePageHandler->handle($request, $this->session); + + // Assert + /** @var ListToolsResult $smallResult */ + $smallResult = $smallPageResponse->result; + /** @var ListToolsResult $largeResult */ + $largeResult = $largePageResponse->result; + + $this->assertCount(2, $smallResult->tools); + $this->assertCount(7, $largeResult->tools); + $this->assertNotNull($smallResult->nextCursor); + $this->assertNotNull($largeResult->nextCursor); + } + private function addToolsToRegistry(int $count): void { for ($i = 0; $i < $count; ++$i) {