Skip to content

Commit 325b4aa

Browse files
bigdevlarrylarry.sulebalogun
andauthored
Add support for pagination on list requests (#55)
10 - Added the list resources handler with test class 10 - Update the ListPromptsHandler to use annotations 10 - Add the list resource handler, and it's test as well 10 - Added the registry logic 10 - Added the list tools handler logic 10 - Fix all php-cs-fixer related issues 10 - Resolved phpstan issues for ListPromptsHandlerTest 10 - Resolved phpstan issues for ListToolsHandlerTest 10 - Resolved phpstan issues for ListResourcesHandlerTest 10 - Resolved phpstan issues in registry class 10 - Resolved phpstan issues for the handler class 10 - Resolved php cs fixer issues 10 - Resolved all phpstan issues by generating the baseline again 10 - Remove the two loop operation and use foreach for optimization 10 - Removed unused code and resolved namespace 10 - Add signatures to interface and registry 10 - Add updated phpstan rule set 10 - Arranged according to property type 10 - Introduce reference page DTO, fix all failing test 10 - Resolve cs-fixer issues 10 - Resolve phpstan issues 10 - Resolve php cs fixer issues 10 - Created a reference directory for the Page DTO 10 - Remove manual loop for clean code readability Co-authored-by: larry.sulebalogun <[email protected]>
1 parent 4db8317 commit 325b4aa

File tree

12 files changed

+1016
-131
lines changed

12 files changed

+1016
-131
lines changed

phpstan-baseline.neon

Lines changed: 18 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ parameters:
5454
count: 1
5555
path: examples/02-discovery-http-userprofile/server.php
5656

57-
5857
-
5958
message: '#^Method Mcp\\Example\\ManualStdioExample\\SimpleHandlers\:\:getItemDetails\(\) return type has no value type specified in iterable type array\.$#'
6059
identifier: missingType.iterableValue
@@ -73,7 +72,6 @@ parameters:
7372
count: 2
7473
path: examples/04-combined-registration-http/server.php
7574

76-
7775
-
7876
message: '#^Method Mcp\\Example\\StdioEnvVariables\\EnvToolHandler\:\:processData\(\) return type has no value type specified in iterable type array\.$#'
7977
identifier: missingType.iterableValue
@@ -266,7 +264,6 @@ parameters:
266264
count: 2
267265
path: examples/07-complex-tool-schema-http/McpEventScheduler.php
268266

269-
270267
-
271268
message: '#^Method Mcp\\Example\\SchemaShowcaseExample\\SchemaShowcaseElements\:\:calculateRange\(\) return type has no value type specified in iterable type array\.$#'
272269
identifier: missingType.iterableValue
@@ -321,8 +318,6 @@ parameters:
321318
count: 1
322319
path: examples/08-schema-showcase-streamable/SchemaShowcaseElements.php
323320

324-
325-
326321
-
327322
message: '#^Call to an undefined method Mcp\\Capability\\Registry\\ResourceTemplateReference\:\:handle\(\)\.$#'
328323
identifier: method.notFound
@@ -342,103 +337,49 @@ parameters:
342337
path: src/Schema/Result/ReadResourceResult.php
343338

344339
-
345-
message: '#^Method Mcp\\Capability\\Registry\\ReferenceProviderInterface\:\:getPrompts\(\) invoked with 2 parameters, 0 required\.$#'
346-
identifier: arguments.count
347-
count: 1
348-
path: src/Server/RequestHandler/ListPromptsHandler.php
349-
350-
-
351-
message: '#^Result of && is always false\.$#'
352-
identifier: booleanAnd.alwaysFalse
353-
count: 1
354-
path: src/Server/RequestHandler/ListPromptsHandler.php
355-
356-
-
357-
message: '#^Strict comparison using \!\=\= between null and null will always evaluate to false\.$#'
358-
identifier: notIdentical.alwaysFalse
359-
count: 1
360-
path: src/Server/RequestHandler/ListPromptsHandler.php
361-
362-
-
363-
message: '#^Method Mcp\\Capability\\Registry\\ReferenceProviderInterface\:\:getResources\(\) invoked with 2 parameters, 0 required\.$#'
364-
identifier: arguments.count
365-
count: 1
366-
path: src/Server/RequestHandler/ListResourcesHandler.php
367-
368-
-
369-
message: '#^Result of && is always false\.$#'
370-
identifier: booleanAnd.alwaysFalse
371-
count: 1
372-
path: src/Server/RequestHandler/ListResourcesHandler.php
373-
374-
-
375-
message: '#^Strict comparison using \!\=\= between null and null will always evaluate to false\.$#'
376-
identifier: notIdentical.alwaysFalse
377-
count: 1
378-
path: src/Server/RequestHandler/ListResourcesHandler.php
379-
380-
-
381-
message: '#^Method Mcp\\Capability\\Registry\\ReferenceProviderInterface\:\:getTools\(\) invoked with 2 parameters, 0 required\.$#'
382-
identifier: arguments.count
383-
count: 1
384-
path: src/Server/RequestHandler/ListToolsHandler.php
385-
386-
-
387-
message: '#^Result of && is always false\.$#'
388-
identifier: booleanAnd.alwaysFalse
389-
count: 1
390-
path: src/Server/RequestHandler/ListToolsHandler.php
391-
392-
-
393-
message: '#^Strict comparison using \!\=\= between null and null will always evaluate to false\.$#'
394-
identifier: notIdentical.alwaysFalse
395-
count: 1
396-
path: src/Server/RequestHandler/ListToolsHandler.php
397-
398-
-
399-
message: '#^Method Mcp\\Server\\ServerBuilder\:\:getCompletionProviders\(\) return type has no value type specified in iterable type array\.$#'
340+
message: '#^Method Mcp\\Server\\ServerBuilder\:\:addPrompt\(\) has parameter \$handler with no value type specified in iterable type array\.$#'
400341
identifier: missingType.iterableValue
401342
count: 1
402343
path: src/Server/ServerBuilder.php
403344

404345
-
405-
message: '#^Method Mcp\\Server\\ServerBuilder\:\:setDiscovery\(\) has parameter \$excludeDirs with no value type specified in iterable type array\.$#'
346+
message: '#^Method Mcp\\Server\\ServerBuilder\:\:addResource\(\) has parameter \$handler with no value type specified in iterable type array\.$#'
406347
identifier: missingType.iterableValue
407348
count: 1
408349
path: src/Server/ServerBuilder.php
409350

410351
-
411-
message: '#^Method Mcp\\Server\\ServerBuilder\:\:setDiscovery\(\) has parameter \$scanDirs with no value type specified in iterable type array\.$#'
352+
message: '#^Method Mcp\\Server\\ServerBuilder\:\:addResourceTemplate\(\) has parameter \$handler with no value type specified in iterable type array\.$#'
412353
identifier: missingType.iterableValue
413354
count: 1
414355
path: src/Server/ServerBuilder.php
415356

416357
-
417-
message: '#^Method Mcp\\Server\\ServerBuilder\:\:addPrompt\(\) has parameter \$handler with no value type specified in iterable type array\.$#'
358+
message: '#^Method Mcp\\Server\\ServerBuilder\:\:addTool\(\) has parameter \$handler with no value type specified in iterable type array\.$#'
418359
identifier: missingType.iterableValue
419360
count: 1
420361
path: src/Server/ServerBuilder.php
421362

422363
-
423-
message: '#^Method Mcp\\Server\\ServerBuilder\:\:addResource\(\) has parameter \$handler with no value type specified in iterable type array\.$#'
364+
message: '#^Method Mcp\\Server\\ServerBuilder\:\:addTool\(\) has parameter \$inputSchema with no value type specified in iterable type array\.$#'
424365
identifier: missingType.iterableValue
425366
count: 1
426367
path: src/Server/ServerBuilder.php
427368

428369
-
429-
message: '#^Method Mcp\\Server\\ServerBuilder\:\:addResourceTemplate\(\) has parameter \$handler with no value type specified in iterable type array\.$#'
370+
message: '#^Method Mcp\\Server\\ServerBuilder\:\:getCompletionProviders\(\) return type has no value type specified in iterable type array\.$#'
430371
identifier: missingType.iterableValue
431372
count: 1
432373
path: src/Server/ServerBuilder.php
433374

434375
-
435-
message: '#^Method Mcp\\Server\\ServerBuilder\:\:addTool\(\) has parameter \$handler with no value type specified in iterable type array\.$#'
376+
message: '#^Method Mcp\\Server\\ServerBuilder\:\:setDiscovery\(\) has parameter \$excludeDirs with no value type specified in iterable type array\.$#'
436377
identifier: missingType.iterableValue
437378
count: 1
438379
path: src/Server/ServerBuilder.php
439380

440381
-
441-
message: '#^Method Mcp\\Server\\ServerBuilder\:\:addTool\(\) has parameter \$inputSchema with no value type specified in iterable type array\.$#'
382+
message: '#^Method Mcp\\Server\\ServerBuilder\:\:setDiscovery\(\) has parameter \$scanDirs with no value type specified in iterable type array\.$#'
442383
identifier: missingType.iterableValue
443384
count: 1
444385
path: src/Server/ServerBuilder.php
@@ -456,37 +397,37 @@ parameters:
456397
path: src/Server/ServerBuilder.php
457398

458399
-
459-
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$prompts type has no value type specified in iterable type array\.$#'
460-
identifier: missingType.iterableValue
400+
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$paginationLimit \(int\|null\) is never assigned null so it can be removed from the property type\.$#'
401+
identifier: property.unusedType
461402
count: 1
462403
path: src/Server/ServerBuilder.php
463404

464405
-
465-
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$resourceTemplates type has no value type specified in iterable type array\.$#'
466-
identifier: missingType.iterableValue
406+
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$paginationLimit is never read, only written\.$#'
407+
identifier: property.onlyWritten
467408
count: 1
468409
path: src/Server/ServerBuilder.php
469410

470411
-
471-
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$resources type has no value type specified in iterable type array\.$#'
412+
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$prompts type has no value type specified in iterable type array\.$#'
472413
identifier: missingType.iterableValue
473414
count: 1
474415
path: src/Server/ServerBuilder.php
475416

476417
-
477-
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$tools type has no value type specified in iterable type array\.$#'
418+
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$resourceTemplates type has no value type specified in iterable type array\.$#'
478419
identifier: missingType.iterableValue
479420
count: 1
480421
path: src/Server/ServerBuilder.php
481422

482423
-
483-
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$paginationLimit \(int\|null\) is never assigned null so it can be removed from the property type\.$#'
484-
identifier: property.unusedType
424+
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$resources type has no value type specified in iterable type array\.$#'
425+
identifier: missingType.iterableValue
485426
count: 1
486427
path: src/Server/ServerBuilder.php
487428

488429
-
489-
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$paginationLimit is never read, only written\.$#'
490-
identifier: property.onlyWritten
430+
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$tools type has no value type specified in iterable type array\.$#'
431+
identifier: missingType.iterableValue
491432
count: 1
492433
path: src/Server/ServerBuilder.php

src/Capability/Registry.php

Lines changed: 136 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
use Mcp\Event\ResourceListChangedEvent;
2323
use Mcp\Event\ResourceTemplateListChangedEvent;
2424
use Mcp\Event\ToolListChangedEvent;
25+
use Mcp\Exception\InvalidCursorException;
2526
use Mcp\Schema\Prompt;
2627
use Mcp\Schema\Resource;
2728
use Mcp\Schema\ResourceTemplate;
2829
use Mcp\Schema\ServerCapabilities;
2930
use Mcp\Schema\Tool;
31+
use Mcp\Server\RequestHandler\Reference\Page;
3032
use Psr\EventDispatcher\EventDispatcherInterface;
3133
use Psr\Log\LoggerInterface;
3234
use Psr\Log\NullLogger;
@@ -244,27 +246,92 @@ public function getPrompt(string $name): ?PromptReference
244246
return $this->prompts[$name] ?? null;
245247
}
246248

