Skip to content

Laravel MCP SDK - Caching Best Practices

Comprehensive guide to implementing intelligent caching strategies for MCP tools, resources, and prompts in Laravel applications.

Overview

Effective caching is crucial for MCP applications because:

  • Tool Execution: Many tools perform expensive operations (database queries, API calls, file processing)
  • Resource Access: Resources may involve file I/O or network requests
  • OpenAI Integration: AI model calls are costly and have rate limits
  • Conversation Context: Chat history and context need efficient storage

Tool Result Caching

Smart Cache Keys and TTL

php
<?php

namespace App\Mcp\Tools;

use MCP\Laravel\Laravel\LaravelTool;
use Illuminate\Support\Facades\Cache;

class SmartCachedTool extends LaravelTool
{
    public function name(): string
    {
        return 'smart_cached_tool';
    }

    public function description(): string
    {
        return 'Tool with intelligent caching based on data volatility';
    }

    public function handle(array $params): array
    {
        $cacheStrategy = $this->determineCacheStrategy($params);
        $cacheKey = $this->buildSmartCacheKey($params);

        if (!$this->shouldBypassCache($params)) {
            $cached = Cache::tags($cacheStrategy['tags'])
                ->get($cacheKey);

            if ($cached !== null) {
                return $this->enrichCachedResult($cached);
            }
        }

        // Execute tool logic
        $result = $this->executeToolLogic($params);

        // Cache with intelligent TTL
        Cache::tags($cacheStrategy['tags'])
            ->put($cacheKey, $result, $cacheStrategy['ttl']);

        return $this->enrichFreshResult($result);
    }

    private function determineCacheStrategy(array $params): array
    {
        // Dynamic strategy based on parameters
        if ($this->isRealTimeData($params)) {
            return [
                'ttl' => 60,    // 1 minute for real-time data
                'tags' => ['realtime', $this->name()],
            ];
        }

        if ($this->isHistoricalData($params)) {
            return [
                'ttl' => 86400, // 24 hours for historical data
                'tags' => ['historical', $this->name()],
            ];
        }

        if ($this->isUserSpecificData($params)) {
            return [
                'ttl' => 3600,  // 1 hour for user data
                'tags' => ['user_data', $this->name(), "user_{$this->getUserId()}"],
            ];
        }

        return [
            'ttl' => 1800,      // 30 minutes default
            'tags' => ['default', $this->name()],
        ];
    }

    private function buildSmartCacheKey(array $params): string
    {
        $keyParts = [$this->name()];

        // Add time-based component for time-sensitive data
        if ($this->isTimeSensitive($params)) {
            $keyParts[] = 'time:' . floor(time() / 300); // 5-minute buckets
        }

        // Add user context if user-specific
        if ($this->isUserSpecificData($params)) {
            $keyParts[] = 'user:' . $this->getUserId();
        }

        // Add parameter hash
        $keyParts[] = 'params:' . $this->hashParameters($params);

        return implode(':', $keyParts);
    }

    private function hashParameters(array $params): string
    {
        // Remove cache-control parameters before hashing
        $cacheParams = array_diff_key($params, [
            'force_refresh' => true,
            'cache_ttl' => true,
            '_cache_control' => true,
        ]);

        return md5(json_encode($cacheParams, JSON_SORT_KEYS));
    }

    private function shouldBypassCache(array $params): bool
    {
        return $params['force_refresh'] ?? false;
    }

    private function enrichCachedResult(array $cached): array
    {
        return array_merge($cached, [
            '_cache_hit' => true,
            '_cached_at' => $cached['_cached_at'] ?? now(),
            '_cache_age' => now()->diffInSeconds($cached['_cached_at'] ?? now()),
        ]);
    }

    private function enrichFreshResult(array $result): array
    {
        return array_merge($result, [
            '_cache_hit' => false,
            '_cached_at' => now(),
            '_cache_age' => 0,
        ]);
    }

    // Helper methods for cache strategy determination
    private function isRealTimeData(array $params): bool
    {
        return in_array($params['data_type'] ?? '', ['stock_price', 'sensor_data', 'live_metrics']);
    }

    private function isHistoricalData(array $params): bool
    {
        return isset($params['date']) &&
               now()->diffInDays($params['date']) > 1;
    }

    private function isUserSpecificData(array $params): bool
    {
        return isset($params['user_id']) ||
               str_contains($params['query'] ?? '', 'my ') ||
               str_contains($params['query'] ?? '', 'user');
    }

