Skip to main content
Glama

Cut-Copy-Paste Clipboard Server

database.ts•9.75 kB
import Database from 'better-sqlite3'; import { dirname, join, basename } from 'path'; import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs'; import { randomBytes } from 'crypto'; /** * Database initialization and management for the MCP Cut-Copy-Paste Clipboard Server */ export class DatabaseManager { private db: Database.Database; private encryptionKey: Buffer; private keyPath: string | null = null; private readonly isInMemory: boolean; constructor(dbPath?: string) { // Default to .mcp-clipboard in user's home directory const defaultPath = join( process.env.HOME || process.env.USERPROFILE || '.', '.mcp-clipboard', 'clipboard.db' ); const finalPath = dbPath || defaultPath; this.isInMemory = finalPath === ':memory:'; // Ensure directory and encryption key exist for on-disk databases if (!this.isInMemory) { const dir = dirname(finalPath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true, mode: 0o700 }); } try { chmodSync(dir, 0o700); } catch { // Ignore chmod errors (e.g., on Windows or restricted filesystems) } const keyFilename = basename(finalPath) === 'clipboard.db' ? 'clipboard.key' : `${basename(finalPath)}.key`; this.keyPath = join(dir, keyFilename); this.encryptionKey = this.loadOrCreateKey(this.keyPath); } else { this.encryptionKey = randomBytes(32); } this.db = new Database(finalPath); if (!this.isInMemory) { try { chmodSync(finalPath, 0o600); } catch { // Ignore chmod errors (e.g., on Windows or restricted filesystems) } } this.db.pragma('journal_mode = WAL'); // Better concurrency this.db.pragma('foreign_keys = ON'); // Enforce foreign key constraints this.initializeSchema(); } /** * Initialize database schema with all required tables */ private initializeSchema(): void { // Sessions table this.db.exec(` CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, last_activity INTEGER NOT NULL ) `); // Clipboard buffer table (one per session) this.db.exec(` CREATE TABLE IF NOT EXISTS clipboard_buffer ( session_id TEXT PRIMARY KEY, content TEXT NOT NULL, source_file TEXT NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, copied_at INTEGER NOT NULL, operation_type TEXT NOT NULL CHECK(operation_type IN ('copy', 'cut')), cut_source_original_content TEXT, encrypted_payload TEXT, encryption_iv TEXT, encryption_tag TEXT, encryption_version INTEGER DEFAULT 1, FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE ) `); // Operations log table this.db.exec(` CREATE TABLE IF NOT EXISTS operations_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, operation_type TEXT NOT NULL CHECK(operation_type IN ('copy', 'cut', 'paste', 'undo')), timestamp INTEGER NOT NULL, source_file TEXT, source_start_line INTEGER, source_end_line INTEGER, target_file TEXT, target_line INTEGER, content_snapshot TEXT, undoable INTEGER DEFAULT 1 CHECK(undoable IN (0, 1)), FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE ) `); // Paste history table (for undo support) this.db.exec(` CREATE TABLE IF NOT EXISTS paste_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, operation_log_id INTEGER NOT NULL, file_path TEXT NOT NULL, original_content TEXT NOT NULL, modified_content TEXT NOT NULL, paste_line INTEGER NOT NULL, timestamp INTEGER NOT NULL, undone INTEGER DEFAULT 0 CHECK(undone IN (0, 1)), cut_source_file TEXT, cut_source_content TEXT, encrypted_payload TEXT, encryption_iv TEXT, encryption_tag TEXT, encryption_version INTEGER DEFAULT 1, FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE, FOREIGN KEY (operation_log_id) REFERENCES operations_log(id) ON DELETE CASCADE ) `); // Run migrations to handle schema updates AFTER all tables are created this.runMigrations(); // Create indexes for common queries this.db.exec(` CREATE INDEX IF NOT EXISTS idx_sessions_last_activity ON sessions(last_activity); CREATE INDEX IF NOT EXISTS idx_operations_log_session ON operations_log(session_id, timestamp DESC); CREATE INDEX IF NOT EXISTS idx_paste_history_session ON paste_history(session_id, timestamp DESC); CREATE INDEX IF NOT EXISTS idx_paste_history_undone ON paste_history(session_id, undone, timestamp DESC); `); } /** * Run database migrations to update schema */ private runMigrations(): void { // Check if cut_source_original_content column exists in clipboard_buffer const tableInfo = this.db.pragma('table_info(clipboard_buffer)') as Array<{ name: string; type: string; }>; const hasColumn = tableInfo.some((col) => col.name === 'cut_source_original_content'); if (!hasColumn) { // Add the column if it doesn't exist this.db.exec(` ALTER TABLE clipboard_buffer ADD COLUMN cut_source_original_content TEXT; `); } const hasEncryptedPayload = tableInfo.some((col) => col.name === 'encrypted_payload'); if (!hasEncryptedPayload) { this.db.exec(` ALTER TABLE clipboard_buffer ADD COLUMN encrypted_payload TEXT; `); } const hasEncryptionIv = tableInfo.some((col) => col.name === 'encryption_iv'); if (!hasEncryptionIv) { this.db.exec(` ALTER TABLE clipboard_buffer ADD COLUMN encryption_iv TEXT; `); } const hasEncryptionTag = tableInfo.some((col) => col.name === 'encryption_tag'); if (!hasEncryptionTag) { this.db.exec(` ALTER TABLE clipboard_buffer ADD COLUMN encryption_tag TEXT; `); } const hasEncryptionVersion = tableInfo.some((col) => col.name === 'encryption_version'); if (!hasEncryptionVersion) { this.db.exec(` ALTER TABLE clipboard_buffer ADD COLUMN encryption_version INTEGER DEFAULT 1; `); } // Migrate paste_history table to add encryption columns const pasteHistoryInfo = this.db.pragma('table_info(paste_history)') as Array<{ name: string; type: string; }>; const hasEncryptedPayloadPH = pasteHistoryInfo.some((col) => col.name === 'encrypted_payload'); if (!hasEncryptedPayloadPH) { this.db.exec(` ALTER TABLE paste_history ADD COLUMN encrypted_payload TEXT; `); } const hasEncryptionIvPH = pasteHistoryInfo.some((col) => col.name === 'encryption_iv'); if (!hasEncryptionIvPH) { this.db.exec(` ALTER TABLE paste_history ADD COLUMN encryption_iv TEXT; `); } const hasEncryptionTagPH = pasteHistoryInfo.some((col) => col.name === 'encryption_tag'); if (!hasEncryptionTagPH) { this.db.exec(` ALTER TABLE paste_history ADD COLUMN encryption_tag TEXT; `); } const hasEncryptionVersionPH = pasteHistoryInfo.some( (col) => col.name === 'encryption_version' ); if (!hasEncryptionVersionPH) { this.db.exec(` ALTER TABLE paste_history ADD COLUMN encryption_version INTEGER DEFAULT 1; `); } } /** * Get the underlying database connection */ getConnection(): Database.Database { return this.db; } /** * Close the database connection */ close(): void { this.db.close(); } /** * Begin a transaction */ beginTransaction(): void { this.db.exec('BEGIN TRANSACTION'); } /** * Commit a transaction */ commit(): void { this.db.exec('COMMIT'); } /** * Rollback a transaction */ rollback(): void { this.db.exec('ROLLBACK'); } /** * Execute a function within a transaction */ transaction<T>(fn: () => T): T { try { this.beginTransaction(); const result = fn(); this.commit(); return result; } catch (error) { this.rollback(); throw error; } } /** * Get encryption key used for clipboard payloads */ getEncryptionKey(): Buffer { return this.encryptionKey; } /** * Get path to the encryption key file (if applicable) */ getEncryptionKeyPath(): string | null { return this.keyPath; } private loadOrCreateKey(path: string): Buffer { if (!existsSync(path)) { const key = randomBytes(32); writeFileSync(path, key.toString('base64'), { mode: 0o600 }); try { chmodSync(path, 0o600); } catch { // Ignore chmod errors (e.g., on Windows or restricted filesystems) } return key; } const raw = readFileSync(path, 'utf-8').trim(); if (!raw) { throw new Error(`Encryption key at ${path} is empty`); } let key: Buffer; try { key = Buffer.from(raw, 'base64'); } catch (error) { throw new Error(`Failed to parse encryption key at ${path}: ${error}`); } if (key.length !== 32) { throw new Error(`Encryption key at ${path} must be 32 bytes when decoded`); } try { chmodSync(path, 0o600); } catch { // Ignore chmod errors (e.g., on Windows or restricted filesystems) } return key; } }

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/Pr0j3c7t0dd-Ltd/cut-copy-paste-mcp'

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