Skip to main content
Glama

OPNSense MCP Server

store.ts14.6 kB
import { promises as fs } from 'fs'; import { join } from 'path'; import path from 'path'; import * as crypto from 'crypto'; import { IaCResource as Resource, ResourceState } from '../resources/base.js'; /** * State entry for a resource */ export interface StateEntry { id: string; type: string; name: string; state: ResourceState; properties: Record<string, any>; outputs: Record<string, any>; metadata: { createdAt: string; updatedAt: string; version: number; }; dependencies: string[]; } /** * Deployment state */ export interface DeploymentState { id: string; name: string; version: number; createdAt: string; updatedAt: string; resources: Record<string, StateEntry>; checkpoints: Checkpoint[]; } /** * Checkpoint for rollback */ export interface Checkpoint { id: string; timestamp: string; description: string; state: Record<string, StateEntry>; } /** * State store options */ export interface StateStoreOptions { directory?: string; format?: 'json' | 'yaml'; encryption?: { enabled: boolean; key?: string; }; } /** * Resource State Store - Manages persistent state for deployments */ export class ResourceStateStore { private state: Map<string, DeploymentState> = new Map(); private options: Required<StateStoreOptions>; private plans: Map<string, any> = new Map(); constructor(options: StateStoreOptions = {}) { this.options = { directory: options.directory || './state', format: options.format || 'json', encryption: options.encryption || { enabled: false } }; } /** * Initialize the state store */ async initialize(): Promise<void> { // Ensure state directory exists try { await fs.access(this.options.directory); } catch { await fs.mkdir(this.options.directory, { recursive: true }); } } /** * Get encryption key from environment or options */ private getEncryptionKey(): Buffer { const key = this.options.encryption.key || process.env.STATE_ENCRYPTION_KEY; if (!key) { throw new Error('Encryption key not provided. Set STATE_ENCRYPTION_KEY environment variable or provide key in options.'); } // Ensure key is 32 bytes for AES-256 const hash = crypto.createHash('sha256'); hash.update(key); return hash.digest(); } /** * Encrypt data using AES-256-GCM */ private encrypt(data: string): string { if (!this.options.encryption?.enabled) return data; const algorithm = 'aes-256-gcm'; const key = this.getEncryptionKey(); const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(algorithm, key, iv); let encrypted = cipher.update(data, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); // Combine IV, authTag, and encrypted data return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`; } /** * Decrypt data using AES-256-GCM */ private decrypt(encryptedData: string): string { if (!this.options.encryption?.enabled) return encryptedData; const algorithm = 'aes-256-gcm'; const key = this.getEncryptionKey(); // Split the encrypted data const parts = encryptedData.split(':'); if (parts.length !== 3) { throw new Error('Invalid encrypted data format'); } const [ivHex, authTagHex, encrypted] = parts; const iv = Buffer.from(ivHex, 'hex'); const authTag = Buffer.from(authTagHex, 'hex'); const decipher = crypto.createDecipheriv(algorithm, key, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } /** * Load deployment state from disk */ async loadDeployment(deploymentId: string): Promise<DeploymentState | null> { try { const filePath = this.getStateFilePath(deploymentId); const encryptedData = await fs.readFile(filePath, 'utf-8'); // Decrypt if encryption is enabled const data = this.decrypt(encryptedData); const state = JSON.parse(data) as DeploymentState; this.state.set(deploymentId, state); return state; } catch (error) { if ((error as any).code === 'ENOENT') { return null; // File doesn't exist } throw error; } } /** * Save deployment state to disk */ async saveDeployment(deployment: DeploymentState): Promise<void> { const filePath = this.getStateFilePath(deployment.id); // Update timestamp deployment.updatedAt = new Date().toISOString(); deployment.version++; // Convert to JSON const jsonData = JSON.stringify(deployment, null, 2); // Encrypt if enabled const data = this.encrypt(jsonData); await fs.writeFile(filePath, data, 'utf-8'); // Update in-memory cache this.state.set(deployment.id, deployment); } /** * Create a new deployment */ async createDeployment(id: string, name: string): Promise<DeploymentState> { const deployment: DeploymentState = { id, name, version: 1, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), resources: {}, checkpoints: [] }; await this.saveDeployment(deployment); return deployment; } /** * Update resource state */ async updateResourceState( deploymentId: string, resource: Resource ): Promise<void> { const deployment = await this.getOrCreateDeployment(deploymentId); const resourceState = resource.toState(); deployment.resources[resource.id] = { id: resource.id, type: resource.type, name: resource.name, state: resourceState, properties: resource.getProperties(), outputs: resource.getOutputs(), metadata: resourceState.metadata, dependencies: resource.getDependencies().map((d: any) => d.resourceId) }; await this.saveDeployment(deployment); } /** * Update multiple resource states */ async updateResourceStates( deploymentId: string, resources: Resource[] ): Promise<void> { const deployment = await this.getOrCreateDeployment(deploymentId); for (const resource of resources) { const resourceState = resource.toState(); deployment.resources[resource.id] = { id: resource.id, type: resource.type, name: resource.name, state: resourceState, properties: resource.getProperties(), outputs: resource.getOutputs(), metadata: resourceState.metadata, dependencies: resource.getDependencies().map((d: any) => d.resourceId) }; } await this.saveDeployment(deployment); } /** * Get resource state */ async getResourceState( deploymentId: string, resourceId: string ): Promise<StateEntry | null> { const deployment = await this.loadDeployment(deploymentId); if (!deployment) return null; return deployment.resources[resourceId] || null; } /** * Remove resource from state */ async removeResourceState( deploymentId: string, resourceId: string ): Promise<void> { const deployment = await this.loadDeployment(deploymentId); if (!deployment) return; delete deployment.resources[resourceId]; await this.saveDeployment(deployment); } /** * Create a checkpoint */ async createCheckpoint( deploymentId: string, description: string ): Promise<Checkpoint> { const deployment = await this.getOrCreateDeployment(deploymentId); const checkpoint: Checkpoint = { id: `checkpoint-${Date.now()}`, timestamp: new Date().toISOString(), description, state: JSON.parse(JSON.stringify(deployment.resources)) // Deep copy }; deployment.checkpoints.push(checkpoint); // Keep only last 10 checkpoints if (deployment.checkpoints.length > 10) { deployment.checkpoints = deployment.checkpoints.slice(-10); } await this.saveDeployment(deployment); return checkpoint; } /** * Rollback to checkpoint */ async rollbackToCheckpoint( deploymentId: string, checkpointId: string ): Promise<void> { const deployment = await this.loadDeployment(deploymentId); if (!deployment) throw new Error(`Deployment ${deploymentId} not found`); const checkpoint = deployment.checkpoints.find(cp => cp.id === checkpointId); if (!checkpoint) throw new Error(`Checkpoint ${checkpointId} not found`); // Create a rollback checkpoint before applying await this.createCheckpoint(deploymentId, `Rollback to ${checkpointId}`); // Restore state from checkpoint deployment.resources = JSON.parse(JSON.stringify(checkpoint.state)); await this.saveDeployment(deployment); } /** * List all deployments */ async listDeployments(): Promise<string[]> { const files = await fs.readdir(this.options.directory); return files .filter(f => f.endsWith('.json')) .map(f => f.replace('.json', '')); } /** * Delete deployment */ async deleteDeployment(deploymentId: string): Promise<void> { const filePath = this.getStateFilePath(deploymentId); try { await fs.unlink(filePath); this.state.delete(deploymentId); } catch (error) { if ((error as any).code !== 'ENOENT') { throw error; } } } /** * Get deployment summary */ async getDeploymentSummary(deploymentId: string): Promise<any> { const deployment = await this.loadDeployment(deploymentId); if (!deployment) return null; const resourcesByType: Record<string, number> = {}; const resourcesByState: Record<string, number> = {}; for (const resource of Object.values(deployment.resources)) { // Count by type resourcesByType[resource.type] = (resourcesByType[resource.type] || 0) + 1; // Count by state const stateKey = String(resource.state); resourcesByState[stateKey] = (resourcesByState[stateKey] || 0) + 1; } return { id: deployment.id, name: deployment.name, version: deployment.version, createdAt: deployment.createdAt, updatedAt: deployment.updatedAt, totalResources: Object.keys(deployment.resources).length, resourcesByType, resourcesByState, checkpoints: deployment.checkpoints.length }; } /** * Export deployment state */ async exportDeployment(deploymentId: string): Promise<string> { const deployment = await this.loadDeployment(deploymentId); if (!deployment) throw new Error(`Deployment ${deploymentId} not found`); return JSON.stringify(deployment, null, 2); } /** * Import deployment state */ async importDeployment(data: string): Promise<DeploymentState> { const deployment = JSON.parse(data) as DeploymentState; // Validate structure if (!deployment.id || !deployment.name || !deployment.resources) { throw new Error('Invalid deployment data'); } // Save imported deployment await this.saveDeployment(deployment); return deployment; } /** * Get or create deployment */ private async getOrCreateDeployment(deploymentId: string): Promise<DeploymentState> { let deployment = await this.loadDeployment(deploymentId); if (!deployment) { deployment = await this.createDeployment(deploymentId, deploymentId); } return deployment; } /** * Get state file path */ private getStateFilePath(deploymentId: string): string { return join(this.options.directory, `${deploymentId}.json`); } /** * Lock deployment for concurrent access protection */ async lockDeployment(deploymentId: string): Promise<() => Promise<void>> { // Simple file-based locking const lockPath = join(this.options.directory, `${deploymentId}.lock`); // Try to create lock file const lockFd = await fs.open(lockPath, 'wx'); // Return unlock function return async () => { await lockFd.close(); await fs.unlink(lockPath); }; } /** * Get current state of all deployments */ async getCurrentState(): Promise<Record<string, DeploymentState>> { const result: Record<string, DeploymentState> = {}; for (const [id, deployment] of this.state) { result[id] = deployment; } return result; } /** * Get deployment by name */ async getDeployment(name: string): Promise<DeploymentState | null> { for (const [id, deployment] of this.state) { if (deployment.name === name) { return deployment; } } try { const files = await fs.readdir(this.options.directory); for (const file of files) { if (file.endsWith('.json')) { const deployment = await this.loadDeployment(file.replace('.json', '')); if (deployment && deployment.name === name) { return deployment; } } } } catch (error) { // Directory might not exist yet } return null; } /** * Get deployment state (alias for getDeployment) */ async getDeploymentState(name: string): Promise<DeploymentState | null> { return await this.getDeployment(name); } /** * Store a deployment plan */ async storePlan(plan: any): Promise<void> { if (!this.plans) { this.plans = new Map(); } this.plans.set(plan.id, plan); } /** * Get a stored plan */ async getPlan(planId: string): Promise<any | null> { if (!this.plans) { return null; } return this.plans.get(planId) || null; } /** * Update deployment state with execution result */ async updateDeploymentState(name: string, result: any): Promise<void> { let deployment = await this.getDeployment(name); if (!deployment) { deployment = await this.createDeployment(name, name); } deployment.updatedAt = new Date().toISOString(); deployment.version++; if (result.resources) { if (Array.isArray(result.resources)) { const resourceMap: Record<string, StateEntry> = {}; result.resources.forEach((res: any) => { resourceMap[res.id || res.name] = res; }); deployment.resources = resourceMap; } else { deployment.resources = result.resources; } } await this.saveDeployment(deployment); } } /** * Global state store instance */ export const stateStore = new ResourceStateStore();

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