    private function isTimeSensitive(array $params): bool
    {
        $timeSensitiveTypes = ['weather', 'traffic', 'availability', 'status'];
        return in_array($params['type'] ?? '', $timeSensitiveTypes);
    }
}

Database Query Tool with Advanced Caching

php
<?php

namespace App\Mcp\Tools;

use MCP\Laravel\Laravel\LaravelTool;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

class CachedDatabaseTool extends LaravelTool
{
    public function name(): string
    {
        return 'cached_database_query';
    }

    public function description(): string
    {
        return 'Execute database queries with intelligent caching and invalidation';
    }

    protected function properties(): array
    {
        return [
            'query' => ['type' => 'string'],
            'bindings' => ['type' => 'array', 'default' => []],
            'cache_strategy' => [
                'type' => 'string',
                'enum' => ['auto', 'aggressive', 'conservative', 'none'],
                'default' => 'auto'
            ],
        ];
    }

    public function handle(array $params): array
    {
        $query = $params['query'];
        $bindings = $params['bindings'] ?? [];
        $strategy = $params['cache_strategy'] ?? 'auto';

        // Analyze query for cache strategy
        $cacheInfo = $this->analyzeQueryForCaching($query, $strategy);

        if ($cacheInfo['cacheable']) {
            $cacheKey = $this->buildQueryCacheKey($query, $bindings);

            // Check cache first
            $cached = Cache::tags($cacheInfo['tags'])
                ->get($cacheKey);

            if ($cached !== null) {
                $this->log('info', 'Database query served from cache', [
                    'cache_key' => $cacheKey,
                    'query_hash' => md5($query),
                ]);

                return $this->enrichCachedResult($cached, $cacheInfo);
            }
        }

        // Execute query
        $startTime = microtime(true);
        $results = DB::select($query, $bindings);
        $executionTime = microtime(true) - $startTime;

        $result = [
            'data' => $results,
            'execution_time' => $executionTime,
            'row_count' => count($results),
            'query_hash' => md5($query),
        ];

        // Cache if appropriate
        if ($cacheInfo['cacheable'] && $this->shouldCacheResult($result, $cacheInfo)) {
            Cache::tags($cacheInfo['tags'])
                ->put($cacheKey, $result, $cacheInfo['ttl']);

            $this->log('info', 'Database query result cached', [
                'cache_key' => $cacheKey,
                'ttl' => $cacheInfo['ttl'],
                'row_count' => count($results),
            ]);
        }

        return $this->textContent(
            "Query executed in {$executionTime}s. Rows: " . count($results) . "\n\n" .
            json_encode($results, JSON_PRETTY_PRINT)
        );
    }

    private function analyzeQueryForCaching(string $query, string $strategy): array
    {
        $queryUpper = strtoupper(trim($query));

        // Determine base cacheability
        $isSelect = str_starts_with($queryUpper, 'SELECT');
        $hasNow = str_contains($queryUpper, 'NOW()') || str_contains($queryUpper, 'CURRENT_TIMESTAMP');
        $hasRandom = str_contains($queryUpper, 'RAND()') || str_contains($queryUpper, 'RANDOM()');

        $cacheable = $isSelect && !$hasNow && !$hasRandom;

        // Determine tables involved
        $tables = $this->extractTablesFromQuery($query);

        // Determine TTL based on query characteristics and strategy
        $ttl = $this->calculateTtl($query, $tables, $strategy);

        // Build cache tags
        $tags = array_merge(['database_queries'], $tables);

        return [
            'cacheable' => $cacheable,
            'ttl' => $ttl,
            'tags' => $tags,
            'tables' => $tables,
            'strategy' => $strategy,
        ];
    }

    private function extractTablesFromQuery(string $query): array
    {
        // Simple regex to extract table names (can be enhanced)
        preg_match_all('/FROM\s+`?(\w+)`?/i', $query, $fromMatches);
        preg_match_all('/JOIN\s+`?(\w+)`?/i', $query, $joinMatches);

        $tables = array_merge(
            $fromMatches[1] ?? [],
            $joinMatches[1] ?? []
        );

        return array_unique($tables);
    }

