Skip to content

Commit 10e7935

Browse files
author
larry.sulebalogun
committed
10 - Added the list tools handler logic
1 parent 52a2de8 commit 10e7935

File tree

4 files changed

+267
-8
lines changed

4 files changed

+267
-8
lines changed

src/Server/RequestHandler/ListPromptsHandler.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ final class ListPromptsHandler implements MethodHandlerInterface
2727
public function __construct(
2828
private readonly Registry $registry,
2929
private readonly int $pageSize = 20,
30-
) {
31-
}
30+
) {}
3231

3332
public function supports(HasMethodInterface $message): bool
3433
{

src/Server/RequestHandler/ListResourcesHandler.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ final class ListResourcesHandler implements MethodHandlerInterface
2828
public function __construct(
2929
private readonly Registry $registry,
3030
private readonly int $pageSize = 20,
31-
) {
32-
}
31+
) {}
3332

3433
public function supports(HasMethodInterface $message): bool
3534
{

src/Server/RequestHandler/ListToolsHandler.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Mcp\Server\RequestHandler;
1313

1414
use Mcp\Capability\Registry;
15+
use Mcp\Exception\InvalidCursorException;
1516
use Mcp\Schema\JsonRpc\HasMethodInterface;
1617
use Mcp\Schema\JsonRpc\Response;
1718
use Mcp\Schema\Request\ListToolsRequest;
@@ -27,21 +28,27 @@ final class ListToolsHandler implements MethodHandlerInterface
2728
public function __construct(
2829
private readonly Registry $registry,
2930
private readonly int $pageSize = 20,
30-
) {
31-
}
31+
) {}
3232

3333
public function supports(HasMethodInterface $message): bool
3434
{
3535
return $message instanceof ListToolsRequest;
3636
}
3737

