Skip to main content
Glama

MCP Bridge Server

persistentStorage.ts8.66 kB
import { promises as fs } from 'fs'; import { join, dirname } from 'path'; import { createHash } from 'crypto'; import { StorageEncryption } from './encryption.js'; import { StorageOptions, StorageMetadata, StorageData, StorageError, StorageErrorType, StorageEvent, StorageEventType, ValidationResult } from './types.js'; /** * Default storage options */ const DEFAULT_OPTIONS: StorageOptions = { directory: '.mcp-storage', encryption: { enabled: true, algorithm: 'aes-256-gcm' }, atomicWrites: true, backupEnabled: true, maxBackups: 5 }; export class PersistentStorage { private options: StorageOptions; private encryption?: StorageEncryption; private eventHandlers: ((event: StorageEvent) => void)[] = []; constructor(options: Partial<StorageOptions> = {}) { this.options = { ...DEFAULT_OPTIONS, ...options }; } /** * Get storage directory path */ public get directory(): string { return this.options.directory; } /** * Initialize storage */ public async initialize(): Promise<void> { try { // Create storage directory if it doesn't exist await fs.mkdir(this.options.directory, { recursive: true }); // Initialize encryption if enabled if (this.options.encryption?.enabled) { this.encryption = new StorageEncryption( this.options.directory, { algorithm: this.options.encryption.algorithm } ); await this.encryption.initialize(); } // Emit initialization event this.emitEvent({ type: StorageEventType.READ, path: this.options.directory, timestamp: new Date() }); } catch (error) { throw new StorageError( StorageErrorType.PERMISSION_DENIED, 'Failed to initialize storage', this.options.directory, error ); } } /** * Read data from storage */ public async read<T>(path: string): Promise<T> { const fullPath = this.getFullPath(path); try { // Read file const data = await fs.readFile(fullPath); // Decrypt if enabled const decrypted = this.encryption ? await this.encryption.decrypt(JSON.parse(data.toString())) : data; // Parse JSON const parsed = JSON.parse(decrypted.toString()); // Validate checksum if (!this.validateChecksum(parsed)) { throw new StorageError( StorageErrorType.CHECKSUM_MISMATCH, 'Data corruption detected' ); } // Emit read event this.emitEvent({ type: StorageEventType.READ, path, timestamp: new Date(), metadata: parsed.metadata }); return parsed.data; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { throw new StorageError( StorageErrorType.FILE_NOT_FOUND, 'File not found', path ); } throw new StorageError( StorageErrorType.INVALID_DATA, 'Failed to read data', path, error ); } } /** * Write data to storage */ public async write<T>(path: string, data: T): Promise<void> { const fullPath = this.getFullPath(path); try { // Create metadata const metadata: StorageMetadata = { version: '1.0', lastModified: new Date(), checksum: this.calculateChecksum(data) }; // Prepare storage data const storageData: StorageData = { version: '1.0', metadata, data }; // Convert to JSON const json = JSON.stringify(storageData); // Encrypt if enabled const encrypted = this.encryption ? JSON.stringify(await this.encryption.encrypt(Buffer.from(json))) : json; if (this.options.atomicWrites) { // Write to temporary file first const tempPath = `${fullPath}.tmp`; await fs.writeFile(tempPath, encrypted); // Rename to final path (atomic operation) await fs.rename(tempPath, fullPath); } else { // Write directly await fs.writeFile(fullPath, encrypted); } // Create backup if enabled if (this.options.backupEnabled) { await this.createBackup(path, encrypted); } // Emit write event this.emitEvent({ type: StorageEventType.WRITE, path, timestamp: new Date(), metadata }); } catch (error) { throw new StorageError( StorageErrorType.INVALID_DATA, 'Failed to write data', path, error ); } } /** * Update data in storage */ public async update<T>(path: string, updater: (data: T) => T): Promise<void> { try { // Read current data const current = await this.read<T>(path); // Apply update const updated = updater(current); // Write back await this.write(path, updated); } catch (error) { if (error instanceof StorageError) { throw error; } throw new StorageError( StorageErrorType.INVALID_DATA, 'Failed to update data', path, error ); } } /** * Delete data from storage */ public async delete(path: string): Promise<void> { const fullPath = this.getFullPath(path); try { await fs.unlink(fullPath); // Emit delete event this.emitEvent({ type: StorageEventType.DELETE, path, timestamp: new Date() }); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { throw new StorageError( StorageErrorType.FILE_NOT_FOUND, 'File not found', path ); } throw new StorageError( StorageErrorType.INVALID_DATA, 'Failed to delete data', path, error ); } } /** * Create backup of data */ private async createBackup(path: string, data: string): Promise<void> { if (!this.options.backupEnabled) return; const backupDir = join(this.options.directory, 'backups'); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = join(backupDir, `${path}.${timestamp}.bak`); try { // Create backup directory await fs.mkdir(backupDir, { recursive: true }); // Write backup await fs.writeFile(backupPath, data); // Clean up old backups if (this.options.maxBackups) { const backups = await fs.readdir(backupDir); const pathBackups = backups.filter(b => b.startsWith(path)); if (pathBackups.length > this.options.maxBackups) { // Sort by timestamp (oldest first) pathBackups.sort(); // Delete oldest backups const toDelete = pathBackups.slice(0, pathBackups.length - this.options.maxBackups); await Promise.all( toDelete.map(b => fs.unlink(join(backupDir, b))) ); } } } catch (error) { // Don't throw on backup failure, just emit error event this.emitEvent({ type: StorageEventType.ERROR, path, timestamp: new Date(), error: 'Failed to create backup' }); } } /** * Calculate checksum for data */ private calculateChecksum(data: any): string { return createHash('sha256') .update(JSON.stringify(data)) .digest('hex'); } /** * Validate checksum in storage data */ private validateChecksum(storageData: StorageData): boolean { const calculated = this.calculateChecksum(storageData.data); return calculated === storageData.metadata.checksum; } /** * Get full path for storage item */ private getFullPath(path: string): string { const fullPath = join(this.options.directory, path); const parentDir = dirname(fullPath); // Ensure path is within storage directory if (!fullPath.startsWith(this.options.directory)) { throw new StorageError( StorageErrorType.PERMISSION_DENIED, 'Path outside storage directory', path ); } return fullPath; } /** * Add event handler */ public onEvent(handler: (event: StorageEvent) => void): void { this.eventHandlers.push(handler); } /** * Emit storage event */ private emitEvent(event: StorageEvent): void { this.eventHandlers.forEach(handler => handler(event)); } /** * Clean up resources */ public async dispose(): Promise<void> { if (this.encryption) { await this.encryption.dispose(); } this.eventHandlers = []; } }

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/glassBead-tc/SubspaceDomain'

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