    private function calculateTtl(string $query, array $tables, string $strategy): int
    {
        $baseTtl = match ($strategy) {
            'aggressive' => 3600,   // 1 hour
            'conservative' => 300,  // 5 minutes
            'none' => 0,           // No caching
            'auto' => 1800,        // 30 minutes
        };

        if ($strategy === 'none') {
            return 0;
        }

        // Adjust based on table characteristics
        foreach ($tables as $table) {
            if (in_array($table, ['sessions', 'cache', 'job_batches'])) {
                $baseTtl = min($baseTtl, 60); // 1 minute for volatile tables
            } elseif (in_array($table, ['users', 'posts', 'products'])) {
                $baseTtl = min($baseTtl, 1800); // 30 minutes for semi-static data
            } elseif (in_array($table, ['settings', 'configurations'])) {
                $baseTtl = max($baseTtl, 3600); // 1 hour for static data
            }
        }

        // Adjust based on query complexity
        if (str_contains(strtoupper($query), 'COUNT(')) {
            $baseTtl *= 2; // Count queries can be cached longer
        }

        return $baseTtl;
    }

    private function buildQueryCacheKey(string $query, array $bindings): string
    {
        return 'db_query:' . md5($query . serialize($bindings));
    }

    private function shouldCacheResult(array $result, array $cacheInfo): bool
    {
        // Don't cache empty results or very large results
        $rowCount = $result['row_count'];

        if ($rowCount === 0) {
            return false;
        }

        if ($rowCount > 1000) {
            return false; // Too large to cache effectively
        }

        // Don't cache slow queries that might be one-off
        if ($result['execution_time'] > 5.0) {
            return false;
        }

        return true;
    }
}

Resource Caching

File-Based Resource with Intelligent Caching

php
<?php

namespace App\Mcp\Resources;

use MCP\Laravel\Laravel\LaravelResource;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;

class CachedFileResource extends LaravelResource
{
    public function uri(): string
    {
        return 'cached_file://{path}';
    }

    public function description(): string
    {
        return 'File resource with intelligent caching based on file metadata';
    }

    public function read(string $uri): array
    {
        $variables = $this->extractUriVariables($uri);
        $filePath = $variables['path'];

        // Get file metadata for cache decisions
        $fileInfo = $this->getFileInfo($filePath);
        $cacheStrategy = $this->determineCacheStrategy($fileInfo);

        $cacheKey = $this->buildFileCacheKey($filePath, $fileInfo);

        // Check cache if file hasn't changed
        if ($cacheStrategy['use_cache']) {
            $cached = Cache::get($cacheKey);
            if ($cached && $cached['file_modified'] === $fileInfo['modified']) {
                return $this->enrichCachedResult($cached);
            }
        }

        // Read file content
        $content = $this->readFileContent($filePath);

        // Process content based on file type
        $processedContent = $this->processFileContent($content, $fileInfo);

        $result = [
            'contents' => [[
                'uri' => $uri,
                'mimeType' => $fileInfo['mime_type'],
                'text' => $processedContent,
            ]],
            'file_size' => $fileInfo['size'],
            'file_modified' => $fileInfo['modified'],
            'processed_at' => now(),
        ];

        // Cache the result
        if ($cacheStrategy['cache_result']) {
            Cache::put($cacheKey, $result, $cacheStrategy['ttl']);
        }

        return $result;
    }

    private function getFileInfo(string $filePath): array
    {
        if (!Storage::exists($filePath)) {
            throw new \Exception("File not found: {$filePath}");
        }

        $size = Storage::size($filePath);
        $modified = Storage::lastModified($filePath);
        $mimeType = Storage::mimeType($filePath);

        return [
            'path' => $filePath,
            'size' => $size,
            'modified' => $modified,
            'mime_type' => $mimeType,
            'extension' => pathinfo($filePath, PATHINFO_EXTENSION),
        ];
    }

