Skip to main content
Glama
northernvariables

FedMCP - Federal Parliamentary Information

toolCache.ts7.74 kB
/** * Tool Result Cache - In-Memory LRU Cache with TTL * * Caches tool execution results to reduce redundant GraphQL queries and API costs. * Uses a simple LRU (Least Recently Used) eviction policy with configurable TTLs. */ import crypto from 'crypto'; interface CacheEntry<T> { value: T; expiresAt: number; createdAt: number; } interface CacheStats { hits: number; misses: number; evictions: number; size: number; } /** * TTL Configuration by Tool Type (in seconds) * * Strategy: * - Static data (MPs, committees, bills): 24 hours * - Semi-static (recent debates): 1 hour * - Search results (hansard): 30 minutes * - Lobbying data: 6 hours * - Scorecards/aggregations: 1 hour */ const TOOL_TTL_CONFIG: Record<string, number> = { // MP Tools - mostly static (MPs don't change often) 'search_mps': 24 * 60 * 60, // 24 hours 'get_mp': 24 * 60 * 60, // 24 hours 'get_mp_scorecard': 60 * 60, // 1 hour (aggregated data) 'get_mp_speeches': 60 * 60, // 1 hour (can have new speeches) // Bill Tools - static once passed 'search_bills': 6 * 60 * 60, // 6 hours 'get_bill': 6 * 60 * 60, // 6 hours 'get_bill_lobbying': 6 * 60 * 60, // 6 hours 'get_bill_debates': 60 * 60, // 1 hour // Hansard/Debate Tools - search results vary 'search_hansard': 30 * 60, // 30 minutes 'get_recent_debates': 60 * 60, // 1 hour // Committee Tools - mostly static 'get_committees': 24 * 60 * 60, // 24 hours 'get_committee': 24 * 60 * 60, // 24 hours 'get_committee_testimony': 60 * 60, // 1 hour // Accountability/Lobbying Tools 'get_top_spenders': 6 * 60 * 60, // 6 hours (quarterly data) 'detect_conflicts_of_interest': 6 * 60 * 60, // 6 hours 'search_lobby_registrations': 6 * 60 * 60, // 6 hours // Navigation Tools - no cache (just URL building) 'navigate_to_hansard': 0, // No cache }; // Default TTL for tools not in config: 30 minutes const DEFAULT_TTL = 30 * 60; // Maximum cache size (number of entries) before LRU eviction const MAX_CACHE_SIZE = 1000; class ToolCache<T = any> { private cache: Map<string, CacheEntry<T>>; private accessOrder: string[]; // For LRU tracking private stats: CacheStats; constructor() { this.cache = new Map(); this.accessOrder = []; this.stats = { hits: 0, misses: 0, evictions: 0, size: 0, }; } /** * Generate cache key from tool name and parameters */ private generateKey(toolName: string, params: Record<string, any>): string { // Sort parameters for consistent key generation const sortedParams = Object.keys(params) .sort() .reduce((acc, key) => { acc[key] = params[key]; return acc; }, {} as Record<string, any>); const payload = `${toolName}:${JSON.stringify(sortedParams)}`; // Use SHA-256 hash for consistent, collision-resistant keys return crypto.createHash('sha256').update(payload).digest('hex'); } /** * Get TTL for a tool (in seconds) */ private getTTL(toolName: string): number { return TOOL_TTL_CONFIG[toolName] ?? DEFAULT_TTL; } /** * Update LRU access order */ private updateAccessOrder(key: string): void { // Remove from current position const index = this.accessOrder.indexOf(key); if (index > -1) { this.accessOrder.splice(index, 1); } // Add to end (most recently used) this.accessOrder.push(key); } /** * Evict least recently used entry */ private evictLRU(): void { if (this.accessOrder.length === 0) return; const lruKey = this.accessOrder.shift()!; this.cache.delete(lruKey); this.stats.evictions++; this.stats.size = this.cache.size; } /** * Clean expired entries */ private cleanExpired(): void { const now = Date.now(); const expiredKeys: string[] = []; this.cache.forEach((entry, key) => { if (entry.expiresAt < now) { expiredKeys.push(key); } }); expiredKeys.forEach((key) => { this.cache.delete(key); const index = this.accessOrder.indexOf(key); if (index > -1) { this.accessOrder.splice(index, 1); } }); if (expiredKeys.length > 0) { this.stats.size = this.cache.size; console.log(`[ToolCache] Cleaned ${expiredKeys.length} expired entries`); } } /** * Get cached value */ get(toolName: string, params: Record<string, any>): T | null { // Clean expired entries periodically (every 100 requests) if ((this.stats.hits + this.stats.misses) % 100 === 0) { this.cleanExpired(); } const key = this.generateKey(toolName, params); const entry = this.cache.get(key); if (!entry) { this.stats.misses++; console.log(`[ToolCache] MISS: ${toolName}`, { params }); return null; } // Check if expired if (entry.expiresAt < Date.now()) { this.cache.delete(key); this.stats.misses++; this.stats.size = this.cache.size; console.log(`[ToolCache] EXPIRED: ${toolName}`, { params }); return null; } // Update access order this.updateAccessOrder(key); this.stats.hits++; const age = Math.floor((Date.now() - entry.createdAt) / 1000); console.log(`[ToolCache] HIT: ${toolName} (age: ${age}s)`, { params }); return entry.value; } /** * Set cached value */ set(toolName: string, params: Record<string, any>, value: T): void { const ttl = this.getTTL(toolName); // Skip caching if TTL is 0 if (ttl === 0) { return; } const key = this.generateKey(toolName, params); // Evict LRU if cache is full if (this.cache.size >= MAX_CACHE_SIZE && !this.cache.has(key)) { this.evictLRU(); } const now = Date.now(); const entry: CacheEntry<T> = { value, expiresAt: now + ttl * 1000, createdAt: now, }; this.cache.set(key, entry); this.updateAccessOrder(key); this.stats.size = this.cache.size; console.log(`[ToolCache] SET: ${toolName} (TTL: ${ttl}s)`, { params }); } /** * Clear all cached entries */ clear(): void { this.cache.clear(); this.accessOrder = []; this.stats = { hits: 0, misses: 0, evictions: 0, size: 0, }; console.log('[ToolCache] Cache cleared'); } /** * Get cache statistics */ getStats(): CacheStats { const total = this.stats.hits + this.stats.misses; const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0; return { ...this.stats, hitRate: Math.round(hitRate * 100) / 100, // Round to 2 decimals } as CacheStats & { hitRate: number }; } /** * Invalidate cache entries for a specific tool */ invalidateTool(toolName: string): void { const keysToDelete: string[] = []; // Find all entries for this tool // This is a simple implementation - could be optimized with a secondary index this.cache.forEach((entry, key) => { // We can't easily reverse-engineer the tool name from the hash, // so this is a limitation of the current implementation // For now, we'll just clear the entire cache }); console.log(`[ToolCache] Invalidating all entries for: ${toolName}`); // TODO: Implement partial invalidation with secondary index } } // Singleton instance const toolCache = new ToolCache(); // Export cache instance and types export { toolCache, type CacheStats }; /** * Log cache statistics (call periodically in production) */ export function logCacheStats(): void { const stats = toolCache.getStats(); console.log('[ToolCache] Statistics:', stats); } /** * Clear cache (useful for testing or manual invalidation) */ export function clearCache(): void { toolCache.clear(); }

Latest Blog Posts

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/northernvariables/FedMCP'

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