38+
/**
39+
* @throws InvalidCursorException When the cursor is invalid
40+
*/
3841
public function handle(ListToolsRequest|HasMethodInterface $message): Response
3942
{
4043
\assert($message instanceof ListToolsRequest);
4144

42-
$cursor = null;
4345
$tools = $this->registry->getTools($this->pageSize, $message->cursor);
44-
$nextCursor = (null !== $cursor && \count($tools) === $this->pageSize) ? $cursor : null;
46+
47+
$nextCursor = $this->registry->calculateNextCursor(
48+
$this->registry->getToolsCount(),
49+
$message->cursor,
50+
\count($tools)
51+
);
4552

4653
return new Response(
4754
$message->getId(),
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Tests\Server\RequestHandler;
13+
14+
use Mcp\Capability\Registry;
15+
use Mcp\Exception\InvalidCursorException;
16+
use Mcp\Schema\Tool;
17+
use Mcp\Schema\Request\ListToolsRequest;
18+
use Mcp\Schema\Result\ListToolsResult;
19+
use Mcp\Server\RequestHandler\ListToolsHandler;
20+
use PHPUnit\Framework\Attributes\TestDox;
21+
use PHPUnit\Framework\TestCase;
22+
23+
class ListToolsHandlerTest extends TestCase
24+
{
25+
private Registry $registry;
26+
private ListToolsHandler $handler;
27+
28+
protected function setUp(): void
29+
{
30+
$this->registry = new Registry();
31+
$this->handler = new ListToolsHandler($this->registry, pageSize: 3); // Use small page size for testing
32+
}
33+
34+
#[TestDox('Returns first page when no cursor provided')]
35+
public function testReturnsFirstPageWhenNoCursorProvided(): void
36+
{
37+
// Arrange
38+
$this->addToolsToRegistry(5);
39+
$request = $this->createListToolsRequest();
40+
41+
// Act
42+
$response = $this->handler->handle($request);
43+
44+
// Assert
45+
/** @var ListToolsHandler $result */
46+
$result = $response->result;
47+
$this->assertInstanceOf(ListToolsResult::class, $result);
48+
$this->assertCount(3, $result->tools);
49+
$this->assertNotNull($result->nextCursor);
50+
51+
$this->assertEquals('tool_0', $result->tools[0]->name);
52+
$this->assertEquals('tool_1', $result->tools[1]->name);
53+
$this->assertEquals('tool_2', $result->tools[2]->name);
54+
}
55+
56+
#[TestDox('Returns paginated tools with cursor')]
57+
public function testReturnsPaginatedToolsWithCursor(): void
58+
{
59+
// Arrange
60+
$this->addToolsToRegistry(10);
61+
$request = $this->createListToolsRequest(cursor: null);
62+
63+
// Act
64+
$response = $this->handler->handle($request);
65+
66+
// Assert
67+
/** @var ListToolsHandler $result */
68+
$result = $response->result;
69+
$this->assertInstanceOf(ListToolsResult::class, $result);
70+
$this->assertCount(3, $result->tools);
71+
$this->assertNotNull($result->nextCursor);
72+
73+
$this->assertEquals('tool_0', $result->tools[0]->name);
74+
$this->assertEquals('tool_1', $result->tools[1]->name);
75+
$this->assertEquals('tool_2', $result->tools[2]->name);
76+
}
77+
78+
#[TestDox('Returns second page with cursor')]
79+
public function testReturnsSecondPageWithCursor(): void
80+
{
81+
// Arrange
82+
$this->addToolsToRegistry(10);
83+
$firstPageRequest = $this->createListToolsRequest();
84+
$firstPageResponse = $this->handler->handle($firstPageRequest);
85+
86+
$secondPageRequest = $this->createListToolsRequest(cursor: $firstPageResponse->result->nextCursor);
87+
88+
// Act
89+
$response = $this->handler->handle($secondPageRequest);
90+
91+
// Assert
92+
/** @var ListToolsHandler $result */
93+
$result = $response->result;
94+
$this->assertInstanceOf(ListToolsResult::class, $result);
95+
$this->assertCount(3, $result->tools);
96+
$this->assertNotNull($result->nextCursor);
97+
98+
$this->assertEquals('tool_3', $result->tools[0]->name);
99+
$this->assertEquals('tool_4', $result->tools[1]->name);
100+
$this->assertEquals('tool_5', $result->tools[2]->name);
101+
}
102+
103+
#[TestDox('Returns last page with null cursor')]
104+
public function testReturnsLastPageWithNullCursor(): void
105+
{
106+
// Arrange
107+
$this->addToolsToRegistry(5);
108+
$firstPageRequest = $this->createListToolsRequest();
109+
$firstPageResponse = $this->handler->handle($firstPageRequest);
110+
111+
$secondPageRequest = $this->createListToolsRequest(cursor: $firstPageResponse->result->nextCursor);
112+
113+
// Act
114+
$response = $this->handler->handle($secondPageRequest);
115+
116+
// Assert
117+
/** @var ListToolsHandler $result */
118+
$result = $response->result;
119+
$this->assertInstanceOf(ListToolsResult::class, $result);
120+
$this->assertCount(2, $result->tools);
121+
$this->assertNull($result->nextCursor);
122+
123+
$this->assertEquals('tool_3', $result->tools[0]->name);
124+
$this->assertEquals('tool_4', $result->tools[1]->name);
125+
}
126+
127+
#[TestDox('Returns all tools when count is less than page size')]
128+
public function testReturnsAllToolsWhenCountIsLessThanPageSize(): void
129+
{
130+
// Arrange
131+
$this->addToolsToRegistry(2); // Less than page size (3)
132+
$request = $this->createListToolsRequest();
133+
134+
// Act
135+
$response = $this->handler->handle($request);
136+
137+
// Assert
138+
/** @var ListToolsHandler $result */
139+
$result = $response->result;
140+
$this->assertInstanceOf(ListToolsResult::class, $result);
141+
$this->assertCount(2, $result->tools);
142+
$this->assertNull($result->nextCursor);
143+
144+
$this->assertEquals('tool_0', $result->tools[0]->name);
145+
$this->assertEquals('tool_1', $result->tools[1]->name);
146+
}
147+
148+
#[TestDox('Handles empty registry')]
149+
public function testHandlesEmptyRegistry(): void
150+
{
151+
// Arrange
152+
$request = $this->createListToolsRequest();
153+
154+
// Act
155+
$response = $this->handler->handle($request);
156+
157+
// Assert
158+
/** @var ListToolsHandler $result */
159+
$result = $response->result;
160+
$this->assertInstanceOf(ListToolsResult::class, $result);
161+
$this->assertCount(0, $result->tools);
162+
$this->assertNull($result->nextCursor);
163+
}
164+
165+
#[TestDox('Throws exception for invalid cursor')]
166+
public function testThrowsExceptionForInvalidCursor(): void
167+
{
168+
// Arrange
169+
$this->addToolsToRegistry(5);
170+
$request = $this->createListToolsRequest(cursor: 'invalid-cursor');
171+
172+
// Assert
173+
$this->expectException(InvalidCursorException::class);
174+
175+
// Act
176+
$this->handler->handle($request);
177+
}
178+
179+
#[TestDox('Throws exception for cursor beyond bounds')]
180+
public function testThrowsExceptionForCursorBeyondBounds(): void
181+
{
182+
// Arrange
183+
$this->addToolsToRegistry(5);
184+
$outOfBoundsCursor = base64_encode('100');
185+
$request = $this->createListToolsRequest(cursor: $outOfBoundsCursor);
186+
187+
// Assert
188+
$this->expectException(InvalidCursorException::class);
189+
190+
// Act
191+
$this->handler->handle($request);
192+
}
193+
194+
#[TestDox('Handles cursor at exact boundary')]
195+
public function testHandlesCursorAtExactBoundary(): void
196+
{
197+
// Arrange
198+
$this->addToolsToRegistry(6);
199+
$exactBoundaryCursor = base64_encode('6');
200+
$request = $this->createListToolsRequest(cursor: $exactBoundaryCursor);
201+
202+
// Act
203+
$response = $this->handler->handle($request);
204+
205+
// Assert
206+
/** @var ListToolsHandler $result */
207+
$result = $response->result;
208+
$this->assertInstanceOf(ListToolsResult::class, $result);
209+
$this->assertCount(0, $result->tools);
210+
$this->assertNull($result->nextCursor);
211+
}
212+
213+
#[TestDox('Maintains stable cursors across calls')]
214+
public function testMaintainsStableCursorsAcrossCalls(): void
215+
{
216+
// Arrange
217+
$this->addToolsToRegistry(10);
218+
219+
// Act
220+
$request = $this->createListToolsRequest();
221+
$response1 = $this->handler->handle($request);
222+
$response2 = $this->handler->handle($request);
223+
224+
// Assert
225+
$this->assertEquals($response1->result->nextCursor, $response2->result->nextCursor);
226+
$this->assertEquals($response1->result->tools, $response2->result->tools);
227+
}
228+
229+
private function addToolsToRegistry(int $count): void
230+
{
231+
for ($i = 0; $i < $count; $i++) {
232+
$tool = new Tool(
233+
name: "tool_$i",
234+
inputSchema: ['type' => 'object'],
235+
description: "Test tool $i",
236+
annotations: null
237+
);
238+
239+
$this->registry->registerTool($tool, fn() => null);
240+
}
241+
}
242+
243+
private function createListToolsRequest(?string $cursor = null): ListToolsRequest
244+
{
245+
$mock = $this->getMockBuilder(ListToolsRequest::class)
246+
->setConstructorArgs([$cursor])
247+
->onlyMethods(['getId'])
248+
->getMock();
249+
250+
$mock->method('getId')->willReturn('test-request-id');
251+
252+
return $mock;
253+
}
254+
}

0 commit comments

Comments
 (0)