    private function determineCacheStrategy(array $fileInfo): array
    {
        $size = $fileInfo['size'];
        $extension = $fileInfo['extension'];
        $age = time() - $fileInfo['modified'];

        // Very large files - don't cache content
        if ($size > 10 * 1024 * 1024) { // 10MB
            return [
                'use_cache' => false,
                'cache_result' => false,
                'ttl' => 0,
                'reason' => 'file_too_large',
            ];
        }

        // Recently modified files - short cache
        if ($age < 3600) { // Modified in last hour
            return [
                'use_cache' => true,
                'cache_result' => true,
                'ttl' => 300, // 5 minutes
                'reason' => 'recently_modified',
            ];
        }

        // Static file types - long cache
        if (in_array($extension, ['pdf', 'jpg', 'png', 'mp4', 'mp3'])) {
            return [
                'use_cache' => true,
                'cache_result' => true,
                'ttl' => 86400, // 24 hours
                'reason' => 'static_content',
            ];
        }

        // Configuration files - medium cache
        if (in_array($extension, ['json', 'xml', 'yaml', 'ini'])) {
            return [
                'use_cache' => true,
                'cache_result' => true,
                'ttl' => 3600, // 1 hour
                'reason' => 'config_file',
            ];
        }

        // Default strategy
        return [
            'use_cache' => true,
            'cache_result' => true,
            'ttl' => 1800, // 30 minutes
            'reason' => 'default',
        ];
    }

    private function buildFileCacheKey(string $filePath, array $fileInfo): string
    {
        return 'file_resource:' . md5($filePath) . ':' . $fileInfo['modified'];
    }

    private function readFileContent(string $filePath): string
    {
        return Storage::get($filePath);
    }

    private function processFileContent(string $content, array $fileInfo): string
    {
        $extension = $fileInfo['extension'];

        // Process based on file type
        return match ($extension) {
            'json' => $this->formatJson($content),
            'xml' => $this->formatXml($content),
            'csv' => $this->formatCsv($content),
            'md' => $this->formatMarkdown($content),
            default => $content,
        };
    }

    private function formatJson(string $content): string
    {
        $decoded = json_decode($content, true);
        return $decoded ? json_encode($decoded, JSON_PRETTY_PRINT) : $content;
    }

    private function formatXml(string $content): string
    {
        $dom = new \DOMDocument();
        $dom->preserveWhiteSpace = false;
        $dom->formatOutput = true;

        if (@$dom->loadXML($content)) {
            return $dom->saveXML();
        }

        return $content;
    }

    private function formatCsv(string $content): string
    {
        $lines = array_slice(explode("\n", $content), 0, 10); // First 10 lines
        return "CSV Preview (first 10 lines):\n" . implode("\n", $lines);
    }

    private function formatMarkdown(string $content): string
    {
        // Could integrate markdown parser here
        return $content;
    }
}

OpenAI Response Caching

Conversation and Response Caching

php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use OpenAI\Laravel\Facades\OpenAI;

class OpenAICacheManager
{
    /**
     * Cache OpenAI responses with intelligent strategies
     */
    public function getCachedResponse(array $messages, array $options = []): ?array
    {
        if (!$this->shouldCacheRequest($messages, $options)) {
            return null;
        }

        $cacheKey = $this->buildResponseCacheKey($messages, $options);
        $cached = Cache::get($cacheKey);

        if ($cached) {
            $this->logCacheHit($cacheKey, $messages);
            return $this->enrichCachedResponse($cached);
        }

        return null;
    }

    /**
     * Store OpenAI response in cache
     */
    public function cacheResponse(array $messages, array $options, array $response): void
    {
        if (!$this->shouldCacheResponse($messages, $options, $response)) {
            return;
        }

        $cacheKey = $this->buildResponseCacheKey($messages, $options);
        $ttl = $this->calculateResponseTtl($messages, $response);

        $cacheData = [
            'response' => $response,
            'cached_at' => now(),
            'messages_hash' => $this->hashMessages($messages),
            'options' => $options,
        ];

        Cache::put($cacheKey, $cacheData, $ttl);

        $this->logCacheStore($cacheKey, $ttl, $response);
    }

    /**
     * Cache conversation context for faster follow-ups
     */
    public function cacheConversationContext(string $conversationId, array $context): void
    {
        $cacheKey = "conversation_context:{$conversationId}";
        Cache::put($cacheKey, $context, 3600); // 1 hour
    }

    /**
     * Get cached conversation context
     */
    public function getConversationContext(string $conversationId): ?array
    {
        $cacheKey = "conversation_context:{$conversationId}";
        return Cache::get($cacheKey);
    }

    /**
     * Cache tool schemas for faster tool calling
     */
    public function cacheToolSchemas(array $tools): void
    {
        $cacheKey = 'openai_tool_schemas:' . md5(json_encode($tools));
        Cache::put($cacheKey, $tools, 86400); // 24 hours
    }

    /**
     * Get cached tool schemas
     */
    public function getToolSchemas(array $toolNames): ?array
    {
        $cacheKey = 'openai_tool_schemas:' . md5(json_encode($toolNames));
        return Cache::get($cacheKey);
    }

