Skip to main content
Glama

Model Context Protocol (MCP) Server

by infinyte
persistence.js9.2 kB
/** * Persistence Service * * Handles data persistence for the MCP server, including: * - Graceful shutdown data synchronization * - Backup and recovery operations * - Data migration between versions */ const databaseService = require('./database'); const stateManager = require('./stateManager'); const fs = require('fs'); const path = require('path'); class PersistenceService { constructor() { this.backupDir = path.join(__dirname, '../../backups'); this.isShuttingDown = false; this.syncInProgress = false; // Ensure backup directory exists if (!fs.existsSync(this.backupDir)) { fs.mkdirSync(this.backupDir, { recursive: true }); } } /** * Initialize the persistence service * @returns {Promise<void>} */ async initialize() { // Register shutdown handlers process.on('SIGINT', () => this.handleShutdown('SIGINT')); process.on('SIGTERM', () => this.handleShutdown('SIGTERM')); // Handle uncaught exceptions process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); this.handleShutdown('uncaughtException', error); }); // Handle unhandled rejections process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); }); console.log('Persistence service initialized'); } /** * Handle application shutdown * @param {string} signal - Shutdown signal * @param {Error} [error] - Error that caused shutdown */ async handleShutdown(signal, error) { // Prevent multiple shutdown handlers from running simultaneously if (this.isShuttingDown) { return; } this.isShuttingDown = true; console.log(`\n🛑 Shutdown initiated (${signal})`); if (error) { console.error('Shutdown due to error:', error); } try { // Sync state to database console.log('💾 Syncing state to database before shutdown...'); await this.syncStateToDatabase(); console.log('✅ State saved successfully'); // Create backup if database is connected if (databaseService.isConnected && !databaseService.useFallback) { console.log('📦 Creating backup...'); await this.createBackup(); console.log('✅ Backup created successfully'); } } catch (syncError) { console.error('❌ Error during shutdown sync:', syncError); } // Close database connection try { console.log('🔌 Closing database connection...'); await databaseService.close(); console.log('✅ Database connection closed'); } catch (dbError) { console.error('❌ Error closing database connection:', dbError); } console.log('👋 Shutdown complete'); // Exit with appropriate code process.exit(error ? 1 : 0); } /** * Sync state to database * @returns {Promise<boolean>} Success status */ async syncStateToDatabase() { if (this.syncInProgress) { return false; } this.syncInProgress = true; try { // Check if stateManager is initialized if (!stateManager.initialized) { await stateManager.initialize(); } // Save state const success = await stateManager.saveStateToDb(); this.syncInProgress = false; return success; } catch (error) { this.syncInProgress = false; console.error('Error syncing state to database:', error); return false; } } /** * Create a backup of current state * @returns {Promise<string>} Backup file path */ async createBackup() { if (databaseService.useFallback) { throw new Error('Cannot create backup when using fallback store'); } try { // Get current state const tools = await databaseService.getAllTools(); const apiConfigs = await databaseService.getConfigurationsByCategory('api_key'); const serverConfigs = await databaseService.getConfigurationsByCategory('server'); // Create backup object (excluding sensitive data) const backup = { version: '1.0.0', timestamp: new Date().toISOString(), tools: tools.map(tool => ({ name: tool.name, description: tool.description, version: tool.version, category: tool.category, parameters: tool.parameters, enabled: tool.enabled, implementation: tool.implementation, implementationPath: tool.implementationPath, metadata: { createdBy: tool.metadata?.createdBy, createdAt: tool.metadata?.createdAt, updatedAt: tool.metadata?.updatedAt, lastUsed: tool.metadata?.lastUsed, usageCount: tool.metadata?.usageCount } })), configurations: [ ...(serverConfigs || []).map(config => ({ key: config.key, category: config.category, description: config.description, isEncrypted: config.isEncrypted // Excluding values for security })), ...(apiConfigs || []).map(config => ({ key: config.key, category: config.category, description: config.description, isEncrypted: config.isEncrypted // Excluding values for security })) ] }; // Generate backup file name const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, ''); const fileName = `mcp_backup_${timestamp}.json`; const filePath = path.join(this.backupDir, fileName); // Write backup to file fs.writeFileSync(filePath, JSON.stringify(backup, null, 2)); return filePath; } catch (error) { console.error('Error creating backup:', error); throw error; } } /** * Restore from a backup file * @param {string} backupFile - Path to backup file * @returns {Promise<boolean>} Success status */ async restoreFromBackup(backupFile) { try { // Read backup file const backupData = JSON.parse(fs.readFileSync(backupFile, 'utf8')); // Validate backup version if (!backupData.version) { throw new Error('Invalid backup file: missing version'); } // Restore tools if (Array.isArray(backupData.tools)) { for (const tool of backupData.tools) { await databaseService.saveToolDefinition(tool); } } // Restore configurations (excluding values that need to be set manually) if (Array.isArray(backupData.configurations)) { for (const config of backupData.configurations) { // Skip API keys (these should be set manually) if (config.category === 'api_key') { continue; } // Get existing config to preserve value const existingConfig = await databaseService.getConfiguration(config.key); if (existingConfig) { await databaseService.updateConfiguration( config.key, existingConfig, config.isEncrypted ); } } } return true; } catch (error) { console.error('Error restoring from backup:', error); return false; } } /** * List available backups * @returns {Array} Backup files with metadata */ listBackups() { try { // Get all backup files const files = fs.readdirSync(this.backupDir) .filter(file => file.endsWith('.json') && file.startsWith('mcp_backup_')); // Get metadata for each file return files.map(file => { const filePath = path.join(this.backupDir, file); const stats = fs.statSync(filePath); try { // Try to get version and timestamp from file const data = JSON.parse(fs.readFileSync(filePath, 'utf8')); return { fileName: file, path: filePath, size: stats.size, created: stats.birthtime, modified: stats.mtime, version: data.version, timestamp: data.timestamp, toolCount: Array.isArray(data.tools) ? data.tools.length : 0, configCount: Array.isArray(data.configurations) ? data.configurations.length : 0 }; } catch (error) { // If we can't parse the file, return basic info return { fileName: file, path: filePath, size: stats.size, created: stats.birthtime, modified: stats.mtime, error: 'Could not parse backup file' }; } }).sort((a, b) => { // Sort by creation date, newest first return new Date(b.created) - new Date(a.created); }); } catch (error) { console.error('Error listing backups:', error); return []; } } } // Create a singleton instance const persistenceService = new PersistenceService(); module.exports = persistenceService;

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/infinyte/mcp-server'

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