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