    private function shouldCacheRequest(array $messages, array $options): bool
    {
        // Don't cache if explicitly disabled
        if ($options['disable_cache'] ?? false) {
            return false;
        }

        // Don't cache requests with high temperature (creative responses)
        if (($options['temperature'] ?? 0.7) > 0.8) {
            return false;
        }

        // Don't cache if tools are involved (may have side effects)
        if (!empty($options['tools'])) {
            return false;
        }

        // Don't cache very long conversations
        if (count($messages) > 20) {
            return false;
        }

        return true;
    }

    private function shouldCacheResponse(array $messages, array $options, array $response): bool
    {
        // Don't cache error responses
        if (isset($response['error'])) {
            return false;
        }

        // Don't cache responses with tool calls
        $choice = $response['choices'][0] ?? [];
        if (!empty($choice['message']['tool_calls'])) {
            return false;
        }

        // Don't cache very short responses (likely errors or incomplete)
        $content = $choice['message']['content'] ?? '';
        if (strlen($content) < 10) {
            return false;
        }

        return true;
    }

    private function buildResponseCacheKey(array $messages, array $options): string
    {
        $keyParts = [
            'openai_response',
            $this->hashMessages($messages),
            md5(json_encode($this->getCacheableOptions($options))),
        ];

        return implode(':', $keyParts);
    }

    private function hashMessages(array $messages): string
    {
        // Create hash of message content only (ignore metadata)
        $messageContent = array_map(function ($message) {
            return [
                'role' => $message['role'],
                'content' => $message['content'],
            ];
        }, $messages);

        return md5(json_encode($messageContent, JSON_SORT_KEYS));
    }

    private function getCacheableOptions(array $options): array
    {
        // Only include options that affect response content
        return array_intersect_key($options, [
            'model' => true,
            'temperature' => true,
            'max_tokens' => true,
            'top_p' => true,
            'frequency_penalty' => true,
            'presence_penalty' => true,
        ]);
    }

    private function calculateResponseTtl(array $messages, array $response): int
    {
        $lastMessage = end($messages);
        $content = $response['choices'][0]['message']['content'] ?? '';

        // Shorter TTL for questions about current events
        if ($this->isCurrentEventQuery($lastMessage['content'] ?? '')) {
            return 300; // 5 minutes
        }

        // Longer TTL for factual/educational content
        if ($this->isFactualContent($content)) {
            return 3600; // 1 hour
        }

        // Medium TTL for general responses
        return 1800; // 30 minutes
    }

    private function isCurrentEventQuery(string $content): bool
    {
        $currentEventKeywords = [
            'today', 'now', 'current', 'latest', 'recent',
            'weather', 'stock price', 'news'
        ];

        foreach ($currentEventKeywords as $keyword) {
            if (str_contains(strtolower($content), $keyword)) {
                return true;
            }
        }

        return false;
    }

    private function isFactualContent(string $content): bool
    {
        // Simple heuristics for factual content
        $factualIndicators = [
            'definition', 'explanation', 'formula', 'algorithm',
            'history', 'theory', 'concept'
        ];

        foreach ($factualIndicators as $indicator) {
            if (str_contains(strtolower($content), $indicator)) {
                return true;
            }
        }

        return false;
    }

    private function enrichCachedResponse(array $cached): array
    {
        return array_merge($cached['response'], [
            '_cache_hit' => true,
            '_cached_at' => $cached['cached_at'],
            '_cache_age' => now()->diffInSeconds($cached['cached_at']),
        ]);
    }

    private function logCacheHit(string $cacheKey, array $messages): void
    {
        Log::info('OpenAI response cache hit', [
            'cache_key' => substr($cacheKey, 0, 32) . '...',
            'message_count' => count($messages),
        ]);
    }

    private function logCacheStore(string $cacheKey, int $ttl, array $response): void
    {
        $usage = $response['usage'] ?? [];

        Log::info('OpenAI response cached', [
            'cache_key' => substr($cacheKey, 0, 32) . '...',
            'ttl' => $ttl,
            'prompt_tokens' => $usage['prompt_tokens'] ?? 0,
            'completion_tokens' => $usage['completion_tokens'] ?? 0,
        ]);
    }
}

Cache Invalidation Strategies

Event-Driven Cache Invalidation

php
<?php

