Skip to content

Commit 0060ea1

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 9e16883 commit 0060ea1

File tree

11 files changed

+712
-87
lines changed

11 files changed

+712
-87
lines changed

composer.json

Lines changed: 66 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,70 @@
11
{
2-
"name": "mcp/sdk",
3-
"type": "library",
4-
"description": "Model Context Protocol SDK for Client and Server applications in PHP",
5-
"license": "MIT",
6-
"authors": [
7-
{
8-
"name": "Christopher Hertel",
9-
"email": "[email protected]"
10-
},
11-
{
12-
"name": "Kyrian Obikwelu",
13-
"email": "[email protected]"
14-
},
15-
{
16-
"name": "Tobias Nyholm",
17-
"email": "[email protected]"
18-
}
19-
],
20-
"require": {
21-
"php": "^8.1",
22-
"ext-fileinfo": "*",
23-
"opis/json-schema": "^2.4",
24-
"phpdocumentor/reflection-docblock": "^5.6",
25-
"psr/clock": "^1.0",
26-
"psr/container": "^2.0",
27-
"psr/event-dispatcher": "^1.0",
28-
"psr/http-factory": "^1.1",
29-
"psr/http-message": "^2.0",
30-
"psr/log": "^1.0 || ^2.0 || ^3.0",
31-
"symfony/finder": "^6.4 || ^7.3",
32-
"symfony/uid": "^6.4 || ^7.3"
2+
"name": "mcp/sdk",
3+
"type": "library",
4+
"description": "Model Context Protocol SDK for Client and Server applications in PHP",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "Christopher Hertel",
9+
"email": "[email protected]"
3310
},
34-
"require-dev": {
35-
"php-cs-fixer/shim": "^3.84",
36-
"phpstan/phpstan": "^2.1",
37-
"phpunit/phpunit": "^10.5",
38-
"psr/cache": "^3.0",
39-
"symfony/console": "^6.4 || ^7.3",
40-
"symfony/process": "^6.4 || ^7.3",
41-
"nyholm/psr7": "^1.8",
42-
"nyholm/psr7-server": "^1.1",
43-
"laminas/laminas-httphandlerrunner": "^2.12"
11+
{
12+
"name": "Kyrian Obikwelu",
13+
"email": "[email protected]"
4414
},
45-
"autoload": {
46-
"psr-4": {
47-
"Mcp\\": "src/"
48-
}
49-
},
50-
"autoload-dev": {
51-
"psr-4": {
52-
"Mcp\\Example\\StdioCalculatorExample\\": "examples/01-discovery-stdio-calculator/",
53-
"Mcp\\Example\\HttpUserProfileExample\\": "examples/02-discovery-http-userprofile/",
54-
"Mcp\\Example\\ManualStdioExample\\": "examples/03-manual-registration-stdio/",
55-
"Mcp\\Example\\CombinedHttpExample\\": "examples/04-combined-registration-http/",
56-
"Mcp\\Example\\StdioEnvVariables\\": "examples/05-stdio-env-variables/",
57-
"Mcp\\Example\\DependenciesStdioExample\\": "examples/06-custom-dependencies-stdio/",
58-
"Mcp\\Example\\ComplexSchemaHttpExample\\": "examples/07-complex-tool-schema-http/",
59-
"Mcp\\Example\\SchemaShowcaseExample\\": "examples/08-schema-showcase-streamable/",
60-
"Mcp\\Example\\HttpTransportExample\\": "examples/10-simple-http-transport/",
61-
"Mcp\\Tests\\": "tests/"
62-
}
63-
},
64-
"config": {
65-
"sort-packages": true
15+
{
16+
"name": "Tobias Nyholm",
17+
"email": "[email protected]"
18+
}
19+
],
20+
"require": {
21+
"php": "^8.1",
22+
"ext-fileinfo": "*",
23+
"opis/json-schema": "^2.4",
24+
"phpdocumentor/reflection-docblock": "^5.6",
25+
"psr/clock": "^1.0",
26+
"psr/container": "^2.0",
27+
"psr/event-dispatcher": "^1.0",
28+
"psr/http-factory": "^1.1",
29+
"psr/http-message": "^2.0",
30+
"psr/log": "^1.0 || ^2.0 || ^3.0",
31+
"symfony/finder": "^6.4 || ^7.3",
32+
"symfony/uid": "^6.4 || ^7.3"
33+
},
34+
"require-dev": {
35+
"php-cs-fixer/shim": "^3.84",
36+
"phpstan/phpstan": "^2.1",
37+
"phpunit/phpunit": "^10.5",
38+
"psr/cache": "^3.0",
39+
"psr/simple-cache": "^3.0",
40+
"symfony/cache": "^6.4 || ^7.3",
41+
"symfony/console": "^6.4 || ^7.3",
42+
"symfony/process": "^6.4 || ^7.3",
43+
"nyholm/psr7": "^1.8",
44+
"nyholm/psr7-server": "^1.1",
45+
"laminas/laminas-httphandlerrunner": "^2.12"
46+
},
47+
"autoload": {
48+
"psr-4": {
49+
"Mcp\\": "src/"
50+
}
51+
},
52+
"autoload-dev": {
53+
"psr-4": {
54+
"Mcp\\Example\\StdioCalculatorExample\\": "examples/01-discovery-stdio-calculator/",
55+
"Mcp\\Example\\HttpUserProfileExample\\": "examples/02-discovery-http-userprofile/",
56+
"Mcp\\Example\\ManualStdioExample\\": "examples/03-manual-registration-stdio/",
57+
"Mcp\\Example\\CombinedHttpExample\\": "examples/04-combined-registration-http/",
58+
"Mcp\\Example\\StdioEnvVariables\\": "examples/05-stdio-env-variables/",
59+
"Mcp\\Example\\DependenciesStdioExample\\": "examples/06-custom-dependencies-stdio/",
60+
"Mcp\\Example\\ComplexSchemaHttpExample\\": "examples/07-complex-tool-schema-http/",
61+
"Mcp\\Example\\SchemaShowcaseExample\\": "examples/08-schema-showcase-streamable/",
62+
"Mcp\\Example\\HttpTransportExample\\": "examples/10-simple-http-transport/",
63+
"Mcp\\Example\\CachedDiscoveryExample\\": "examples/10-cached-discovery-stdio/",
64+
"Mcp\\Tests\\": "tests/"
6665
}
67-
}
66+
},
67+
"config": {
68+
"sort-packages": true
69+
}
70+
}
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)