247-
public function getTools(): array
249+
public function getTools(?int $limit = null, ?string $cursor = null): Page
248250
{
249-
return array_map(fn (ToolReference $tool) => $tool->tool, $this->tools);
251+
$tools = [];
252+
foreach ($this->tools as $toolReference) {
253+
$tools[$toolReference->tool->name] = $toolReference->tool;
254+
}
255+
256+
if (null === $limit) {
257+
return new Page($tools, null);
258+
}
259+
260+
$paginatedTools = $this->paginateResults($tools, $limit, $cursor);
261+
262+
$nextCursor = $this->calculateNextCursor(
263+
\count($tools),
264+
$cursor,
265+
$limit
266+
);
267+
268+
return new Page($paginatedTools, $nextCursor);
250269
}
251270

252-
public function getResources(): array
271+
public function getResources(?int $limit = null, ?string $cursor = null): Page
253272
{
254-
return array_map(fn (ResourceReference $resource) => $resource->schema, $this->resources);
273+
$resources = [];
274+
foreach ($this->resources as $resourceReference) {
275+
$resources[$resourceReference->schema->uri] = $resourceReference->schema;
276+
}
277+
278+
if (null === $limit) {
279+
return new Page($resources, null);
280+
}
281+
282+
$paginatedResources = $this->paginateResults($resources, $limit, $cursor);
283+
284+
$nextCursor = $this->calculateNextCursor(
285+
\count($resources),
286+
$cursor,
287+
$limit
288+
);
289+
290+
return new Page($paginatedResources, $nextCursor);
255291
}
256292