namespace App\Listeners;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class CacheInvalidationListener
{
    /**
     * Handle database model updates
     */
    public function handleModelUpdated($event): void
    {
        $model = $event->model;
        $tableName = $model->getTable();

        // Invalidate related database query caches
        Cache::tags(['database_queries', $tableName])->flush();

        // Invalidate user-specific caches if user model
        if ($model instanceof \App\Models\User) {
            Cache::tags(["user_{$model->id}"])->flush();
        }

        Log::info('Cache invalidated for model update', [
            'model' => get_class($model),
            'id' => $model->getKey(),
            'table' => $tableName,
        ]);
    }

    /**
     * Handle file system changes
     */
    public function handleFileChanged(string $filePath): void
    {
        // Invalidate file resource caches
        $pattern = 'file_resource:' . md5($filePath) . ':*';
        $this->invalidateByPattern($pattern);

        // If it's a configuration file, invalidate config caches
        if (str_contains($filePath, 'config/')) {
            Cache::tags(['config_files'])->flush();
        }

        Log::info('Cache invalidated for file change', [
            'file_path' => $filePath,
        ]);
    }

    /**
     * Handle tool registration changes
     */
    public function handleToolsChanged(): void
    {
        // Invalidate tool schema caches
        Cache::tags(['mcp_tools', 'openai_tool_schemas'])->flush();

        Log::info('Cache invalidated for tool changes');
    }

    private function invalidateByPattern(string $pattern): void
    {
        // Note: This requires Redis cache driver for pattern matching
        if (config('cache.default') === 'redis') {
            $redis = Cache::getRedis();
            $keys = $redis->keys($pattern);
            if (!empty($keys)) {
                $redis->del($keys);
            }
        }
    }
}

Scheduled Cache Maintenance

php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;

class CacheMaintenanceCommand extends Command
{
    protected $signature = 'mcp:cache-maintenance
                           {--clean-expired : Remove expired cache entries}
                           {--optimize : Optimize cache performance}
                           {--stats : Show cache statistics}';

    protected $description = 'Perform MCP cache maintenance tasks';

    public function handle(): int
    {
        if ($this->option('clean-expired')) {
            $this->cleanExpiredEntries();
        }

        if ($this->option('optimize')) {
            $this->optimizeCache();
        }

        if ($this->option('stats')) {
            $this->showCacheStats();
        }

        if (!$this->hasOption()) {
            $this->performFullMaintenance();
        }

        return 0;
    }

    private function cleanExpiredEntries(): void
    {
        $this->info('Cleaning expired cache entries...');

        // Clean up old conversation histories
        $this->cleanOldConversations();

        // Clean up orphaned tool result caches
        $this->cleanOrphanedToolCaches();

        $this->info('✓ Expired entries cleaned');
    }

    private function optimizeCache(): void
    {
        $this->info('Optimizing cache performance...');

        // Warm frequently used caches
        $this->warmFrequentCaches();

        // Compress large cache entries
        $this->compressLargeCaches();

        $this->info('✓ Cache optimized');
    }

    private function showCacheStats(): void
    {
        $stats = $this->gatherCacheStats();

        $this->table(['Metric', 'Value'], [
            ['Total Entries', number_format($stats['total_entries'])],
            ['Memory Usage', $this->formatBytes($stats['memory_usage'])],
            ['Hit Rate', $stats['hit_rate'] . '%'],
            ['Tool Cache Entries', number_format($stats['tool_entries'])],
            ['OpenAI Cache Entries', number_format($stats['openai_entries'])],
            ['File Cache Entries', number_format($stats['file_entries'])],
        ]);
    }

    private function performFullMaintenance(): void
    {
        $this->cleanExpiredEntries();
        $this->optimizeCache();
        $this->showCacheStats();
    }

    private function cleanOldConversations(): void
    {
        // Remove conversations older than 7 days
        $pattern = 'conversation:*';
        $cutoff = now()->subDays(7);

        // Implementation depends on cache driver
        $this->info('Cleaned old conversations');
    }

    private function warmFrequentCaches(): void
    {
        // Warm commonly used tool results
        $commonQueries = [
            ['tool' => 'system_status', 'params' => []],
            ['tool' => 'user_count', 'params' => []],
        ];

        foreach ($commonQueries as $query) {
            // Execute to warm cache
            $this->info("Warming cache for {$query['tool']}");
        }
    }

