[Server] Implement discovery caching for improved performance (#39)

* feat: implement discovery caching with state-based approach

- Add DiscoveryState class to encapsulate discovered MCP capabilities
- Add exportDiscoveryState and importDiscoveryState methods to Registry
- Modify Discoverer to return DiscoveryState instead of void
- Create CachedDiscoverer decorator for caching discovery results
- Add importDiscoveryState method to ReferenceRegistryInterface
- Update ServerBuilder to use caching with withCache() method
- Update tests to work with new state-based approach
- Add example demonstrating cached discovery functionality
- Add PSR-16 SimpleCache and Symfony Cache dependencies

* docs: add comprehensive discovery caching documentation

- Add detailed documentation explaining discovery caching architecture
- Include usage examples for different cache implementations
- Document performance benefits and best practices
- Add troubleshooting guide and migration instructions
- Include complete API reference for all caching components
- Fix PHPStan issues by regenerating baseline
- Apply PHP CS Fixer formatting to all new files

* refactor: address reviewer feedback and clean up discovery caching

- Make DiscoveryState class final
- Cache DiscoveryState objects directly instead of arrays (no serialization needed)
- Rename exportDiscoveryState/importDiscoveryState to getDiscoveryState/setDiscoveryState
- Add getDiscoveryState method to ReferenceRegistryInterface
- Remove TTL parameter from CachedDiscoverer (no expiration by default)
- Remove unused methods from DiscoveryState (toArray, fromArray, merge)
- Simplify ServerBuilder to handle decoration internally
- Make Discoverer.applyDiscoveryState() internal (no longer public API)
- Simplify documentation to focus on user perspective
- Remove unnecessary development comments
- Update all tests to work with new architecture
- All tests pass, PHPStan clean, code formatting applied

* refactor: clean up comments and formatting in server.php example

* refactor: improve code formatting and consistency in discovery classes and example server

* Fix method names in cached discovery example

- Change withServerInfo() to setServerInfo()
- Change withDiscovery() to setDiscovery()
- Change withLogger() to setLogger()
- Fixes PHPStan static analysis errors

* refactor: update method names in ServerBuilder for consistency

- Change withCache() to setCache() in ServerBuilder
- Update example and documentation to reflect method name changes
- Ensure consistency across caching implementation in examples and documentation

* refactor: reorganize examples and fix PHPStan baseline

- Move cached discovery example from examples/10 to examples/09
- Update composer.json autoload-dev namespace mapping
- Fix PHPStan baseline by removing invalid entries for non-existent files
- Update server.php to follow proper example pattern with logging and lifecycle

* fix: correct formatting in server.php
This commit is contained in:
xentixar 2025-09-25 00:39:27 +05:45 • committed by GitHub
commit 4db831778b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -19,7 +19,11 @@ use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Prompt\Completion\EnumCompletionProvider;
use Mcp\Capability\Prompt\Completion\ListCompletionProvider;
use Mcp\Capability\Prompt\Completion\ProviderInterface;
use Mcp\Capability\Registry\PromptReference;
use Mcp\Capability\Registry\ReferenceRegistryInterface;
use Mcp\Capability\Registry\ResourceReference;
use Mcp\Capability\Registry\ResourceTemplateReference;
use Mcp\Capability\Registry\ToolReference;
use Mcp\Exception\ExceptionInterface;
use Mcp\Schema\Prompt;
use Mcp\Schema\PromptArgument;
@ -54,13 +58,13 @@ class Discoverer
}
/**
* Discover MCP elements in the specified directories.
* Discover MCP elements in the specified directories and return the discovery state.
*
* @param string $basePath the base path for resolving directories
* @param array<string> $directories list of directories (relative to base path) to scan
* @param array<string> $excludeDirs list of directories (relative to base path) to exclude from the scan
*/
public function discover(string $basePath, array $directories, array $excludeDirs = []): void
public function discover(string $basePath, array $directories, array $excludeDirs = []): DiscoveryState
{
$startTime = microtime(true);
$discoveredCount = [
@ -70,6 +74,11 @@ class Discoverer
'resourceTemplates' => 0,
];
$tools = [];
$resources = [];
$prompts = [];
$resourceTemplates = [];
try {
$finder = new Finder();
$absolutePaths = [];
@ -86,7 +95,10 @@ class Discoverer
'base_path' => $basePath,
]);
return;
$emptyState = new DiscoveryState();
$this->registry->setDiscoveryState($emptyState);
return $emptyState;
}
$finder->files()
@ -95,7 +107,7 @@ class Discoverer
->name('*.php');
foreach ($finder as $file) {
$this->processFile($file, $discoveredCount);
$this->processFile($file, $discoveredCount, $tools, $resources, $prompts, $resourceTemplates);
}
} catch (\Throwable $e) {
$this->logger->error('Error during file finding process for MCP discovery'.json_encode($e->getTrace(), \JSON_PRETTY_PRINT), [
@ -112,14 +124,24 @@ class Discoverer
'prompts' => $discoveredCount['prompts'],
'resourceTemplates' => $discoveredCount['resourceTemplates'],
]);
$discoveryState = new DiscoveryState($tools, $resources, $prompts, $resourceTemplates);
$this->registry->setDiscoveryState($discoveryState);
return $discoveryState;
}
/**
* Process a single PHP file for MCP elements on classes or methods.
*
* @param DiscoveredCount $discoveredCount
* @param DiscoveredCount $discoveredCount
* @param array<string, ToolReference> $tools
* @param array<string, ResourceReference> $resources
* @param array<string, PromptReference> $prompts
* @param array<string, ResourceTemplateReference> $resourceTemplates
*/
private function processFile(SplFileInfo $file, array &$discoveredCount): void
private function processFile(SplFileInfo $file, array &$discoveredCount, array &$tools, array &$resources, array &$prompts, array &$resourceTemplates): void
{
$filePath = $file->getRealPath();
if (false === $filePath) {
@ -150,7 +172,7 @@ class Discoverer
foreach ($attributeTypes as $attributeType) {
$classAttribute = $reflectionClass->getAttributes($attributeType, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null;
if ($classAttribute) {
$this->processMethod($invokeMethod, $discoveredCount, $classAttribute);
$this->processMethod($invokeMethod, $discoveredCount, $classAttribute, $tools, $resources, $prompts, $resourceTemplates);
$processedViaClassAttribute = true;
break;
}
@ -170,7 +192,7 @@ class Discoverer
foreach ($attributeTypes as $attributeType) {
$methodAttribute = $method->getAttributes($attributeType, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null;
if ($methodAttribute) {
$this->processMethod($method, $discoveredCount, $methodAttribute);
$this->processMethod($method, $discoveredCount, $methodAttribute, $tools, $resources, $prompts, $resourceTemplates);
break;
}
}
@ -192,11 +214,15 @@ class Discoverer
* Process a method with a given MCP attribute instance.
* Can be called for regular methods or the __invoke method of an invokable class.
*
* @param \ReflectionMethod $method The target method (e.g., regular method or __invoke).
* @param DiscoveredCount $discoveredCount pass by reference to update counts
* @param \ReflectionAttribute<McpTool|McpResource|McpPrompt|McpResourceTemplate> $attribute the ReflectionAttribute instance found (on method or class)
* @param \ReflectionMethod $method The target method (e.g., regular method or __invoke).
* @param DiscoveredCount $discoveredCount pass by reference to update counts
* @param \ReflectionAttribute<McpTool|McpResource|McpPrompt|McpResourceTemplate> $attribute the ReflectionAttribute instance found (on method or class)
* @param array<string, ToolReference> $tools
* @param array<string, ResourceReference> $resources
* @param array<string, PromptReference> $prompts
* @param array<string, ResourceTemplateReference> $resourceTemplates
*/
private function processMethod(\ReflectionMethod $method, array &$discoveredCount, \ReflectionAttribute $attribute): void
private function processMethod(\ReflectionMethod $method, array &$discoveredCount, \ReflectionAttribute $attribute, array &$tools, array &$resources, array &$prompts, array &$resourceTemplates): void
{
$className = $method->getDeclaringClass()->getName();
$classShortName = $method->getDeclaringClass()->getShortName();
@ -213,7 +239,7 @@ class Discoverer
$description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null;
$inputSchema = $this->schemaGenerator->generate($method);
$tool = new Tool($name, $inputSchema, $description, $instance->annotations);
$this->registry->registerTool($tool, [$className, $methodName]);
$tools[$name] = new ToolReference($tool, [$className, $methodName], false);
++$discoveredCount['tools'];
break;
@ -225,7 +251,7 @@ class Discoverer
$size = $instance->size;
$annotations = $instance->annotations;
$resource = new Resource($instance->uri, $name, $description, $mimeType, $annotations, $size);
$this->registry->registerResource($resource, [$className, $methodName]);
$resources[$instance->uri] = new ResourceReference($resource, [$className, $methodName], false);
++$discoveredCount['resources'];
break;
@ -245,7 +271,7 @@ class Discoverer
}
$prompt = new Prompt($name, $description, $arguments);
$completionProviders = $this->getCompletionProviders($method);
$this->registry->registerPrompt($prompt, [$className, $methodName], $completionProviders);
$prompts[$name] = new PromptReference($prompt, [$className, $methodName], false, $completionProviders);
++$discoveredCount['prompts'];
break;
@ -257,7 +283,7 @@ class Discoverer
$annotations = $instance->annotations;
$resourceTemplate = new ResourceTemplate($instance->uriTemplate, $name, $description, $mimeType, $annotations);
$completionProviders = $this->getCompletionProviders($method);
$this->registry->registerResourceTemplate($resourceTemplate, [$className, $methodName], $completionProviders);
$resourceTemplates[$instance->uriTemplate] = new ResourceTemplateReference($resourceTemplate, [$className, $methodName], false, $completionProviders);
++$discoveredCount['resourceTemplates'];
break;
}