Skip to main content
Glama

Cut-Copy-Paste Clipboard Server

clipboard-tools.ts•14.6 kB
import { FileHandler } from '../lib/file-handler.js'; import { ClipboardManager } from '../lib/clipboard-manager.js'; import { OperationLogger } from '../lib/operation-logger.js'; import { SessionManager } from '../lib/session-manager.js'; import type { PathAccessControl } from '../lib/path-access-control.js'; /** * Clipboard tools for MCP server * Provides copy, cut, paste, undo, and history operations */ export class ClipboardTools { constructor( private fileHandler: FileHandler, private clipboardManager: ClipboardManager, private operationLogger: OperationLogger, private sessionManager: SessionManager, private pathAccessControl?: PathAccessControl ) {} /** * Copy lines from a file to clipboard */ async copyLines( sessionId: string, filePath: string, startLine: number, endLine: number ): Promise<{ success: boolean; content: string; lines: string[]; message: string; }> { try { // Validate session const session = this.sessionManager.getSession(sessionId); if (!session) { throw new Error('Invalid session'); } const normalizedFilePath = this.requireFilePath(filePath); this.validatePathAccess(normalizedFilePath); const normalizedStartLine = this.requirePositiveLineNumber(startLine, 'start_line'); const normalizedEndLine = this.requirePositiveLineNumber(endLine, 'end_line'); if (normalizedStartLine > normalizedEndLine) { throw new Error('Invalid line range: start line must be <= end line'); } // Read lines from file const result = this.fileHandler.readLines( normalizedFilePath, normalizedStartLine, normalizedEndLine ); // Store in clipboard this.clipboardManager.setClipboard(sessionId, { content: result.content, sourceFile: normalizedFilePath, startLine: normalizedStartLine, endLine: normalizedEndLine, operationType: 'copy', }); // Log operation this.operationLogger.logCopy(sessionId, { sourceFile: normalizedFilePath, startLine: normalizedStartLine, endLine: normalizedEndLine, content: result.content, }); return { success: true, content: result.content, lines: result.lines, message: `Copied ${result.lines.length} line(s) from ${filePath}:${startLine}-${endLine}`, }; } catch (error) { throw new Error(`Copy failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * Cut lines from a file to clipboard */ async cutLines( sessionId: string, filePath: string, startLine: number, endLine: number ): Promise<{ success: boolean; content: string; lines: string[]; message: string; }> { try { // Validate session const session = this.sessionManager.getSession(sessionId); if (!session) { throw new Error('Invalid session'); } const normalizedFilePath = this.requireFilePath(filePath); this.validatePathAccess(normalizedFilePath); const normalizedStartLine = this.requirePositiveLineNumber(startLine, 'start_line'); const normalizedEndLine = this.requirePositiveLineNumber(endLine, 'end_line'); if (normalizedStartLine > normalizedEndLine) { throw new Error('Invalid line range: start line must be <= end line'); } // Capture original file content BEFORE any modifications const originalFileSnapshot = this.fileHandler.getFileSnapshot(normalizedFilePath); // Read lines to be cut const result = this.fileHandler.readLines( normalizedFilePath, normalizedStartLine, normalizedEndLine ); // Store in clipboard with original file content for undo this.clipboardManager.setClipboard(sessionId, { content: result.content, sourceFile: normalizedFilePath, startLine: normalizedStartLine, endLine: normalizedEndLine, operationType: 'cut', cutSourceOriginalContent: originalFileSnapshot.content, // Store ORIGINAL state }); // Log operation before deleting this.operationLogger.logCut(sessionId, { sourceFile: normalizedFilePath, startLine: normalizedStartLine, endLine: normalizedEndLine, content: result.content, }); // Delete lines from source file this.fileHandler.deleteLines(normalizedFilePath, normalizedStartLine, normalizedEndLine); return { success: true, content: result.content, lines: result.lines, message: `Cut ${result.lines.length} line(s) from ${filePath}:${startLine}-${endLine}`, }; } catch (error) { throw new Error(`Cut failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * Paste clipboard content to one or more locations */ async pasteLines( sessionId: string, targets: Array<{ file_path: string; target_line: number }> ): Promise<{ success: boolean; pastedTo: Array<{ file: string; line: number }>; message: string; }> { try { // Validate session const session = this.sessionManager.getSession(sessionId); if (!session) { throw new Error('Invalid session'); } const normalizedTargets = this.normalizePasteTargets(targets); // Get clipboard content const clipboard = this.clipboardManager.getClipboard(sessionId); if (!clipboard) { throw new Error('Clipboard is empty'); } // Validate path access for all targets for (const target of normalizedTargets) { this.validatePathAccess(target.filePath); } // Prepare paste targets with original content for undo const pasteTargets = normalizedTargets.map((target) => { try { const snapshot = this.fileHandler.getFileSnapshot(target.filePath); return { filePath: target.filePath, targetLine: target.targetLine, originalContent: snapshot.content, }; } catch (error) { throw new Error( `Failed to read ${target.filePath}: ${error instanceof Error ? error.message : String(error)}` ); } }); // Perform paste operations const pastedTo: Array<{ file: string; line: number }> = []; for (const target of normalizedTargets) { try { this.fileHandler.insertLines( target.filePath, target.targetLine, clipboard.content.split('\n') ); pastedTo.push({ file: target.filePath, line: target.targetLine, }); } catch (error) { // If any paste fails, we should ideally rollback throw new Error( `Failed to paste to ${target.filePath}:${target.targetLine}: ${ error instanceof Error ? error.message : String(error) }` ); } } // If this paste came from a cut operation, use the stored original content let cutSourceFile: string | undefined; let cutSourceContent: string | undefined; if (clipboard.operationType === 'cut' && clipboard.cutSourceOriginalContent) { cutSourceFile = clipboard.sourceFile; cutSourceContent = clipboard.cutSourceOriginalContent; } // Log paste operation with history for undo this.operationLogger.logPaste(sessionId, { targets: pasteTargets, content: clipboard.content, cutSourceFile, cutSourceContent, }); return { success: true, pastedTo, message: `Pasted to ${pastedTo.length} location(s)`, }; } catch (error) { throw new Error(`Paste failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * Show current clipboard content */ async showClipboard(sessionId: string): Promise<{ hasContent: boolean; content?: string; sourceFile?: string; startLine?: number; endLine?: number; operationType?: 'copy' | 'cut'; copiedAt?: number; }> { try { // Validate session const session = this.sessionManager.getSession(sessionId); if (!session) { throw new Error('Invalid session'); } const clipboard = this.clipboardManager.getClipboard(sessionId); if (!clipboard) { return { hasContent: false, }; } return { hasContent: true, content: clipboard.content, sourceFile: clipboard.sourceFile, startLine: clipboard.startLine, endLine: clipboard.endLine, operationType: clipboard.operationType, copiedAt: clipboard.copiedAt, }; } catch (error) { throw new Error( `Failed to show clipboard: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Undo the last paste operation */ async undoLastPaste(sessionId: string): Promise<{ success: boolean; restoredFiles: Array<{ file: string; line: number }>; message: string; }> { try { // Validate session const session = this.sessionManager.getSession(sessionId); if (!session) { throw new Error('Invalid session'); } // Get last paste operation const lastPaste = this.operationLogger.getLastPaste(sessionId); if (!lastPaste) { throw new Error('No paste operation to undo'); } // Restore original content for each target const restoredFiles: Array<{ file: string; line: number }> = []; for (const target of lastPaste.targets) { try { // Simply overwrite the entire file with original content // This is the most reliable way to restore the exact state const fs = await import('fs'); fs.writeFileSync(target.filePath, target.originalContent, 'utf-8'); restoredFiles.push({ file: target.filePath, line: target.targetLine, }); } catch (error) { throw new Error( `Failed to restore ${target.filePath}: ${ error instanceof Error ? error.message : String(error) }` ); } } // If this paste came from a cut operation, restore the source file if (lastPaste.cutSourceFile && lastPaste.cutSourceContent) { try { const fs = await import('fs'); fs.writeFileSync(lastPaste.cutSourceFile, lastPaste.cutSourceContent, 'utf-8'); restoredFiles.push({ file: lastPaste.cutSourceFile, line: 0, // Source file restored entirely, not a specific line }); } catch (error) { throw new Error( `Failed to restore cut source file ${lastPaste.cutSourceFile}: ${ error instanceof Error ? error.message : String(error) }` ); } } // Log undo operation this.operationLogger.logUndo(sessionId, lastPaste.operationId); return { success: true, restoredFiles, message: `Undone paste operation, restored ${restoredFiles.length} file(s)`, }; } catch (error) { throw new Error(`Undo failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get operation history for a session */ async getOperationHistory( sessionId: string, limit: number = 10 ): Promise<{ operations: Array<{ operationId: number; operationType: string; timestamp: number; details: { sourceFile?: string; startLine?: number; endLine?: number; targetFile?: string; targetLine?: number; }; }>; }> { try { // Validate session const session = this.sessionManager.getSession(sessionId); if (!session) { throw new Error('Invalid session'); } const limitToUse = limit === undefined ? 10 : this.requirePositiveLineNumber(limit, 'limit'); const history = this.operationLogger.getHistory(sessionId, limitToUse); return { operations: history.map((op) => ({ operationId: op.operationId, operationType: op.operationType, timestamp: op.timestamp, details: { sourceFile: op.sourceFile, startLine: op.startLine, endLine: op.endLine, targetFile: op.targetFile, targetLine: op.targetLine, }, })), }; } catch (error) { throw new Error( `Failed to get history: ${error instanceof Error ? error.message : String(error)}` ); } } private validatePathAccess(filePath: string): void { if (this.pathAccessControl && !this.pathAccessControl.isPathAllowed(filePath)) { throw new Error(`Access denied: path not in allowlist: ${filePath}`); } } private requireFilePath(filePath: unknown, field: string = 'file_path'): string { if (typeof filePath !== 'string' || filePath.trim() === '') { throw new Error(`${field} must be a non-empty string`); } return filePath; } private requirePositiveLineNumber(value: unknown, field: string): number { if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value)) { throw new Error(`${field} must be a positive integer`); } if (value <= 0) { throw new Error(`${field} must be a positive integer`); } return value; } private requireNonNegativeLineNumber(value: unknown, field: string): number { if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value)) { throw new Error(`${field} must be an integer >= 0`); } if (value < 0) { throw new Error(`${field} must be an integer >= 0`); } return value; } private normalizePasteTargets( targets: Array<{ file_path: string; target_line: number }> ): Array<{ filePath: string; targetLine: number }> { if (!Array.isArray(targets) || targets.length === 0) { throw new Error('targets must be a non-empty array'); } return targets.map((target, index) => { const filePath = this.requireFilePath(target?.file_path, `targets[${index}].file_path`); const targetLine = this.requireNonNegativeLineNumber( target?.target_line, `targets[${index}].target_line` ); return { filePath, targetLine, }; }); } }

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