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
95 changes: 18 additions & 77 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ parameters:
count: 1
path: examples/02-discovery-http-userprofile/server.php


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


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


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



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

-
message: '#^Method Mcp\\Capability\\Registry\\ReferenceProviderInterface\:\:getPrompts\(\) invoked with 2 parameters, 0 required\.$#'
identifier: arguments.count
count: 1
path: src/Server/RequestHandler/ListPromptsHandler.php

-
message: '#^Result of && is always false\.$#'
identifier: booleanAnd.alwaysFalse
count: 1
path: src/Server/RequestHandler/ListPromptsHandler.php

-
message: '#^Strict comparison using \!\=\= between null and null will always evaluate to false\.$#'
identifier: notIdentical.alwaysFalse
count: 1
path: src/Server/RequestHandler/ListPromptsHandler.php

-
message: '#^Method Mcp\\Capability\\Registry\\ReferenceProviderInterface\:\:getResources\(\) invoked with 2 parameters, 0 required\.$#'
identifier: arguments.count
count: 1
path: src/Server/RequestHandler/ListResourcesHandler.php

-
message: '#^Result of && is always false\.$#'
identifier: booleanAnd.alwaysFalse
count: 1
path: src/Server/RequestHandler/ListResourcesHandler.php

-
message: '#^Strict comparison using \!\=\= between null and null will always evaluate to false\.$#'
identifier: notIdentical.alwaysFalse
count: 1
path: src/Server/RequestHandler/ListResourcesHandler.php

-
message: '#^Method Mcp\\Capability\\Registry\\ReferenceProviderInterface\:\:getTools\(\) invoked with 2 parameters, 0 required\.$#'
identifier: arguments.count
count: 1
path: src/Server/RequestHandler/ListToolsHandler.php

-
message: '#^Result of && is always false\.$#'
identifier: booleanAnd.alwaysFalse
count: 1
path: src/Server/RequestHandler/ListToolsHandler.php

-
message: '#^Strict comparison using \!\=\= between null and null will always evaluate to false\.$#'
identifier: notIdentical.alwaysFalse
count: 1
path: src/Server/RequestHandler/ListToolsHandler.php

-
message: '#^Method Mcp\\Server\\ServerBuilder\:\:getCompletionProviders\(\) return type has no value type specified in iterable type array\.$#'
message: '#^Method Mcp\\Server\\ServerBuilder\:\:addPrompt\(\) has parameter \$handler with no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
count: 1
path: src/Server/ServerBuilder.php

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

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

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

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

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

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

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

-
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$prompts type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
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\:\:\$resourceTemplates type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
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\:\:\$resources type has no value type specified in iterable type array\.$#'
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$prompts 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\:\:\$tools type has no value type specified in iterable type array\.$#'
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$resourceTemplates 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\:\:\$paginationLimit \(int\|null\) is never assigned null so it can be removed from the property type\.$#'
identifier: property.unusedType
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$resources 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\:\:\$paginationLimit is never read, only written\.$#'
identifier: property.onlyWritten
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$tools type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
count: 1
path: src/Server/ServerBuilder.php
146 changes: 136 additions & 10 deletions src/Capability/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
use Mcp\Event\ResourceListChangedEvent;
use Mcp\Event\ResourceTemplateListChangedEvent;
use Mcp\Event\ToolListChangedEvent;
use Mcp\Exception\InvalidCursorException;
use Mcp\Schema\Prompt;
use Mcp\Schema\Resource;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\ServerCapabilities;
use Mcp\Schema\Tool;
use Mcp\Server\RequestHandler\Reference\Page;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
Expand Down Expand Up @@ -244,27 +246,92 @@ public function getPrompt(string $name): ?PromptReference
return $this->prompts[$name] ?? null;
}

public function getTools(): array
public function getTools(?int $limit = null, ?string $cursor = null): Page
{
return array_map(fn (ToolReference $tool) => $tool->tool, $this->tools);
$tools = [];
foreach ($this->tools as $toolReference) {
$tools[$toolReference->tool->name] = $toolReference->tool;
}

if (null === $limit) {
return new Page($tools, null);
}

$paginatedTools = $this->paginateResults($tools, $limit, $cursor);

$nextCursor = $this->calculateNextCursor(
\count($tools),
$cursor,
$limit
);

return new Page($paginatedTools, $nextCursor);
}

