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