Skip to content

Commit e70f617

Browse files
committed
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
1 parent b1e54f1 commit e70f617

File tree

11 files changed

+649
-24
lines changed

11 files changed

+649
-24
lines changed

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"phpstan/phpstan": "^2.1",
3434
"phpunit/phpunit": "^10.5",
3535
"psr/cache": "^3.0",
36+
"psr/simple-cache": "^3.0",
37+
"symfony/cache": "^6.4 || ^7.3",
3638
"symfony/console": "^6.4 || ^7.3",
3739
"symfony/process": "^6.4 || ^7.3"
3840
},
@@ -51,6 +53,7 @@
5153
"Mcp\\Example\\DependenciesStdioExample\\": "examples/06-custom-dependencies-stdio/",
5254
"Mcp\\Example\\ComplexSchemaHttpExample\\": "examples/07-complex-tool-schema-http/",
5355
"Mcp\\Example\\SchemaShowcaseExample\\": "examples/08-schema-showcase-streamable/",
56+
"Mcp\\Example\\CachedDiscoveryExample\\": "examples/10-cached-discovery-stdio/",
5457
"Mcp\\Tests\\": "tests/"
5558
}
5659
},
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the official PHP MCP SDK.
7+
*
8+
* A collaboration between Symfony and the PHP Foundation.
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Mcp\Example\CachedDiscoveryExample;
15+
16+
use Mcp\Capability\Attribute\McpTool;
17+
18+
/**
19+
* Example MCP elements for demonstrating cached discovery.
20+
*
21+
* This class contains simple calculator tools that will be discovered
22+
* and cached for improved performance on subsequent server starts.
23+
*/
24+
class CachedCalculatorElements
25+
{
26+
#[McpTool(name: 'add_numbers')]
27+
public function add(int $a, int $b): int
28+
{
29+
return $a + $b;
30+
}
31+
32+
#[McpTool(name: 'multiply_numbers')]
33+
public function multiply(int $a, int $b): int
34+
{
35+
return $a * $b;
36+
}
37+
38+
#[McpTool(name: 'divide_numbers')]
39+
public function divide(int $a, int $b): float
40+
{
41+
if (0 === $b) {
42+
throw new \InvalidArgumentException('Division by zero is not allowed');
43+
}
44+
45+
return $a / $b;
46+
}
47+
48+
#[McpTool(name: 'power')]
49+
public function power(int $base, int $exponent): int
50+
{
51+
return (int) $base ** $exponent;
52+
}
53+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
declare(strict_types=1);
5+
6+
/*
7+
* This file is part of the official PHP MCP SDK.
8+
*
9+
* A collaboration between Symfony and the PHP Foundation.
10+
*
11+
* For the full copyright and license information, please view the LICENSE
12+
* file that was distributed with this source code.
13+
*/
14+
15+
require_once __DIR__.'/../bootstrap.php';
16+
17+
use Mcp\Server;
18+
use Mcp\Server\Transport\StdioTransport;
19+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
20+
use Symfony\Component\Cache\Psr16Cache;
21+
22+
// Example showing how to use discovery caching for improved performance
23+
// This is especially useful in development environments where the server
24+
// is restarted frequently, or in production where discovery happens on every request.
25+
26+
Server::make()
27+
->withServerInfo('Cached Discovery Calculator', '1.0.0', 'Calculator with cached discovery for better performance.')
28+
->withDiscovery(__DIR__, ['.'])
29+
->withLogger(logger())
30+
->withCache(new Psr16Cache(new ArrayAdapter())) // Enable discovery caching
31+
->build()
32+
->connect(new StdioTransport());
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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\Capability\Discovery;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Psr\SimpleCache\CacheInterface;
16+
17+
/**
18+
* Cached decorator for the Discoverer class.
19+
*
20+
* This decorator caches the results of file system operations and reflection
21+
* to improve performance when discovery is called multiple times.
22+
*
23+
* @author Xentixar <[email protected]>
24+
*/
25+
class CachedDiscoverer
26+
{
27+
private const CACHE_PREFIX = 'mcp_discovery_';
28+
private const CACHE_TTL = 3600; // 1 hour default TTL
29+
30+
public function __construct(
31+
private readonly Discoverer $discoverer,
32+
private readonly CacheInterface $cache,
33+
private readonly LoggerInterface $logger,
34+
private readonly int $cacheTtl = self::CACHE_TTL,
35+
) {
36+
}
37+
38+
/**
39+
* Discover MCP elements in the specified directories with caching.
40+
*
41+
* @param string $basePath the base path for resolving directories
42+
* @param array<string> $directories list of directories (relative to base path) to scan
43+
* @param array<string> $excludeDirs list of directories (relative to base path) to exclude from the scan
44+
*/
45+
public function discover(string $basePath, array $directories, array $excludeDirs = []): DiscoveryState
46+
{
47+
$cacheKey = $this->generateCacheKey($basePath, $directories, $excludeDirs);
48+
49+
// Check if we have cached results
50+
$cachedResult = $this->cache->get($cacheKey);
51+
if (null !== $cachedResult) {
52+
$this->logger->debug('Using cached discovery results', [
53+
'cache_key' => $cacheKey,
54+
'base_path' => $basePath,
55+
'directories' => $directories,
56+
]);
57+
58+
// Restore the discovery state from cache
59+
return $this->restoreDiscoveryStateFromCache($cachedResult);
60+
}
61+
62+
$this->logger->debug('Cache miss, performing fresh discovery', [
63+
'cache_key' => $cacheKey,
64+
'base_path' => $basePath,
65+
'directories' => $directories,
66+
]);
67+
68+
// Perform fresh discovery
69+
$discoveryState = $this->discoverer->discover($basePath, $directories, $excludeDirs);
70+
71+
// Cache the results
72+
$this->cacheDiscoveryResults($cacheKey, $discoveryState);
73+
74+
return $discoveryState;
75+
}
76+
77+
/**
78+
* Generate a cache key based on discovery parameters.
79+
*
80+
* @param array<string> $directories
81+
* @param array<string> $excludeDirs
82+
*/
83+
private function generateCacheKey(string $basePath, array $directories, array $excludeDirs): string
84+
{
85+
$keyData = [
86+
'base_path' => $basePath,
87+
'directories' => $directories,
88+
'exclude_dirs' => $excludeDirs,
89+
];
90+
91+
return self::CACHE_PREFIX.md5(serialize($keyData));
92+
}
93+
94+
/**
95+
* Cache the discovery state.
96+
*/
97+
private function cacheDiscoveryResults(string $cacheKey, DiscoveryState $state): void
98+
{
99+
try {
100+
// Convert state to array for caching
101+
$stateData = $state->toArray();
102+
103+
// Store in cache
104+
$this->cache->set($cacheKey, $stateData, $this->cacheTtl);
105+
106+
$this->logger->debug('Cached discovery results', [
107+
'cache_key' => $cacheKey,
108+
'ttl' => $this->cacheTtl,
109+
'element_count' => $state->getElementCount(),
110+
]);
111+
} catch (\Throwable $e) {
112+
$this->logger->warning('Failed to cache discovery results', [
113+
'cache_key' => $cacheKey,
114+
'exception' => $e->getMessage(),
115+
]);
116+
}
117+
}
118+
119+
/**
120+
* Restore discovery state from cached data.
121+
*
122+
* @param array<string, mixed> $cachedResult
123+
*/
124+
private function restoreDiscoveryStateFromCache(array $cachedResult): DiscoveryState
125+
{
126+
try {
127+
return DiscoveryState::fromArray($cachedResult);
128+
} catch (\Throwable $e) {
129+
$this->logger->error('Failed to restore discovery state from cache', [
130+
'exception' => $e->getMessage(),
131+
]);
132+
throw $e;
133+
}
134+
}
135+
136+
/**
137+
* Clear the discovery cache.
138+
* Useful for development or when files change.
139+
*/
140+
public function clearCache(): void
141+
{
142+
// This is a simple implementation that clears all discovery cache entries
143+
// In a more sophisticated implementation, we might want to track cache keys
144+
// and clear them selectively
145+
146+
$this->cache->clear();
147+
$this->logger->info('Discovery cache cleared');
148+
}
149+
}

0 commit comments

Comments
 (0)