[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

@ -11,6 +11,7 @@
namespace Mcp\Capability;
use Mcp\Capability\Discovery\DiscoveryState;
use Mcp\Capability\Registry\PromptReference;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Capability\Registry\ReferenceRegistryInterface;
@ -260,8 +261,10 @@ final class Registry implements ReferenceProviderInterface, ReferenceRegistryInt
public function getResourceTemplates(): array
{
return array_map(fn (ResourceTemplateReference $template) => $template->resourceTemplate,
$this->resourceTemplates);
return array_map(
fn (ResourceTemplateReference $template) => $template->resourceTemplate,
$this->resourceTemplates
);
}
public function hasElements(): bool
@ -271,4 +274,57 @@ final class Registry implements ReferenceProviderInterface, ReferenceRegistryInt
|| !empty($this->prompts)
|| !empty($this->resourceTemplates);
}
/**
* Get the current discovery state (only discovered elements, not manual ones).
*/
public function getDiscoveryState(): DiscoveryState
{
return new DiscoveryState(
tools: array_filter($this->tools, fn ($tool) => !$tool->isManual),
resources: array_filter($this->resources, fn ($resource) => !$resource->isManual),
prompts: array_filter($this->prompts, fn ($prompt) => !$prompt->isManual),
resourceTemplates: array_filter($this->resourceTemplates, fn ($template) => !$template->isManual),
);
}
/**
* Set discovery state, replacing all discovered elements.
* Manual elements are preserved.
*/
public function setDiscoveryState(DiscoveryState $state): void
{
// Clear existing discovered elements
$this->clear();
// Import new discovered elements
foreach ($state->getTools() as $name => $tool) {
$this->tools[$name] = $tool;
}
foreach ($state->getResources() as $uri => $resource) {
$this->resources[$uri] = $resource;
}
foreach ($state->getPrompts() as $name => $prompt) {
$this->prompts[$name] = $prompt;
}
foreach ($state->getResourceTemplates() as $uriTemplate => $template) {
$this->resourceTemplates[$uriTemplate] = $template;
}
// Dispatch events for the imported elements
if ($this->eventDispatcher instanceof EventDispatcherInterface) {
if (!empty($state->getTools())) {
$this->eventDispatcher->dispatch(new ToolListChangedEvent());
}
if (!empty($state->getResources()) || !empty($state->getResourceTemplates())) {
$this->eventDispatcher->dispatch(new ResourceListChangedEvent());
}
if (!empty($state->getPrompts())) {
$this->eventDispatcher->dispatch(new PromptListChangedEvent());
}
}
}
}