public function getResources(): array
public function getResources(?int $limit = null, ?string $cursor = null): Page
{
return array_map(fn (ResourceReference $resource) => $resource->schema, $this->resources);
$resources = [];
foreach ($this->resources as $resourceReference) {
$resources[$resourceReference->schema->uri] = $resourceReference->schema;
}

if (null === $limit) {
return new Page($resources, null);
}

$paginatedResources = $this->paginateResults($resources, $limit, $cursor);

$nextCursor = $this->calculateNextCursor(
\count($resources),
$cursor,
$limit
);

return new Page($paginatedResources, $nextCursor);
}

public function getPrompts(): array
public function getPrompts(?int $limit = null, ?string $cursor = null): Page
{
return array_map(fn (PromptReference $prompt) => $prompt->prompt, $this->prompts);
$prompts = [];
foreach ($this->prompts as $promptReference) {
$prompts[$promptReference->prompt->name] = $promptReference->prompt;
}

if (null === $limit) {
return new Page($prompts, null);
}

$paginatedPrompts = $this->paginateResults($prompts, $limit, $cursor);

$nextCursor = $this->calculateNextCursor(
\count($prompts),
$cursor,
$limit
);

return new Page($paginatedPrompts, $nextCursor);
}

public function getResourceTemplates(): array
public function getResourceTemplates(?int $limit = null, ?string $cursor = null): Page
{
return array_map(
fn (ResourceTemplateReference $template) => $template->resourceTemplate,
$this->resourceTemplates
$templates = [];
foreach ($this->resourceTemplates as $templateReference) {
$templates[$templateReference->resourceTemplate->uriTemplate] = $templateReference->resourceTemplate;
}

if (null === $limit) {
return new Page($templates, null);
}

$paginatedTemplates = $this->paginateResults($templates, $limit, $cursor);

$nextCursor = $this->calculateNextCursor(
\count($templates),
$cursor,
$limit
);

return new Page($paginatedTemplates, $nextCursor);
}

public function hasElements(): bool
Expand Down Expand Up @@ -327,4 +394,63 @@ public function setDiscoveryState(DiscoveryState $state): void
}
}
}

/**
* Calculate next cursor for pagination.
*
* @param int $totalItems Count of all items
* @param string|null $currentCursor Current cursor position
* @param int $limit Number requested/returned per page
*/
private function calculateNextCursor(int $totalItems, ?string $currentCursor, int $limit): ?string
{
$currentOffset = 0;

if (null !== $currentCursor) {
$decodedCursor = base64_decode($currentCursor, true);
if (false !== $decodedCursor && is_numeric($decodedCursor)) {
$currentOffset = (int) $decodedCursor;
}
}

$nextOffset = $currentOffset + $limit;

if ($nextOffset < $totalItems) {
return base64_encode((string) $nextOffset);
}

return null;
}

/**
* Helper method to paginate results using cursor-based pagination.
*
* @param array<int|string, mixed> $items The full array of items to paginate The full array of items to paginate
* @param int $limit Maximum number of items to return
* @param string|null $cursor Base64 encoded offset position
*
* @return array<int|string, mixed> Paginated results
*
* @throws InvalidCursorException When cursor is invalid (MCP error code -32602)
*/
private function paginateResults(array $items, int $limit, ?string $cursor = null): array
{
$offset = 0;
if (null !== $cursor) {
$decodedCursor = base64_decode($cursor, true);

if (false === $decodedCursor || !is_numeric($decodedCursor)) {
throw new InvalidCursorException($cursor);
}

$offset = (int) $decodedCursor;

// Validate offset is within reasonable bounds
if ($offset < 0 || $offset > \count($items)) {
throw new InvalidCursorException($cursor);
}
}

return array_values(\array_slice($items, $offset, $limit));
}
}
Loading