Skip to main content
Glama

Analytical MCP Server

cache_manager.ts17.8 kB
/** * Cache Manager Utility * * Provides sophisticated caching capabilities for API responses, research results, * and extracted facts to improve performance and reduce API calls. */ import fs from 'fs/promises'; import path from 'path'; import { Logger } from './logger.js'; import { config, isFeatureEnabled } from './config.js'; // Types for cache entries and options export interface CacheEntry<T> { data: T; timestamp: number; ttl: number; metadata?: Record<string, any>; source?: string; } export interface CacheOptions { ttl?: number; // Time to live in milliseconds namespace?: string; // Cache namespace for logical grouping persistent?: boolean; // Whether to persist to disk refreshThreshold?: number; // Percentage of TTL after which to refresh (0-100) metadata?: Record<string, any>; // Additional metadata to store cacheFileOptions?: { directory?: string; prefix?: string; }; } export interface CacheStats { hits: number; misses: number; puts: number; evictions: number; size: number; oldestEntry: number | null; newestEntry: number | null; } /** * Cache manager for improved performance */ export class CacheManager { private cache: Map<string, CacheEntry<any>> = new Map(); private persistentCacheEnabled: boolean; private persistentCacheDir: string; private stats: Record<string, CacheStats> = {}; private defaultTTL: number; // Default cache options private static DEFAULT_OPTIONS: CacheOptions = { ttl: 24 * 60 * 60 * 1000, // 24 hours namespace: 'default', persistent: false, refreshThreshold: 80, // Refresh after 80% of TTL has elapsed cacheFileOptions: { directory: './cache', prefix: 'cache_', }, }; constructor( options: { persistentCacheEnabled?: boolean; persistentCacheDir?: string; defaultTTL?: number; } = {} ) { // Use both direct config and feature flag system for consistency const cachingEnabled = isFeatureEnabled('caching'); this.persistentCacheEnabled = cachingEnabled && (options.persistentCacheEnabled ?? config.CACHE_PERSISTENT === 'true'); this.persistentCacheDir = options.persistentCacheDir ?? config.CACHE_DIR ?? path.join(process.cwd(), 'cache'); this.defaultTTL = options.defaultTTL ?? parseInt(config.CACHE_DEFAULT_TTL ?? '86400000', 10); // Initialize stats for default namespace this.stats['default'] = this.createEmptyStats(); Logger.debug(`Cache manager initialized`, { cachingEnabled, persistentCacheEnabled: this.persistentCacheEnabled, persistentCacheDir: this.persistentCacheDir, defaultTTL: this.defaultTTL }); // Ensure persistent cache directory exists if enabled if (this.persistentCacheEnabled) { this.ensureCacheDirectoryExists().catch((err: any) => { Logger.warn(`Failed to create cache directory: ${err.message}`); }); } // Set up cleanup interval const cleanupInterval = parseInt(config.CACHE_CLEANUP_INTERVAL ?? '3600000', 10); setInterval(() => this.cleanup(), cleanupInterval); } /** * Get an item from the cache (synchronous for backward compatibility) */ get<T>(key: string, options: CacheOptions = {}): T | null { // Early return if caching is disabled via feature flag if (!isFeatureEnabled('caching')) { return null; } const resolvedOptions = this.resolveOptions(options); const cacheKey = this.getCacheKey(key, resolvedOptions.namespace!); // Check in-memory cache first const entry = this.cache.get(cacheKey); const statsKey = resolvedOptions.namespace || 'default'; // Initialize stats for namespace if needed if (!this.stats[statsKey]) { this.stats[statsKey] = this.createEmptyStats(); } if (entry) { const now = Date.now(); const age = now - entry.timestamp; // Check if the entry is still valid if (age < entry.ttl) { this.stats[statsKey].hits++; // Check if we should trigger a refresh (but still return cached data) const refreshThreshold = resolvedOptions.refreshThreshold || 80; const refreshThresholdMs = (entry.ttl * refreshThreshold) / 100; if (age > refreshThresholdMs) { Logger.debug(`Cache entry is nearing expiration, triggering background refresh`, { key: cacheKey, age, ttl: entry.ttl, refreshThreshold, }); // We'll return a signal that a refresh is recommended return { ...entry.data, __cacheRefreshRecommended: true, } as T; } return entry.data; } else { // Entry is expired, remove it this.cache.delete(cacheKey); this.stats[statsKey].evictions++; this.stats[statsKey].size = this.cache.size; } } // If not in memory and persistent cache is enabled, load asynchronously in background if (this.persistentCacheEnabled && resolvedOptions.persistent) { // Trigger async load but don't wait for it in sync method this.loadFromDiskAsync<T>(cacheKey).then(persistentEntry => { if (persistentEntry) { const now = Date.now(); const age = now - persistentEntry.timestamp; // Check if the loaded entry is still valid if (age < persistentEntry.ttl) { // Store in memory cache for future access this.cache.set(cacheKey, persistentEntry); this.stats[statsKey].size = this.cache.size; } } }).catch(error => { Logger.warn(`Failed to load cache entry from disk: ${cacheKey}`, error); }); } // Not found or expired this.stats[statsKey].misses++; return null; } /** * Get an item from the cache (async version with disk support) */ async getAsync<T>(key: string, options: CacheOptions = {}): Promise<T | null> { // Early return if caching is disabled via feature flag if (!isFeatureEnabled('caching')) { return null; } const resolvedOptions = this.resolveOptions(options); const cacheKey = this.getCacheKey(key, resolvedOptions.namespace!); // Check in-memory cache first const entry = this.cache.get(cacheKey); const statsKey = resolvedOptions.namespace || 'default'; // Initialize stats for namespace if needed if (!this.stats[statsKey]) { this.stats[statsKey] = this.createEmptyStats(); } if (entry) { const now = Date.now(); const age = now - entry.timestamp; // Check if the entry is still valid if (age < entry.ttl) { this.stats[statsKey].hits++; return entry.data; } else { // Entry is expired, remove it this.cache.delete(cacheKey); this.stats[statsKey].evictions++; this.stats[statsKey].size = this.cache.size; } } // If not in memory and persistent cache is enabled, try to load from disk if (this.persistentCacheEnabled && resolvedOptions.persistent) { try { const persistentEntry = await this.loadFromDiskAsync<T>(cacheKey); if (persistentEntry) { const now = Date.now(); const age = now - persistentEntry.timestamp; // Check if the loaded entry is still valid if (age < persistentEntry.ttl) { // Store in memory cache and return this.cache.set(cacheKey, persistentEntry); this.stats[statsKey].hits++; this.stats[statsKey].size = this.cache.size; return persistentEntry.data; } } } catch (error) { Logger.warn(`Failed to load cache entry from disk: ${cacheKey}`, error); } } // Not found or expired this.stats[statsKey].misses++; return null; } /** * Set an item in the cache */ set<T>(key: string, data: T, options: CacheOptions = {}): void { // Early return if caching is disabled via feature flag if (!isFeatureEnabled('caching')) { return; } const resolvedOptions = this.resolveOptions(options); const cacheKey = this.getCacheKey(key, resolvedOptions.namespace!); const entry: CacheEntry<T> = { data, timestamp: Date.now(), ttl: resolvedOptions.ttl!, metadata: resolvedOptions.metadata, }; // Store in memory cache this.cache.set(cacheKey, entry); // Update stats const statsKey = resolvedOptions.namespace || 'default'; if (!this.stats[statsKey]) { this.stats[statsKey] = this.createEmptyStats(); } this.stats[statsKey].puts++; this.stats[statsKey].size = this.cache.size; if (!this.stats[statsKey].oldestEntry || entry.timestamp < this.stats[statsKey].oldestEntry) { this.stats[statsKey].oldestEntry = entry.timestamp; } if (!this.stats[statsKey].newestEntry || entry.timestamp > this.stats[statsKey].newestEntry) { this.stats[statsKey].newestEntry = entry.timestamp; } // Persist to disk if enabled if (this.persistentCacheEnabled && resolvedOptions.persistent) { this.saveToDisk(cacheKey, entry).catch((error) => Logger.warn(`Failed to persist cache entry to disk: ${cacheKey}`, error) ); } Logger.debug(`Cache entry set`, { key: cacheKey, ttl: entry.ttl, persistent: resolvedOptions.persistent, }); } /** * Check if an item exists in the cache */ has(key: string, namespace: string = 'default'): boolean { const cacheKey = this.getCacheKey(key, namespace); const entry = this.cache.get(cacheKey); if (entry) { const now = Date.now(); const age = now - entry.timestamp; // Check if the entry is still valid if (age < entry.ttl) { return true; } else { // Entry is expired, remove it this.cache.delete(cacheKey); return false; } } return false; } /** * Remove an item from the cache */ remove(key: string, namespace: string = 'default'): boolean { const cacheKey = this.getCacheKey(key, namespace); const result = this.cache.delete(cacheKey); // Also remove from disk if persistent cache is enabled if (this.persistentCacheEnabled) { try { const cacheFilePath = this.getCacheFilePath(cacheKey); fs.unlink(cacheFilePath).catch(() => {}); } catch (error) { // Ignore errors when removing from disk } } // Update stats const statsKey = namespace || 'default'; if (this.stats[statsKey]) { this.stats[statsKey].size = this.cache.size; } return result; } /** * Remove all items from a namespace */ clearNamespace(namespace: string): void { const keysToRemove: string[] = []; // Find all keys in the namespace for (const [key] of this.cache.entries()) { if (key.startsWith(`${namespace}:`)) { keysToRemove.push(key); } } // Remove from memory cache keysToRemove.forEach((key) => this.cache.delete(key)); // Remove from disk if persistent cache is enabled if (this.persistentCacheEnabled) { keysToRemove.forEach((key) => { try { const cacheFilePath = this.getCacheFilePath(key); fs.unlink(cacheFilePath).catch(() => {}); } catch (error) { // Ignore errors when removing from disk } }); } // Reset stats for namespace this.stats[namespace] = this.createEmptyStats(); Logger.debug(`Cleared cache namespace: ${namespace}`); } /** * Clear all caches */ clear(): void { // Clear memory cache this.cache.clear(); // Reset stats Object.keys(this.stats).forEach((key) => { this.stats[key] = this.createEmptyStats(); }); Logger.debug('Cleared all caches'); } /** * Get cache statistics */ getStats(namespace: string = 'default'): CacheStats { return this.stats[namespace] || this.createEmptyStats(); } /** * Get all cache statistics */ getAllStats(): Record<string, CacheStats> { return this.stats; } /** * Clean up expired entries */ cleanup(): void { const now = Date.now(); const expiredKeys: string[] = []; // Find expired entries for (const [key, entry] of this.cache.entries()) { const age = now - entry.timestamp; if (age >= entry.ttl) { expiredKeys.push(key); } } // Remove expired entries expiredKeys.forEach((key) => { this.cache.delete(key); // Also remove from disk if persistent cache is enabled if (this.persistentCacheEnabled) { try { const cacheFilePath = this.getCacheFilePath(key); fs.unlink(cacheFilePath).catch(() => {}); } catch (error) { // Ignore errors when removing from disk } } }); // Update stats Object.keys(this.stats).forEach((key) => { if (this.stats[key]) { this.stats[key].evictions += expiredKeys.filter((cacheKey) => cacheKey.startsWith(`${key}:`) ).length; this.stats[key].size = Array.from(this.cache.keys()).filter((cacheKey) => cacheKey.startsWith(`${key}:`) ).length; } }); if (expiredKeys.length > 0) { Logger.debug(`Cleaned up ${expiredKeys.length} expired cache entries`); } } /** * Preload cache from disk */ async preload(): Promise<number> { if (!this.persistentCacheEnabled) { return 0; } try { // Ensure cache directory exists await fs.mkdir(this.persistentCacheDir, { recursive: true }); // Read directory const files = await fs.readdir(this.persistentCacheDir); const cacheFiles = files.filter((file) => file.startsWith('cache_')); let loadedCount = 0; // Load each file for (const file of cacheFiles) { try { const filePath = path.join(this.persistentCacheDir, file); const content = await fs.readFile(filePath, 'utf-8'); const entry: CacheEntry<any> = JSON.parse(content); // Check if entry is still valid const now = Date.now(); const age = now - entry.timestamp; if (age < entry.ttl) { // Extract key from filename const key = file.substring(6); // Remove 'cache_' prefix // Store in memory cache this.cache.set(key, entry); loadedCount++; } else { // Entry is expired, remove it await fs.unlink(filePath); } } catch (error) { Logger.warn(`Failed to load cache file: ${file}`, error); } } Logger.info(`Preloaded ${loadedCount} cache entries from disk`); return loadedCount; } catch (error) { Logger.error('Failed to preload cache from disk', error); return 0; } } /** * Get default TTL */ getDefaultTTL(): number { return this.defaultTTL; } /** * Get cache size */ size(): number { return this.cache.size; } /** * Helper to get cache key */ private getCacheKey(key: string, namespace: string): string { return `${namespace}:${key}`; } /** * Helper to resolve cache options */ private resolveOptions(options: CacheOptions): Required<CacheOptions> { return { ...CacheManager.DEFAULT_OPTIONS, ...options, ttl: options.ttl ?? this.defaultTTL, namespace: options.namespace ?? 'default', cacheFileOptions: { ...CacheManager.DEFAULT_OPTIONS.cacheFileOptions, ...options.cacheFileOptions, }, } as Required<CacheOptions>; } /** * Save cache entry to disk */ private async saveToDisk<T>(key: string, entry: CacheEntry<T>): Promise<void> { if (!this.persistentCacheEnabled) { return; } try { // Ensure cache directory exists await fs.mkdir(this.persistentCacheDir, { recursive: true }); // Save entry to file const cacheFilePath = this.getCacheFilePath(key); await fs.writeFile(cacheFilePath, JSON.stringify(entry, null, 2)); } catch (error) { Logger.warn(`Failed to save cache entry to disk: ${key}`, error); } } /** * Load cache entry from disk (async version for better performance) */ private async loadFromDiskAsync<T>(key: string): Promise<CacheEntry<T> | null> { if (!this.persistentCacheEnabled) { return null; } try { const cacheFilePath = this.getCacheFilePath(key); // Check if file exists try { await fs.access(cacheFilePath); } catch { return null; } // Read and parse file asynchronously const content = await fs.readFile(cacheFilePath, 'utf-8'); return JSON.parse(content); } catch (error) { return null; } } /** * Ensure cache directory exists */ private async ensureCacheDirectoryExists(): Promise<void> { try { await fs.mkdir(this.persistentCacheDir, { recursive: true }); } catch (error) { Logger.error('Failed to create cache directory', error); throw error; } } /** * Get cache file path */ private getCacheFilePath(key: string): string { // Sanitize the key for file system use const sanitizedKey = key.replace(/[^a-zA-Z0-9_-]/g, '_'); return path.join(this.persistentCacheDir, `cache_${sanitizedKey}.json`); } /** * Create empty stats object */ private createEmptyStats(): CacheStats { return { hits: 0, misses: 0, puts: 0, evictions: 0, size: 0, oldestEntry: null, newestEntry: null, }; } } // Export singleton instance export const cacheManager = new CacheManager();

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/quanticsoul4772/analytical-mcp'

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