Skip to main content
Glama

OPNSense MCP Server

manager.ts12.7 kB
// MCP Cache Manager for Local Infrastructure Integration import { OPNSenseAPIClient } from '../api/client.js'; import Redis from 'ioredis'; import pkg from 'pg'; const { Pool } = pkg; export interface CacheConfig { redisHost?: string; redisPort?: number; redisPassword?: string; redisDb?: number; redisKeyPrefix?: string; postgresHost?: string; postgresPort?: number; postgresDb?: string; postgresSchema?: string; postgresUser?: string; postgresPassword?: string; cacheTTL?: number; enableCache?: boolean; } export interface CachedData<T> { data: T; timestamp: Date; ttl: number; source: 'cache' | 'api'; } export class MCPCacheManager { private config: CacheConfig; private client: OPNSenseAPIClient; private redisClient: Redis | null = null; private pgPool: any = null; private keyPrefix: string; constructor(client: OPNSenseAPIClient, config: CacheConfig = {}) { this.client = client; this.config = { redisHost: config.redisHost || process.env.REDIS_HOST || 'localhost', redisPort: config.redisPort || parseInt(process.env.REDIS_PORT || '6379'), redisPassword: config.redisPassword || process.env.REDIS_PASSWORD, redisDb: config.redisDb || parseInt(process.env.REDIS_DB || '0'), redisKeyPrefix: config.redisKeyPrefix || process.env.REDIS_KEY_PREFIX || 'opnsense:', postgresHost: config.postgresHost || process.env.POSTGRES_HOST || 'localhost', postgresPort: config.postgresPort || parseInt(process.env.POSTGRES_PORT || '5432'), postgresDb: config.postgresDb || process.env.POSTGRES_DB || 'opnsense_mcp', postgresSchema: config.postgresSchema || process.env.POSTGRES_SCHEMA || 'public', postgresUser: config.postgresUser || process.env.POSTGRES_USER || 'mcp_user', postgresPassword: config.postgresPassword || process.env.POSTGRES_PASSWORD, cacheTTL: config.cacheTTL || parseInt(process.env.CACHE_TTL || '300'), enableCache: config.enableCache !== false && process.env.ENABLE_CACHE !== 'false' }; this.keyPrefix = this.config.redisKeyPrefix!; // Initialize connections // Don't auto-initialize - wait for connect() to be called } /** * Connect to cache services */ async connect(): Promise<void> { await this.initializeConnections(); } /** * Initialize database connections */ private async initializeConnections(): Promise<void> { if (!this.config.enableCache) return; try { // Connect to Redis with optional authentication this.redisClient = new Redis({ host: this.config.redisHost, port: this.config.redisPort, password: this.config.redisPassword, db: this.config.redisDb, keyPrefix: this.keyPrefix, retryStrategy: (times) => { const delay = Math.min(times * 50, 2000); return delay; } }); this.redisClient.on('connect', () => { console.log(`Connected to Redis at ${this.config.redisHost}:${this.config.redisPort} (using database ${this.config.redisDb})`); }); this.redisClient.on('error', (err) => { console.error('Redis connection error:', err); }); // Connect to PostgreSQL this.pgPool = new Pool({ host: this.config.postgresHost, port: this.config.postgresPort, database: this.config.postgresDb, user: this.config.postgresUser, password: this.config.postgresPassword, max: 10, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, }); // Set search path to include opnsense schema await this.pgPool.query(`SET search_path TO ${this.config.postgresSchema}, public`); console.log(`Connected to PostgreSQL at ${this.config.postgresHost}:${this.config.postgresPort}/${this.config.postgresDb} (schema: ${this.config.postgresSchema})`); } catch (error) { console.error('Failed to initialize cache connections:', error); } } /** * Get data with caching */ async get<T>( key: string, fetcher: () => Promise<T>, ttl?: number ): Promise<CachedData<T>> { if (!this.config.enableCache || !this.redisClient) { const data = await fetcher(); return { data, timestamp: new Date(), ttl: 0, source: 'api' }; } try { // Check Redis cache (key already has prefix from Redis client) const cached = await this.redisClient.get(key); if (cached) { await this.logCacheAccess(key, 'hit'); return { data: JSON.parse(cached), timestamp: new Date(), ttl: ttl || this.config.cacheTTL!, source: 'cache' }; } } catch (error) { console.error('Redis get error:', error); } // Fetch from API const data = await fetcher(); // Store in Redis cache try { await this.redisClient!.setex( key, ttl || this.config.cacheTTL!, JSON.stringify(data) ); } catch (error) { console.error('Redis set error:', error); } // Log cache miss await this.logCacheAccess(key, 'miss'); return { data, timestamp: new Date(), ttl: ttl || this.config.cacheTTL!, source: 'api' }; } /** * Invalidate cache entries matching pattern */ async invalidate(pattern: string): Promise<void> { if (!this.redisClient) return; try { // Get all keys matching pattern (accounting for key prefix) const keys = await this.redisClient.keys(`${pattern}*`); if (keys.length > 0) { // Remove the key prefix before deleting const unprefixedKeys = keys.map(k => k.replace(this.keyPrefix, '')); await this.redisClient.del(...unprefixedKeys); console.log(`Invalidated ${keys.length} cache entries matching: ${this.keyPrefix}${pattern}`); } } catch (error) { console.error('Cache invalidation error:', error); } } /** * Get cached firewall rules */ async getFirewallRules(): Promise<CachedData<any[]>> { return this.get('cache:firewall:rules', () => this.client.searchFirewallRules(), 300 // 5 minute cache ); } /** * Get cached VLANs */ async getVlans(): Promise<CachedData<any[]>> { return this.get('cache:network:vlans', () => this.client.searchVlans(), 600 // 10 minute cache ); } /** * Get cached interfaces */ async getInterfaces(): Promise<CachedData<any>> { return this.get('cache:network:interfaces', () => this.client.get('/interfaces/overview/export'), 1800 // 30 minute cache ); } /** * Log operation for audit trail in PostgreSQL */ async logOperation(operation: { type: string; target: string; action: string; result: 'success' | 'failure'; params?: any; backupId?: string; error?: string; durationMs?: number; }): Promise<void> { if (!this.pgPool) return; try { const query = ` INSERT INTO opnsense.operations (operation_id, type, target, action, params, result, error_message, backup_id, duration_ms) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) `; const operationId = `op_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; await this.pgPool.query(query, [ operationId, operation.type, operation.target, operation.action, JSON.stringify(operation.params || {}), operation.result, operation.error || null, operation.backupId || null, operation.durationMs || null ]); // Also cache recent operations in Redis for quick access if (this.redisClient) { const recentKey = 'cache:operations:recent'; await this.redisClient.lpush(recentKey, JSON.stringify({ ...operation, operationId, timestamp: new Date() })); // Keep only last 100 operations await this.redisClient.ltrim(recentKey, 0, 99); await this.redisClient.expire(recentKey, 3600); // 1 hour TTL } } catch (error) { console.error('Failed to log operation:', error); } } /** * Get recent operations from cache or database */ async getRecentOperations(limit: number = 10): Promise<any[]> { // Try Redis first if (this.redisClient) { try { const operations = await this.redisClient.lrange('cache:operations:recent', 0, limit - 1); if (operations.length > 0) { return operations.map(op => JSON.parse(op)); } } catch (error) { console.error('Redis operations fetch error:', error); } } // Fall back to PostgreSQL if (this.pgPool) { try { const result = await this.pgPool.query( 'SELECT * FROM opnsense.recent_operations LIMIT $1', [limit] ); return result.rows; } catch (error) { console.error('PostgreSQL operations fetch error:', error); } } return []; } /** * Store command in Redis queue for async processing */ async queueCommand(command: { id: string; type: string; params: any; priority?: number; }): Promise<void> { if (!this.redisClient) return; try { const queueKey = command.priority ? 'queue:commands:priority' : 'queue:commands:normal'; await this.redisClient.lpush(queueKey, JSON.stringify({ ...command, timestamp: new Date() })); console.log(`Queued command ${command.id} to ${queueKey}`); } catch (error) { console.error('Failed to queue command:', error); } } /** * Health check for cache services */ async healthCheck(): Promise<{ redis: boolean; postgres: boolean; redisInfo?: any; postgresInfo?: any; }> { const health: any = { redis: false, postgres: false }; // Check Redis if (this.redisClient) { try { const pong = await this.redisClient.ping(); health.redis = pong === 'PONG'; if (health.redis) { const info = await this.redisClient.info('server'); health.redisInfo = { version: info.match(/redis_version:(.+)/)?.[1], uptime: info.match(/uptime_in_seconds:(.+)/)?.[1] }; } } catch (error) { console.error('Redis health check failed:', error); } } // Check PostgreSQL if (this.pgPool) { try { const result = await this.pgPool.query('SELECT version(), current_schema()'); health.postgres = true; health.postgresInfo = { version: result.rows[0].version, schema: result.rows[0].current_schema }; } catch (error) { console.error('PostgreSQL health check failed:', error); } } return health; } /** * Log cache access for analytics */ private async logCacheAccess(key: string, result: 'hit' | 'miss'): Promise<void> { if (!this.pgPool) return; try { await this.pgPool.query( `INSERT INTO opnsense.cache_performance (time, cache_key, hit_miss, response_time_ms, data_size_bytes) VALUES (NOW(), $1, $2, $3, $4)`, [key, result, 0, 0] ); } catch (error) { // Don't fail operations due to logging errors console.error('Cache access logging error:', error); } } /** * Get cache statistics from PostgreSQL */ async getStats(): Promise<{ hits: number; misses: number; hitRate: number; recentStats: any[]; }> { if (!this.pgPool) { return { hits: 0, misses: 0, hitRate: 0, recentStats: [] }; } try { const result = await this.pgPool.query( 'SELECT * FROM opnsense.cache_statistics LIMIT 24' ); const totals = result.rows.reduce((acc: {hits: number, misses: number}, row: any) => ({ hits: acc.hits + parseInt(row.hits), misses: acc.misses + parseInt(row.misses) }), { hits: 0, misses: 0 }); return { ...totals, hitRate: totals.hits / (totals.hits + totals.misses) * 100 || 0, recentStats: result.rows }; } catch (error) { console.error('Failed to get cache stats:', error); return { hits: 0, misses: 0, hitRate: 0, recentStats: [] }; } } /** * Close all connections */ async close(): Promise<void> { if (this.redisClient) { await this.redisClient.quit(); } if (this.pgPool) { await this.pgPool.end(); } } } export default MCPCacheManager;

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/vespo92/OPNSenseMCP'

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