Skip to main content
Glama

Cut-Copy-Paste Clipboard Server

clipboard-tools.test.ts•22.3 kB
import { ClipboardTools } from '../clipboard-tools.js'; 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 { DatabaseManager } from '../../lib/database.js'; import { PathAccessControl } from '../../lib/path-access-control.js'; import { existsSync, rmSync, writeFileSync, mkdirSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; describe('ClipboardTools', () => { let dbPath: string; let testDir: string; let dbManager: DatabaseManager; let fileHandler: FileHandler; let clipboardManager: ClipboardManager; let operationLogger: OperationLogger; let sessionManager: SessionManager; let clipboardTools: ClipboardTools; let testSessionId: string; beforeEach(() => { const uniqueId = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 9)}`; dbPath = join(tmpdir(), `test-clipboard-${uniqueId}.db`); testDir = join(tmpdir(), `test-files-${uniqueId}`); mkdirSync(testDir, { recursive: true }); dbManager = new DatabaseManager(dbPath); fileHandler = new FileHandler(); clipboardManager = new ClipboardManager(dbManager); operationLogger = new OperationLogger(dbManager); sessionManager = new SessionManager(dbManager); clipboardTools = new ClipboardTools( fileHandler, clipboardManager, operationLogger, sessionManager ); // Create test session testSessionId = sessionManager.createSession(); }); afterEach(() => { if (dbManager) { dbManager.close(); const keyPath = dbManager.getEncryptionKeyPath(); if (keyPath && existsSync(keyPath)) { rmSync(keyPath); } } if (existsSync(dbPath)) { rmSync(dbPath); } if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } }); describe('copyLines', () => { it('should copy lines from file to clipboard', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\nline 4\nline 5\n'); const result = await clipboardTools.copyLines(testSessionId, filePath, 2, 4); expect(result.success).toBe(true); expect(result.lines).toEqual(['line 2', 'line 3', 'line 4']); expect(result.content).toBe('line 2\nline 3\nline 4'); }); it('should store content in clipboard', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\n'); await clipboardTools.copyLines(testSessionId, filePath, 1, 2); const clipboard = clipboardManager.getClipboard(testSessionId); expect(clipboard?.content).toBe('line 1\nline 2'); expect(clipboard?.operationType).toBe('copy'); }); it('should log copy operation', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); await clipboardTools.copyLines(testSessionId, filePath, 1, 1); const history = operationLogger.getHistory(testSessionId); expect(history.length).toBe(1); expect(history[0].operationType).toBe('copy'); }); it('should validate integer line numbers', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); await expect(clipboardTools.copyLines(testSessionId, filePath, 1.2, 2)).rejects.toThrow( 'Copy failed: start_line must be a positive integer' ); }); it('should throw error for invalid session', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\n'); await expect(clipboardTools.copyLines('invalid-session', filePath, 1, 1)).rejects.toThrow( 'Invalid session' ); }); it('should not modify source file', async () => { const filePath = join(testDir, 'test.txt'); const originalContent = 'line 1\nline 2\nline 3\n'; writeFileSync(filePath, originalContent); await clipboardTools.copyLines(testSessionId, filePath, 1, 2); const snapshot = fileHandler.getFileSnapshot(filePath); expect(snapshot.content).toBe(originalContent); }); }); describe('cutLines', () => { it('should cut lines from file to clipboard', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\nline 4\n'); const result = await clipboardTools.cutLines(testSessionId, filePath, 2, 3); expect(result.success).toBe(true); expect(result.lines).toEqual(['line 2', 'line 3']); }); it('should remove lines from source file', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\n'); await clipboardTools.cutLines(testSessionId, filePath, 2, 2); const snapshot = fileHandler.getFileSnapshot(filePath); expect(snapshot.content).toBe('line 1\nline 3\n'); }); it('should store content in clipboard with cut type', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); await clipboardTools.cutLines(testSessionId, filePath, 1, 1); const clipboard = clipboardManager.getClipboard(testSessionId); expect(clipboard?.operationType).toBe('cut'); }); it('should log cut operation', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\n'); await clipboardTools.cutLines(testSessionId, filePath, 1, 1); const history = operationLogger.getHistory(testSessionId); expect(history[0].operationType).toBe('cut'); }); it('should validate integer line numbers for cuts', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); await expect(clipboardTools.cutLines(testSessionId, filePath, 1, 2.7)).rejects.toThrow( 'Cut failed: end_line must be a positive integer' ); }); }); describe('pasteLines', () => { it('should paste clipboard content to single target', async () => { const sourceFile = join(testDir, 'source.txt'); const targetFile = join(testDir, 'target.txt'); writeFileSync(sourceFile, 'copied line\n'); writeFileSync(targetFile, 'line 1\nline 2\n'); await clipboardTools.copyLines(testSessionId, sourceFile, 1, 1); const result = await clipboardTools.pasteLines(testSessionId, [ { file_path: targetFile, target_line: 2 }, ]); expect(result.success).toBe(true); expect(result.pastedTo.length).toBe(1); const snapshot = fileHandler.getFileSnapshot(targetFile); expect(snapshot.content).toContain('copied line'); }); it('should paste to multiple targets', async () => { const sourceFile = join(testDir, 'source.txt'); const target1 = join(testDir, 'target1.txt'); const target2 = join(testDir, 'target2.txt'); writeFileSync(sourceFile, 'pasted content\n'); writeFileSync(target1, 'file 1\n'); writeFileSync(target2, 'file 2\n'); await clipboardTools.copyLines(testSessionId, sourceFile, 1, 1); const result = await clipboardTools.pasteLines(testSessionId, [ { file_path: target1, target_line: 1 }, { file_path: target2, target_line: 1 }, ]); expect(result.pastedTo.length).toBe(2); }); it('should throw error if clipboard is empty', async () => { const targetFile = join(testDir, 'target.txt'); writeFileSync(targetFile, 'content\n'); await expect( clipboardTools.pasteLines(testSessionId, [{ file_path: targetFile, target_line: 1 }]) ).rejects.toThrow('Clipboard is empty'); }); it('should log paste operation', async () => { const sourceFile = join(testDir, 'source.txt'); const targetFile = join(testDir, 'target.txt'); writeFileSync(sourceFile, 'content\n'); writeFileSync(targetFile, 'existing\n'); await clipboardTools.copyLines(testSessionId, sourceFile, 1, 1); await clipboardTools.pasteLines(testSessionId, [{ file_path: targetFile, target_line: 1 }]); const history = operationLogger.getHistory(testSessionId); expect(history.some((op) => op.operationType === 'paste')).toBe(true); }); it('should validate paste targets', async () => { const sourceFile = join(testDir, 'source.txt'); const targetFile = join(testDir, 'target.txt'); writeFileSync(sourceFile, 'content\n'); writeFileSync(targetFile, 'existing\n'); await clipboardTools.copyLines(testSessionId, sourceFile, 1, 1); await expect( clipboardTools.pasteLines(testSessionId, [{ file_path: targetFile, target_line: 2.5 }]) ).rejects.toThrow('Paste failed: targets[0].target_line must be an integer >= 0'); }); it('should require at least one target', async () => { await expect(clipboardTools.pasteLines(testSessionId, [])).rejects.toThrow( 'Paste failed: targets must be a non-empty array' ); }); }); describe('showClipboard', () => { it('should return clipboard content', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); await clipboardTools.copyLines(testSessionId, filePath, 1, 2); const result = await clipboardTools.showClipboard(testSessionId); expect(result.hasContent).toBe(true); expect(result.content).toBe('line 1\nline 2'); expect(result.operationType).toBe('copy'); }); it('should indicate empty clipboard', async () => { const result = await clipboardTools.showClipboard(testSessionId); expect(result.hasContent).toBe(false); expect(result.content).toBeUndefined(); }); it('should include source metadata', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\n'); await clipboardTools.copyLines(testSessionId, filePath, 2, 3); const result = await clipboardTools.showClipboard(testSessionId); expect(result.sourceFile).toBe(filePath); expect(result.startLine).toBe(2); expect(result.endLine).toBe(3); }); }); describe('undoLastPaste', () => { it('should undo last paste operation', async () => { const sourceFile = join(testDir, 'source.txt'); const targetFile = join(testDir, 'target.txt'); const originalContent = 'original line 1\noriginal line 2\n'; writeFileSync(sourceFile, 'new content\n'); writeFileSync(targetFile, originalContent); // Copy and paste await clipboardTools.copyLines(testSessionId, sourceFile, 1, 1); await clipboardTools.pasteLines(testSessionId, [{ file_path: targetFile, target_line: 1 }]); // Undo const result = await clipboardTools.undoLastPaste(testSessionId); expect(result.success).toBe(true); expect(result.restoredFiles.length).toBe(1); const snapshot = fileHandler.getFileSnapshot(targetFile); expect(snapshot.content).toBe(originalContent); }); it('should throw error if no paste to undo', async () => { await expect(clipboardTools.undoLastPaste(testSessionId)).rejects.toThrow( 'No paste operation to undo' ); }); it('should log undo operation', async () => { const sourceFile = join(testDir, 'source.txt'); const targetFile = join(testDir, 'target.txt'); writeFileSync(sourceFile, 'content\n'); writeFileSync(targetFile, 'original\n'); await clipboardTools.copyLines(testSessionId, sourceFile, 1, 1); await clipboardTools.pasteLines(testSessionId, [{ file_path: targetFile, target_line: 1 }]); await clipboardTools.undoLastPaste(testSessionId); const history = operationLogger.getHistory(testSessionId); expect(history.some((op) => op.operationType === 'undo')).toBe(true); }); it('should not undo same paste twice', async () => { const sourceFile = join(testDir, 'source.txt'); const targetFile = join(testDir, 'target.txt'); writeFileSync(sourceFile, 'content\n'); writeFileSync(targetFile, 'original\n'); await clipboardTools.copyLines(testSessionId, sourceFile, 1, 1); await clipboardTools.pasteLines(testSessionId, [{ file_path: targetFile, target_line: 1 }]); await clipboardTools.undoLastPaste(testSessionId); // Try to undo again await expect(clipboardTools.undoLastPaste(testSessionId)).rejects.toThrow( 'No paste operation to undo' ); }); }); describe('getOperationHistory', () => { it('should return operation history', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\n'); await clipboardTools.copyLines(testSessionId, filePath, 1, 1); await clipboardTools.copyLines(testSessionId, filePath, 2, 2); const result = await clipboardTools.getOperationHistory(testSessionId); expect(result.operations.length).toBe(2); expect(result.operations[0].operationType).toBe('copy'); }); it('should respect limit parameter', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\n'); for (let i = 0; i < 10; i++) { await clipboardTools.copyLines(testSessionId, filePath, 1, 1); } const result = await clipboardTools.getOperationHistory(testSessionId, 5); expect(result.operations.length).toBe(5); }); it('should validate limit parameter', async () => { await expect(clipboardTools.getOperationHistory(testSessionId, 1.5)).rejects.toThrow( 'Failed to get history: limit must be a positive integer' ); }); it('should order by most recent first', async () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); await clipboardTools.copyLines(testSessionId, filePath, 1, 1); await wait(10); await clipboardTools.cutLines(testSessionId, filePath, 1, 1); const result = await clipboardTools.getOperationHistory(testSessionId); expect(result.operations[0].operationType).toBe('cut'); expect(result.operations[1].operationType).toBe('copy'); }); }); describe('Path Access Control', () => { let allowFilePath: string; let pathControl: PathAccessControl; let restrictedTools: ClipboardTools; let restrictedFileHandler: FileHandler; beforeEach(() => { allowFilePath = join(testDir, 'paths.allow'); }); it('should block copy operations for paths not in allowlist', async () => { writeFileSync(allowFilePath, '/allowed/path/**\n'); pathControl = new PathAccessControl(allowFilePath); restrictedFileHandler = new FileHandler(pathControl); restrictedTools = new ClipboardTools( restrictedFileHandler, clipboardManager, operationLogger, sessionManager, pathControl ); const filePath = join(testDir, 'blocked.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); await expect(restrictedTools.copyLines(testSessionId, filePath, 1, 2)).rejects.toThrow( 'Access denied: path not in allowlist' ); }); it('should allow copy operations for paths in allowlist', async () => { writeFileSync(allowFilePath, `${testDir}/**\n`); pathControl = new PathAccessControl(allowFilePath); restrictedFileHandler = new FileHandler(pathControl); restrictedTools = new ClipboardTools( restrictedFileHandler, clipboardManager, operationLogger, sessionManager, pathControl ); const filePath = join(testDir, 'allowed.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); const result = await restrictedTools.copyLines(testSessionId, filePath, 1, 2); expect(result.success).toBe(true); expect(result.lines).toEqual(['line 1', 'line 2']); }); it('should block cut operations for paths not in allowlist', async () => { writeFileSync(allowFilePath, '/allowed/path/**\n'); pathControl = new PathAccessControl(allowFilePath); restrictedFileHandler = new FileHandler(pathControl); restrictedTools = new ClipboardTools( restrictedFileHandler, clipboardManager, operationLogger, sessionManager, pathControl ); const filePath = join(testDir, 'blocked.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); await expect(restrictedTools.cutLines(testSessionId, filePath, 1, 2)).rejects.toThrow( 'Access denied: path not in allowlist' ); }); it('should allow cut operations for paths in allowlist', async () => { writeFileSync(allowFilePath, `${testDir}/**\n`); pathControl = new PathAccessControl(allowFilePath); restrictedFileHandler = new FileHandler(pathControl); restrictedTools = new ClipboardTools( restrictedFileHandler, clipboardManager, operationLogger, sessionManager, pathControl ); const filePath = join(testDir, 'allowed.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); const result = await restrictedTools.cutLines(testSessionId, filePath, 1, 2); expect(result.success).toBe(true); expect(result.lines).toEqual(['line 1', 'line 2']); }); it('should block paste operations for paths not in allowlist', async () => { writeFileSync(allowFilePath, '/allowed/path/**\n'); pathControl = new PathAccessControl(allowFilePath); restrictedFileHandler = new FileHandler(pathControl); restrictedTools = new ClipboardTools( restrictedFileHandler, clipboardManager, operationLogger, sessionManager, pathControl ); // First copy something with unrestricted tools const sourceFile = join(testDir, 'source.txt'); writeFileSync(sourceFile, 'line 1\nline 2\n'); await clipboardTools.copyLines(testSessionId, sourceFile, 1, 2); // Try to paste to blocked location const targetFile = join(testDir, 'blocked.txt'); writeFileSync(targetFile, 'existing\n'); await expect( restrictedTools.pasteLines(testSessionId, [{ file_path: targetFile, target_line: 1 }]) ).rejects.toThrow('Access denied: path not in allowlist'); }); it('should allow paste operations for paths in allowlist', async () => { writeFileSync(allowFilePath, `${testDir}/**\n`); pathControl = new PathAccessControl(allowFilePath); restrictedFileHandler = new FileHandler(pathControl); restrictedTools = new ClipboardTools( restrictedFileHandler, clipboardManager, operationLogger, sessionManager, pathControl ); // Copy something const sourceFile = join(testDir, 'source.txt'); writeFileSync(sourceFile, 'line 1\nline 2\n'); await restrictedTools.copyLines(testSessionId, sourceFile, 1, 2); // Paste to allowed location const targetFile = join(testDir, 'target.txt'); writeFileSync(targetFile, 'existing\n'); const result = await restrictedTools.pasteLines(testSessionId, [ { file_path: targetFile, target_line: 1 }, ]); expect(result.success).toBe(true); }); it('should respect negation patterns for clipboard operations', async () => { writeFileSync(allowFilePath, `${testDir}/**\n!${testDir}/blocked/**\n`); pathControl = new PathAccessControl(allowFilePath); restrictedFileHandler = new FileHandler(pathControl); restrictedTools = new ClipboardTools( restrictedFileHandler, clipboardManager, operationLogger, sessionManager, pathControl ); // Create blocked subdirectory const blockedDir = join(testDir, 'blocked'); mkdirSync(blockedDir, { recursive: true }); const allowedFile = join(testDir, 'allowed.txt'); const blockedFile = join(blockedDir, 'blocked.txt'); writeFileSync(allowedFile, 'content'); writeFileSync(blockedFile, 'content'); // Should work for allowed file await expect( restrictedTools.copyLines(testSessionId, allowedFile, 1, 1) ).resolves.toHaveProperty('success', true); // Should fail for blocked file await expect(restrictedTools.copyLines(testSessionId, blockedFile, 1, 1)).rejects.toThrow( 'Access denied: path not in allowlist' ); }); it('should validate all paste targets before attempting any writes', async () => { writeFileSync(allowFilePath, `${testDir}/**\n!${testDir}/blocked/**\n`); pathControl = new PathAccessControl(allowFilePath); restrictedFileHandler = new FileHandler(pathControl); restrictedTools = new ClipboardTools( restrictedFileHandler, clipboardManager, operationLogger, sessionManager, pathControl ); // Create blocked subdirectory const blockedDir = join(testDir, 'blocked'); mkdirSync(blockedDir, { recursive: true }); // Copy something first const sourceFile = join(testDir, 'source.txt'); writeFileSync(sourceFile, 'line 1\nline 2\n'); await restrictedTools.copyLines(testSessionId, sourceFile, 1, 2); // Try to paste to mix of allowed and blocked locations const allowedTarget = join(testDir, 'allowed.txt'); const blockedTarget = join(blockedDir, 'blocked.txt'); writeFileSync(allowedTarget, 'existing\n'); writeFileSync(blockedTarget, 'existing\n'); await expect( restrictedTools.pasteLines(testSessionId, [ { file_path: allowedTarget, target_line: 1 }, { file_path: blockedTarget, target_line: 1 }, ]) ).rejects.toThrow('Access denied: path not in allowlist'); }); }); });

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