    private function gatherCacheStats(): array
    {
        // Implementation depends on cache driver
        return [
            'total_entries' => 1000,
            'memory_usage' => 1024 * 1024 * 50, // 50MB
            'hit_rate' => 85.5,
            'tool_entries' => 250,
            'openai_entries' => 150,
            'file_entries' => 75,
        ];
    }

    private function formatBytes(int $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        $bytes = max($bytes, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count($units) - 1);
        $bytes /= pow(1024, $pow);
        return round($bytes, 2) . ' ' . $units[$pow];
    }

    private function hasOption(): bool
    {
        return $this->option('clean-expired') ||
               $this->option('optimize') ||
               $this->option('stats');
    }
}

Performance Monitoring

Cache Performance Metrics

php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class CacheMetricsCollector
{
    private array $metrics = [];

    /**
     * Record cache hit
     */
    public function recordHit(string $key, string $category = 'general'): void
    {
        $this->incrementMetric("cache.{$category}.hits");
        $this->incrementMetric('cache.total.hits');
    }

    /**
     * Record cache miss
     */
    public function recordMiss(string $key, string $category = 'general'): void
    {
        $this->incrementMetric("cache.{$category}.misses");
        $this->incrementMetric('cache.total.misses');
    }

    /**
     * Record cache write
     */
    public function recordWrite(string $key, int $size, int $ttl, string $category = 'general'): void
    {
        $this->incrementMetric("cache.{$category}.writes");
        $this->incrementMetric('cache.total.writes');
        $this->recordGauge("cache.{$category}.avg_size", $size);
        $this->recordGauge("cache.{$category}.avg_ttl", $ttl);
    }

    /**
     * Get cache performance report
     */
    public function getPerformanceReport(): array
    {
        $report = [
            'hit_rates' => [],
            'write_volumes' => [],
            'performance_issues' => [],
            'recommendations' => [],
        ];

        foreach (['tools', 'openai', 'files', 'database'] as $category) {
            $hits = $this->getMetric("cache.{$category}.hits", 0);
            $misses = $this->getMetric("cache.{$category}.misses", 0);
            $total = $hits + $misses;

            if ($total > 0) {
                $hitRate = ($hits / $total) * 100;
                $report['hit_rates'][$category] = round($hitRate, 2);

                // Identify performance issues
                if ($hitRate < 50) {
                    $report['performance_issues'][] = "Low hit rate for {$category}: {$hitRate}%";
                    $report['recommendations'][] = "Consider increasing TTL for {$category} cache";
                }
            }

            $writes = $this->getMetric("cache.{$category}.writes", 0);
            $report['write_volumes'][$category] = $writes;

            // Check for excessive writes
            if ($writes > $hits * 2) {
                $report['performance_issues'][] = "High write-to-hit ratio for {$category}";
                $report['recommendations'][] = "Review {$category} caching strategy";
            }
        }

        return $report;
    }

    /**
     * Monitor cache size and suggest cleanup
     */
    public function monitorCacheSize(): array
    {
        $sizeReport = [
            'total_size' => $this->getTotalCacheSize(),
            'category_sizes' => $this->getCategorySizes(),
            'cleanup_suggestions' => [],
        ];

        // Suggest cleanup if total size is too large
        if ($sizeReport['total_size'] > 100 * 1024 * 1024) { // 100MB
            $sizeReport['cleanup_suggestions'][] = 'Total cache size exceeds 100MB - consider cleanup';
        }

        // Check individual categories
        foreach ($sizeReport['category_sizes'] as $category => $size) {
            if ($size > 20 * 1024 * 1024) { // 20MB
                $sizeReport['cleanup_suggestions'][] = "Category {$category} is using {$this->formatBytes($size)}";
            }
        }

        return $sizeReport;
    }

    private function incrementMetric(string $key): void
    {
        $this->metrics[$key] = ($this->metrics[$key] ?? 0) + 1;
    }

    private function recordGauge(string $key, float $value): void
    {
        if (!isset($this->metrics[$key . '_values'])) {
            $this->metrics[$key . '_values'] = [];
        }
        $this->metrics[$key . '_values'][] = $value;
    }

    private function getMetric(string $key, $default = null)
    {
        return $this->metrics[$key] ?? $default;
    }

    private function getTotalCacheSize(): int
    {
        // Implementation depends on cache driver
        return 50 * 1024 * 1024; // Placeholder: 50MB
    }

    private function getCategorySizes(): array
    {
        // Implementation depends on cache driver
        return [
            'tools' => 20 * 1024 * 1024,
            'openai' => 15 * 1024 * 1024,
            'files' => 10 * 1024 * 1024,
            'database' => 5 * 1024 * 1024,
        ];
    }

    private function formatBytes(int $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        $bytes = max($bytes, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count($units) - 1);
        $bytes /= pow(1024, $pow);
        return round($bytes, 2) . ' ' . $units[$pow];
    }
}

