UNPKG

@houmak/minerva-mcp-server

Version:

Minerva Model Context Protocol (MCP) Server for Microsoft 365 and Azure integrations

256 lines (255 loc) 7.99 kB
import { createClient } from 'redis'; import { logger } from '../logger.js'; export class RedisCache { client; config; isConnected = false; constructor(config) { this.config = { ttl: 3600, // 1 hour default maxRetries: 3, retryDelay: 1000, ...config, }; this.client = createClient({ socket: { host: this.config.host, port: this.config.port, reconnectStrategy: (retries) => { if (retries > this.config.maxRetries) { logger.error('Redis connection failed after max retries'); return false; } return this.config.retryDelay; }, }, password: this.config.password, database: this.config.db || 0, }); this.setupEventHandlers(); } setupEventHandlers() { this.client.on('connect', () => { logger.info('Redis client connected'); this.isConnected = true; }); this.client.on('ready', () => { logger.info('Redis client ready'); }); this.client.on('error', (error) => { logger.error('Redis client error:', error); this.isConnected = false; }); this.client.on('end', () => { logger.info('Redis client disconnected'); this.isConnected = false; }); this.client.on('reconnecting', () => { logger.info('Redis client reconnecting...'); }); } async connect() { try { await this.client.connect(); } catch (error) { logger.error('Failed to connect to Redis:', error); throw error; } } async disconnect() { try { await this.client.disconnect(); } catch (error) { logger.error('Failed to disconnect from Redis:', error); } } async get(key) { if (!this.isConnected) { logger.warn('Redis not connected, skipping cache get'); return null; } try { const value = await this.client.get(key); if (!value) { return null; } const entry = JSON.parse(value); // Check if entry is expired if (Date.now() - entry.timestamp > entry.ttl * 1000) { await this.delete(key); return null; } logger.debug(`Cache hit for key: ${key}`); return entry.data; } catch (error) { logger.error(`Error getting cache key ${key}:`, error); return null; } } async set(key, data, ttl) { if (!this.isConnected) { logger.warn('Redis not connected, skipping cache set'); return; } try { const entry = { data, timestamp: Date.now(), ttl: ttl || this.config.ttl, }; await this.client.setEx(key, entry.ttl, JSON.stringify(entry)); logger.debug(`Cache set for key: ${key} with TTL: ${entry.ttl}s`); } catch (error) { logger.error(`Error setting cache key ${key}:`, error); } } async delete(key) { if (!this.isConnected) { return; } try { await this.client.del(key); logger.debug(`Cache deleted for key: ${key}`); } catch (error) { logger.error(`Error deleting cache key ${key}:`, error); } } async clear() { if (!this.isConnected) { return; } try { await this.client.flushDb(); logger.info('Cache cleared'); } catch (error) { logger.error('Error clearing cache:', error); } } async exists(key) { if (!this.isConnected) { return false; } try { const result = await this.client.exists(key); return result === 1; } catch (error) { logger.error(`Error checking cache key ${key}:`, error); return false; } } async getKeys(pattern) { if (!this.isConnected) { return []; } try { return await this.client.keys(pattern); } catch (error) { logger.error(`Error getting cache keys with pattern ${pattern}:`, error); return []; } } async getStats() { if (!this.isConnected) { return { connected: false, keys: 0, memory: {}, info: {}, }; } try { const [keys, memory, info] = await Promise.all([ this.client.dbSize(), this.client.memoryUsage(''), this.client.info(), ]); return { connected: true, keys, memory, info: this.parseRedisInfo(info), }; } catch (error) { logger.error('Error getting Redis stats:', error); return { connected: false, keys: 0, memory: {}, info: {}, }; } } parseRedisInfo(info) { const lines = info.split('\r\n'); const result = {}; for (const line of lines) { if (line.includes(':')) { const [key, value] = line.split(':'); result[key] = value; } } return result; } isHealthy() { return this.isConnected; } // Cache-specific methods for Minerva async cacheGraphResponse(endpoint, params, data, ttl) { const key = this.generateGraphCacheKey(endpoint, params); await this.set(key, data, ttl); } async getCachedGraphResponse(endpoint, params) { const key = this.generateGraphCacheKey(endpoint, params); return await this.get(key); } async cachePnPResponse(command, params, data, ttl) { const key = this.generatePnPCacheKey(command, params); await this.set(key, data, ttl); } async getCachedPnPResponse(command, params) { const key = this.generatePnPCacheKey(command, params); return await this.get(key); } generateGraphCacheKey(endpoint, params) { const paramsHash = JSON.stringify(params); return `graph:${endpoint}:${Buffer.from(paramsHash).toString('base64')}`; } generatePnPCacheKey(command, params) { const paramsHash = JSON.stringify(params); return `pnp:${command}:${Buffer.from(paramsHash).toString('base64')}`; } // Cache invalidation patterns async invalidateUserCache(userId) { const pattern = userId ? `graph:*/users/${userId}*` : 'graph:*/users/*'; const keys = await this.getKeys(pattern); for (const key of keys) { await this.delete(key); } logger.info(`Invalidated ${keys.length} user cache entries`); } async invalidateSiteCache(siteId) { const pattern = siteId ? `graph:*/sites/${siteId}*` : 'graph:*/sites/*'; const keys = await this.getKeys(pattern); for (const key of keys) { await this.delete(key); } logger.info(`Invalidated ${keys.length} site cache entries`); } async invalidateGroupCache(groupId) { const pattern = groupId ? `graph:*/groups/${groupId}*` : 'graph:*/groups/*'; const keys = await this.getKeys(pattern); for (const key of keys) { await this.delete(key); } logger.info(`Invalidated ${keys.length} group cache entries`); } }