257-
public function getPrompts(): array
293+
public function getPrompts(?int $limit = null, ?string $cursor = null): Page
258294
{
259-
return array_map(fn (PromptReference $prompt) => $prompt->prompt, $this->prompts);
295+
$prompts = [];
296+
foreach ($this->prompts as $promptReference) {
297+
$prompts[$promptReference->prompt->name] = $promptReference->prompt;
298+
}
299+
300+
if (null === $limit) {
301+
return new Page($prompts, null);
302+
}
303+
304+
$paginatedPrompts = $this->paginateResults($prompts, $limit, $cursor);
305+
306+
$nextCursor = $this->calculateNextCursor(
307+
\count($prompts),
308+
$cursor,
309+
$limit
310+
);
311+
312+
return new Page($paginatedPrompts, $nextCursor);
260313
}
261314

262-
public function getResourceTemplates(): array
315+
public function getResourceTemplates(?int $limit = null, ?string $cursor = null): Page
263316
{
264-
return array_map(
265-
fn (ResourceTemplateReference $template) => $template->resourceTemplate,
266-
$this->resourceTemplates
317+
$templates = [];
318+
foreach ($this->resourceTemplates as $templateReference) {
319+
$templates[$templateReference->resourceTemplate->uriTemplate] = $templateReference->resourceTemplate;
320+
}
321+
322+
if (null === $limit) {
323+
return new Page($templates, null);
324+
}
325+
326+
$paginatedTemplates = $this->paginateResults($templates, $limit, $cursor);
327+
328+
$nextCursor = $this->calculateNextCursor(
329+
\count($templates),
330+
$cursor,
331+
$limit
267332
);
333+
334+
return new Page($paginatedTemplates, $nextCursor);
268335
}
269336

270337
public function hasElements(): bool
@@ -327,4 +394,63 @@ public function setDiscoveryState(DiscoveryState $state): void
327394
}
328395
}
329396
}
397+
398+
/**
399+
* Calculate next cursor for pagination.
400+
*
401+
* @param int $totalItems Count of all items
402+
* @param string|null $currentCursor Current cursor position
403+
* @param int $limit Number requested/returned per page
404+
*/
405+
private function calculateNextCursor(int $totalItems, ?string $currentCursor, int $limit): ?string
406+
{
407+
$currentOffset = 0;
408+
409+
if (null !== $currentCursor) {
410+
$decodedCursor = base64_decode($currentCursor, true);
411+
if (false !== $decodedCursor && is_numeric($decodedCursor)) {
412+
$currentOffset = (int) $decodedCursor;
413+
}
414+
}
415+
416+
$nextOffset = $currentOffset + $limit;
417+
418+
if ($nextOffset < $totalItems) {
419+
return base64_encode((string) $nextOffset);
420+
}
421+
422+
return null;
423+
}
424+
425+
/**
426+
* Helper method to paginate results using cursor-based pagination.
427+
*
428+
* @param array<int|string, mixed> $items The full array of items to paginate The full array of items to paginate
429+
* @param int $limit Maximum number of items to return
430+
* @param string|null $cursor Base64 encoded offset position
431+
*
432+
* @return array<int|string, mixed> Paginated results
433+
*
434+
* @throws InvalidCursorException When cursor is invalid (MCP error code -32602)
435+
*/
436+
private function paginateResults(array $items, int $limit, ?string $cursor = null): array
437+
{
438+
$offset = 0;
439+
if (null !== $cursor) {
440+
$decodedCursor = base64_decode($cursor, true);
441+
442+
if (false === $decodedCursor || !is_numeric($decodedCursor)) {
443+
throw new InvalidCursorException($cursor);
444+
}
445+
446+
$offset = (int) $decodedCursor;
447+
448+
// Validate offset is within reasonable bounds
449+
if ($offset < 0 || $offset > \count($items)) {
450+
throw new InvalidCursorException($cursor);
451+
}
452+
}
453+
454+
return array_values(\array_slice($items, $offset, $limit));
455+
}
330456
}

0 commit comments

Comments
 (0)