Skip to main content
Glama

CodeAnalysis MCP Server

by 0xjcf
redisCacheStore.ts13.5 kB
import Redis from "ioredis"; import { createHash } from "crypto"; /** * Configuration options for the Redis Cache Store */ export interface RedisCacheStoreOptions { /** * Redis connection URL (e.g., redis://localhost:6379) */ redisUrl: string; /** * Key prefix for Redis cache keys (default: "mcp:cache:") */ prefix?: string; /** * Default TTL for cache entries in seconds (default: 300) */ defaultTtl?: number; /** * Maximum size for the memory cache (default: 1000) */ memCacheSize?: number; /** * Enable memory caching layer (default: true) */ useMemoryCache?: boolean; } /** * Interface for cached item with metadata */ interface CacheItem<T> { value: T; timestamp: number; expiresAt: number | null; } /** * Redis-backed cache store with optional memory caching layer * * Provides a tiered caching implementation with: * 1. In-memory LRU cache for frequent access * 2. Redis-backed distributed cache for persistence * * Features: * - Automatic cache invalidation based on TTL * - Support for cache namespaces/categories * - Batch operations for efficiency * - Memory cache hit rate tracking */ export class RedisCacheStore { private client: Redis; private prefix: string; private defaultTtl: number; private useMemoryCache: boolean; // Memory cache implementation private memCache: Map<string, CacheItem<any>> = new Map(); private memCacheSize: number; private memCacheStats = { hits: 0, misses: 0, sets: 0, }; /** * Creates a new RedisCacheStore * @param options Configuration options */ constructor(options: RedisCacheStoreOptions) { this.client = new Redis(options.redisUrl); this.prefix = options.prefix || "mcp:cache:"; this.defaultTtl = options.defaultTtl || 300; // 5 minutes default this.memCacheSize = options.memCacheSize || 1000; this.useMemoryCache = options.useMemoryCache !== false; // Set up error handler for Redis client if (typeof this.client.on === "function") { this.client.on("error", (err: Error) => { console.error("Redis cache client error:", err); }); } } /** * Closes the Redis connection */ public async disconnect(): Promise<void> { try { await this.client.quit(); } catch (error) { console.error("Error disconnecting Redis cache client:", error); } } /** * Gets a cache key with namespace * @param key Base cache key * @param namespace Optional namespace (category) * @returns Full Redis key with prefix and namespace */ private getCacheKey(key: string, namespace?: string): string { if (namespace) { return `${this.prefix}${namespace}:${key}`; } return `${this.prefix}${key}`; } /** * Creates a hash for complex keys * @param data Data to hash * @returns SHA-256 hash string */ private createKeyHash(data: any): string { if (typeof data === "string") { return data; } const jsonStr = JSON.stringify(data); return createHash("sha256").update(jsonStr).digest("hex"); } /** * Gets an item from cache * @param key Cache key * @param namespace Optional namespace * @returns Cached value or null if not found/expired */ public async get<T>(key: string, namespace?: string): Promise<T | null> { const cacheKey = this.getCacheKey(this.createKeyHash(key), namespace); // Try memory cache first if enabled if (this.useMemoryCache) { const memItem = this.memCache.get(cacheKey); if (memItem) { // Check if the item is expired if (memItem.expiresAt === null || memItem.expiresAt > Date.now()) { this.memCacheStats.hits++; return memItem.value as T; } else { // Remove expired item from memory cache this.memCache.delete(cacheKey); } } this.memCacheStats.misses++; } // Try Redis cache try { const data = await this.client.get(cacheKey); if (!data) { return null; } const item = JSON.parse(data) as CacheItem<T>; // Update memory cache if enabled if (this.useMemoryCache) { this.setMemoryCache(cacheKey, item); } return item.value; } catch (error) { console.error("Failed to get item from Redis cache:", error); return null; // Fail open for cache issues } } /** * Sets an item in the cache * @param key Cache key * @param value Value to cache * @param ttl TTL in seconds (optional, uses default if not specified) * @param namespace Optional namespace */ public async set<T>( key: string, value: T, ttl?: number, namespace?: string ): Promise<void> { const cacheKey = this.getCacheKey(this.createKeyHash(key), namespace); const ttlValue = ttl ?? this.defaultTtl; const now = Date.now(); const item: CacheItem<T> = { value, timestamp: now, expiresAt: ttlValue > 0 ? now + ttlValue * 1000 : null, }; // Update memory cache if enabled if (this.useMemoryCache) { this.setMemoryCache(cacheKey, item); } // Update Redis cache try { if (ttlValue > 0) { await this.client.set(cacheKey, JSON.stringify(item), "EX", ttlValue); } else { await this.client.set(cacheKey, JSON.stringify(item)); } } catch (error) { console.error("Failed to set item in Redis cache:", error); // Continue even if Redis fails - we still have memory cache } } /** * Adds item to memory cache with LRU eviction if needed * @param key Cache key * @param item Cache item with value and metadata */ private setMemoryCache<T>(key: string, item: CacheItem<T>): void { // Clean up memory cache using LRU when it gets too large if (this.memCache.size > this.memCacheSize) { // Find the oldest key let oldestKey: string | undefined; let oldestTimestamp = Date.now(); for (const [cacheKey, entry] of this.memCache.entries()) { if (entry.timestamp < oldestTimestamp) { oldestTimestamp = entry.timestamp; oldestKey = cacheKey; } } // Remove the oldest entry if (oldestKey) { this.memCache.delete(oldestKey); } } this.memCache.set(key, item); this.memCacheStats.sets++; } /** * Removes an item from the cache * @param key Cache key * @param namespace Optional namespace */ public async delete(key: string, namespace?: string): Promise<void> { const cacheKey = this.getCacheKey(this.createKeyHash(key), namespace); // Remove from memory cache if enabled if (this.useMemoryCache) { this.memCache.delete(cacheKey); } // Remove from Redis cache try { await this.client.del(cacheKey); } catch (error) { console.error("Failed to delete item from Redis cache:", error); } } /** * Invalidates all items in a namespace * @param namespace Namespace to invalidate */ public async invalidateNamespace(namespace: string): Promise<void> { const namespacePrefix = `${this.prefix}${namespace}:*`; // Clear matching items from memory cache if enabled if (this.useMemoryCache) { for (const key of this.memCache.keys()) { if (key.startsWith(`${this.prefix}${namespace}:`)) { this.memCache.delete(key); } } } // Clear matching items from Redis try { const keys = await this.client.keys(namespacePrefix); if (keys.length > 0) { await this.client.del(...keys); } } catch (error) { console.error("Failed to invalidate namespace in Redis cache:", error); } } /** * Clears the entire cache (both memory and Redis) */ public async clear(): Promise<void> { // Clear memory cache if enabled if (this.useMemoryCache) { this.memCache.clear(); this.memCacheStats = { hits: 0, misses: 0, sets: 0 }; } // Clear Redis cache try { const keys = await this.client.keys(`${this.prefix}*`); if (keys.length > 0) { await this.client.del(...keys); } } catch (error) { console.error("Failed to clear Redis cache:", error); } } /** * Gets multiple items from cache in a single batch operation * @param keys Array of cache keys * @param namespace Optional namespace * @returns Object with keys mapped to their cached values (or null if not found) */ public async getMany<T>( keys: string[], namespace?: string ): Promise<Record<string, T | null>> { const result: Record<string, T | null> = {}; const missingKeys: string[] = []; const keyMapping: Record<string, string> = {}; // Check memory cache first if enabled if (this.useMemoryCache) { for (const key of keys) { const cacheKey = this.getCacheKey(this.createKeyHash(key), namespace); keyMapping[cacheKey] = key; const memItem = this.memCache.get(cacheKey); if ( memItem && (memItem.expiresAt === null || memItem.expiresAt > Date.now()) ) { result[key] = memItem.value as T; this.memCacheStats.hits++; } else { if ( memItem && memItem.expiresAt !== null && memItem.expiresAt <= Date.now() ) { // Remove expired item from memory cache this.memCache.delete(cacheKey); } missingKeys.push(key); this.memCacheStats.misses++; } } } else { // If memory cache is disabled, all keys need to be fetched from Redis missingKeys.push(...keys); for (const key of keys) { const cacheKey = this.getCacheKey(this.createKeyHash(key), namespace); keyMapping[cacheKey] = key; } } // If all keys were found in memory cache, return immediately if (missingKeys.length === 0) { return result; } // Fetch missing keys from Redis using pipeline for efficiency try { const pipeline = this.client.pipeline(); for (const key of missingKeys) { const cacheKey = this.getCacheKey(this.createKeyHash(key), namespace); pipeline.get(cacheKey); } const responses = await pipeline.exec(); if (!responses) return result; for (let i = 0; i < responses.length; i++) { const [err, data] = responses[i]; const originalKey = missingKeys[i]; if (err || !data) { result[originalKey] = null; continue; } try { const item = JSON.parse(data as string) as CacheItem<T>; // Update memory cache if enabled if (this.useMemoryCache) { const cacheKey = this.getCacheKey( this.createKeyHash(originalKey), namespace ); this.setMemoryCache(cacheKey, item); } result[originalKey] = item.value; } catch (parseError) { console.error("Failed to parse Redis cache item:", parseError); result[originalKey] = null; } } } catch (error) { console.error("Failed to get items from Redis cache:", error); // Set remaining keys to null for (const key of missingKeys) { if (!(key in result)) { result[key] = null; } } } return result; } /** * Sets multiple items in the cache in a single batch operation * @param items Object mapping keys to values * @param ttl TTL in seconds (optional, uses default if not specified) * @param namespace Optional namespace */ public async setMany<T>( items: Record<string, T>, ttl?: number, namespace?: string ): Promise<void> { const ttlValue = ttl ?? this.defaultTtl; const now = Date.now(); // Update Redis cache using pipeline for efficiency try { const pipeline = this.client.pipeline(); for (const [key, value] of Object.entries(items)) { const cacheKey = this.getCacheKey(this.createKeyHash(key), namespace); const item: CacheItem<T> = { value, timestamp: now, expiresAt: ttlValue > 0 ? now + ttlValue * 1000 : null, }; // Update memory cache if enabled if (this.useMemoryCache) { this.setMemoryCache(cacheKey, item); } // Add to Redis pipeline if (ttlValue > 0) { pipeline.set(cacheKey, JSON.stringify(item), "EX", ttlValue); } else { pipeline.set(cacheKey, JSON.stringify(item)); } } await pipeline.exec(); } catch (error) { console.error("Failed to set items in Redis cache:", error); // Continue even if Redis fails - we still have memory cache } } /** * Gets cache statistics * @returns Object with cache statistics */ public getStats(): any { const hitRate = this.memCacheStats.hits + this.memCacheStats.misses > 0 ? this.memCacheStats.hits / (this.memCacheStats.hits + this.memCacheStats.misses) : 0; return { memoryCache: { enabled: this.useMemoryCache, size: this.memCache.size, maxSize: this.memCacheSize, hits: this.memCacheStats.hits, misses: this.memCacheStats.misses, sets: this.memCacheStats.sets, hitRate: hitRate.toFixed(2), }, }; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/0xjcf/MCP_CodeAnalysis'

If you have feedback or need assistance with the MCP directory API, please join our Discord server