Testing Cache Implementation

Cache Testing Utilities

php
<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Support\Facades\Cache;

class CacheImplementationTest extends TestCase
{
    public function test_tool_result_caching_works(): void
    {
        $tool = new \App\Mcp\Tools\SmartCachedTool();

        // First call should miss cache
        $result1 = $tool->handle(['test' => 'value']);
        $this->assertFalse($result1['_cache_hit']);

        // Second call should hit cache
        $result2 = $tool->handle(['test' => 'value']);
        $this->assertTrue($result2['_cache_hit']);

        // Different parameters should miss cache
        $result3 = $tool->handle(['test' => 'different']);
        $this->assertFalse($result3['_cache_hit']);
    }

    public function test_cache_invalidation_on_data_change(): void
    {
        // Create initial cached result
        $tool = new \App\Mcp\Tools\CachedDatabaseTool();
        $result1 = $tool->handle([
            'query' => 'SELECT COUNT(*) FROM users',
            'bindings' => []
        ]);

        // Simulate data change
        \App\Models\User::factory()->create();

        // Should invalidate cache and return fresh data
        $result2 = $tool->handle([
            'query' => 'SELECT COUNT(*) FROM users',
            'bindings' => []
        ]);

        $this->assertNotEquals(
            $result1['data'][0]->count,
            $result2['data'][0]->count
        );
    }

    public function test_openai_response_caching(): void
    {
        $cacheManager = app(\App\Services\OpenAICacheManager::class);

        $messages = [
            ['role' => 'user', 'content' => 'What is 2+2?']
        ];

        $options = ['temperature' => 0.1]; // Low temperature for caching

        // Should not have cached response initially
        $cached = $cacheManager->getCachedResponse($messages, $options);
        $this->assertNull($cached);

        // Mock OpenAI response
        $response = [
            'choices' => [
                ['message' => ['content' => '2+2 equals 4']]
            ],
            'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5]
        ];

        // Cache the response
        $cacheManager->cacheResponse($messages, $options, $response);

        // Should now return cached response
        $cached = $cacheManager->getCachedResponse($messages, $options);
        $this->assertNotNull($cached);
        $this->assertTrue($cached['_cache_hit']);
    }

    public function test_cache_performance_monitoring(): void
    {
        $metrics = app(\App\Services\CacheMetricsCollector::class);

        // Record some cache operations
        $metrics->recordHit('test_key', 'tools');
        $metrics->recordMiss('test_key2', 'tools');
        $metrics->recordWrite('test_key3', 1024, 3600, 'tools');

        $report = $metrics->getPerformanceReport();

        $this->assertArrayHasKey('hit_rates', $report);
        $this->assertArrayHasKey('tools', $report['hit_rates']);
        $this->assertEquals(50.0, $report['hit_rates']['tools']); // 1 hit, 1 miss = 50%
    }
}

Best Practices Summary

1. Cache Strategy Selection

  • Real-time data: Short TTL (1-5 minutes)
  • Semi-static data: Medium TTL (30 minutes - 1 hour)
  • Static data: Long TTL (1-24 hours)
  • User-specific data: Tagged for easy invalidation

2. Cache Key Design

  • Include all relevant parameters
  • Use consistent naming conventions
  • Include version/timestamp for time-sensitive data
  • Keep keys reasonably short but descriptive

3. Invalidation Strategies

  • Use cache tags for grouped invalidation
  • Implement event-driven invalidation
  • Monitor cache hit rates
  • Regular maintenance and cleanup

4. Performance Optimization

  • Monitor cache metrics
  • Implement cache warming for common queries
  • Use appropriate cache drivers (Redis for production)
  • Compress large cache entries

5. Error Handling

  • Graceful fallback when cache fails
  • Don't cache error responses
  • Log cache operations for debugging
  • Implement circuit breakers for cache dependencies

See Also

Released under the MIT License.