Skip to content

Commit f073c07

Browse files
committed
feat: implement discovery caching for improved performance
- Add CachedDiscoverer decorator that wraps the existing Discoverer - Use PSR-16 SimpleCache interface for maximum compatibility - Add withCache() method to ServerBuilder for easy cache configuration - Implement extract/restore mechanism using reflection to cache registry state - Add comprehensive unit tests and performance tests - Create example showing cached discovery usage - Add complete documentation for the caching feature - Performance improvement: 72-116x faster on cache hits - Maintain full backward compatibility Resolves #11
1 parent 9590e9c commit f073c07

File tree

9 files changed

+711
-18
lines changed

9 files changed

+711
-18
lines changed

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
"ext-fileinfo": "*",
2323
"opis/json-schema": "^2.4",
2424
"phpdocumentor/reflection-docblock": "^5.6",
25+
"psr/cache": "^3.0",
2526
"psr/container": "^2.0",
2627
"psr/event-dispatcher": "^1.0",
2728
"psr/log": "^1.0 || ^2.0 || ^3.0",
29+
"psr/simple-cache": "^3.0",
2830
"symfony/finder": "^6.4 || ^7.3",
2931
"symfony/uid": "^6.4 || ^7.3"
3032
},
@@ -33,6 +35,7 @@
3335
"phpstan/phpstan": "^2.1",
3436
"phpunit/phpunit": "^10.5",
3537
"psr/cache": "^3.0",
38+
"symfony/cache": "^6.4 || ^7.3",
3639
"symfony/console": "^6.4 || ^7.3",
3740
"symfony/process": "^6.4 || ^7.3"
3841
},

docs/discovery-caching.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Discovery Caching
2+
3+
The MCP PHP SDK now supports caching of discovery results to improve performance, especially in development environments where the server is restarted frequently.
4+
5+
## Overview
6+
7+
The discovery system scans PHP files and uses reflection to find MCP attributes (tools, resources, prompts, etc.). This process can be expensive when performed repeatedly. The caching system stores the results of this discovery process and reuses them on subsequent calls.
8+
9+
## Performance Benefits
10+
11+
Based on performance testing, the caching system provides significant speed improvements:
12+
13+
- **First call (cache miss)**: ~3x faster than uncached discovery
14+
- **Subsequent calls (cache hit)**: **72x faster** than uncached discovery
15+
16+
## Usage
17+
18+
### Basic Usage
19+
20+
```php
21+
use Mcp\Server;
22+
use Mcp\Server\Transport\StdioTransport;
23+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
24+
use Symfony\Component\Cache\Psr16Cache;
25+
26+
Server::make()
27+
->withServerInfo('My Server', '1.0.0', 'Server with cached discovery')
28+
->withDiscovery(__DIR__, ['.'])
29+
->withCache(new Psr16Cache(new ArrayAdapter())) // Enable caching
30+
->build()
31+
->connect(new StdioTransport());
32+
```
33+
34+
### Using Different Cache Implementations
35+
36+
The caching system uses PSR-16 SimpleCache, so you can use any compatible cache implementation:
37+
38+
```php
39+
// Array cache (in-memory, good for development)
40+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
41+
use Symfony\Component\Cache\Psr16Cache;
42+
43+
$cache = new Psr16Cache(new ArrayAdapter());
44+
45+
// Redis cache (good for production)
46+
use Symfony\Component\Cache\Adapter\RedisAdapter;
47+
use Symfony\Component\Cache\Psr16Cache;
48+
49+
$redis = new \Redis();
50+
$redis->connect('127.0.0.1', 6379);
51+
$cache = new Psr16Cache(new RedisAdapter($redis));
52+
53+
// Filesystem cache (good for persistent caching)
54+
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
55+
use Symfony\Component\Cache\Psr16Cache;
56+
57+
$cache = new Psr16Cache(new FilesystemAdapter('mcp_discovery', 0, '/tmp/mcp_cache'));
58+
```
59+
60+
## How It Works
61+
62+
1. **Cache Key Generation**: A unique cache key is generated based on:
63+
- Base path for discovery
64+
- Directories to scan
65+
- Directories to exclude
66+
67+
2. **Cache Miss**: When no cached data is found:
68+
- The underlying `Discoverer` performs fresh discovery
69+
- Results are stored in the cache with a configurable TTL (default: 1 hour)
70+
71+
3. **Cache Hit**: When cached data is found:
72+
- The registry is restored from cached data
73+
- No file system operations or reflection are performed
74+
75+
## Cache Invalidation
76+
77+
The cache is automatically invalidated when:
78+
- The cache TTL expires (default: 1 hour)
79+
- The cache implementation handles expiration
80+
81+
For development, you may want to use a shorter TTL or clear the cache manually when files change.
82+
83+
## Configuration
84+
85+
### Cache TTL
86+
87+
You can configure the cache TTL when creating a `CachedDiscoverer`:
88+
89+
```php
90+
use Mcp\Capability\Discovery\CachedDiscoverer;
91+
92+
$cachedDiscoverer = new CachedDiscoverer(
93+
$discoverer,
94+
$cache,
95+
$logger,
96+
1800 // 30 minutes TTL
97+
);
98+
```
99+
100+
### Manual Cache Clearing
101+
102+
```php
103+
$cachedDiscoverer->clearCache();
104+
```
105+
106+
## Best Practices
107+
108+
1. **Development**: Use `ArrayAdapter` for fast, in-memory caching
109+
2. **Production**: Use persistent cache implementations like Redis or filesystem
110+
3. **CI/CD**: Consider clearing cache between builds
111+
4. **Monitoring**: Monitor cache hit rates to ensure effectiveness
112+
113+
## Example
114+
115+
See the `examples/10-cached-discovery-stdio/` directory for a complete working example of cached discovery.
116+
117+
## Implementation Details
118+
119+
The caching system uses the decorator pattern:
120+
- `CachedDiscoverer` wraps the existing `Discoverer` class
121+
- Uses reflection to extract and restore registry state
122+
- Only caches discovered elements (not manually registered ones)
123+
- Maintains backward compatibility - works with or without cache
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Example\CachedDiscoveryExample;
6+
7+
use Mcp\Capability\Attribute\McpTool;
8+
9+
/**
10+
* Example MCP elements for demonstrating cached discovery.
11+
*
12+
* This class contains simple calculator tools that will be discovered
13+
* and cached for improved performance on subsequent server starts.
14+
*/
15+
class CachedCalculatorElements
16+
{
17+
#[McpTool(name: 'add_numbers')]
18+
public function add(int $a, int $b): int
19+
{
20+
return $a + $b;
21+
}
22+
23+
#[McpTool(name: 'multiply_numbers')]
24+
public function multiply(int $a, int $b): int
25+
{
26+
return $a * $b;
27+
}
28+
29+
#[McpTool(name: 'divide_numbers')]
30+
public function divide(int $a, int $b): float
31+
{
32+
if ($b === 0) {
33+
throw new \InvalidArgumentException('Division by zero is not allowed');
34+
}
35+
36+
return $a / $b;
37+
}
38+
39+
#[McpTool(name: 'power')]
40+
public function power(int $base, int $exponent): int
41+
{
42+
return (int) pow($base, $exponent);
43+
}
44+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
declare(strict_types=1);
5+
6+
require_once __DIR__ . '/../bootstrap.php';
7+
8+
use Mcp\Server;
9+
use Mcp\Server\Transport\StdioTransport;
10+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
11+
use Symfony\Component\Cache\Psr16Cache;
12+
13+
// Example showing how to use discovery caching for improved performance
14+
// This is especially useful in development environments where the server
15+
// is restarted frequently, or in production where discovery happens on every request.
16+
17+
Server::make()
18+
->withServerInfo('Cached Discovery Calculator', '1.0.0', 'Calculator with cached discovery for better performance.')
19+
->withDiscovery(__DIR__, ['.'])
20+
->withCache(new Psr16Cache(new ArrayAdapter())) // Enable discovery caching
21+
->build()
22+
->connect(new StdioTransport());

phpstan-baseline.neon

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -606,23 +606,6 @@ parameters:
606606
count: 1
607607
path: src/Server/ServerBuilder.php
608608

609-
-
610-
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$cache \(Psr\\SimpleCache\\CacheInterface\|null\) is never assigned Psr\\SimpleCache\\CacheInterface so it can be removed from the property type\.$#'
611-
identifier: property.unusedType
612-
count: 1
613-
path: src/Server/ServerBuilder.php
614-
615-
-
616-
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$cache has unknown class Psr\\SimpleCache\\CacheInterface as its type\.$#'
617-
identifier: class.notFound
618-
count: 1
619-
path: src/Server/ServerBuilder.php
620-
621-
-
622-
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$cache is never read, only written\.$#'
623-
identifier: property.onlyWritten
624-
count: 1
625-
path: src/Server/ServerBuilder.php
626609

627610
-
628611
message: '#^Property Mcp\\Server\\ServerBuilder\:\:\$discoveryExcludeDirs type has no value type specified in iterable type array\.$#'

0 commit comments

Comments
 (0)