MCP Backup Server

by hexitex
Verified
#!/usr/bin/env node import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; import { promises as fsPromises } from 'fs'; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import os from 'os'; import { minimatch } from 'minimatch'; import { BackupCreateSchema, BackupListSchema, BackupRestoreSchema, BackupFolderCreateSchema, BackupFolderListSchema, BackupFolderRestoreSchema, ListAllBackupsSchema, CancelSchema, toolDescriptions } from './toolDescriptions.js'; import { BackupMetadata, BackupFolderMetadata, BackupResult, Operation } from './types.js'; import { checkOperationCancelled, formatJsonResponse, formatErrorResponse, validateRequiredParams, validateFileExists, validateFolderExists, exists } from './utils.js'; // Type for tool input const ToolInputSchema = ToolSchema.shape.inputSchema; type ToolInput = z.infer<typeof ToolInputSchema>; // Create a local ensureDirectoryExists function to avoid conflict with the imported one async function ensureBackupDirectoryExists(dirPath: string): Promise<void> { try { await fsPromises.mkdir(dirPath, { recursive: true }); } catch (error) { console.error(`Error creating directory ${dirPath}:`, error); throw error; } } // Constants const SERVER_VERSION = '1.0.0'; const SERVER_NAME = 'backup-mcp-server'; const BACKUP_DIR = process.env.BACKUP_DIR || path.join(os.homedir(), '.code_backups'); const MAX_VERSIONS = parseInt(process.env.MAX_VERSIONS || '10', 10); const EMERGENCY_BACKUP_DIR = process.env.EMERGENCY_BACKUP_DIR || path.join(os.homedir(), '.code_emergency_backups'); // Normalize backup directory paths for Windows const BACKUP_DIR_NORMALIZED = path.normalize(BACKUP_DIR); const EMERGENCY_BACKUP_DIR_NORMALIZED = path.normalize(EMERGENCY_BACKUP_DIR); // Track current operation let currentOperationId: string | null = null; // Map to track operations const operations = new Map<string, Operation>(); // Report progress for an operation function reportProgress(operationId: string, progress: number): void { // Only report progress if operationId is valid if (operationId) { console.error(`Operation ${operationId} progress: ${progress}%`); } } // Update operation progress safely function updateOperationProgress(operationId: string, progress: number): void { const operation = operations.get(operationId); if (operation) { operation.progress = progress; } } // Helper function to report progress function logProgress(progress: number): void { if (currentOperationId) { updateOperationProgress(currentOperationId, progress); reportProgress(currentOperationId, progress); } } // Generate a backup folder name function getBackupFolderName(folderPath: string, timestamp: string): string { const folderName = path.basename(folderPath); return `${folderName}.${timestamp}`; } // Create a new operation function createOperation(type: string, params: any): Operation { const id = crypto.randomUUID(); const operation: Operation = { id, type, progress: 0, cancelled: false, status: 'running' }; operations.set(id, operation); return operation; } // Cancel operation function cancelOperation(operationId: string): boolean { const operation = operations.get(operationId); if (operation) { operation.cancelled = true; return true; } return false; } // Create MCP server const server = new Server( { name: SERVER_NAME, version: SERVER_VERSION, }, { capabilities: { tools: {}, }, }, ); // Initialize server methods if not already initialized if (!(server as any).methods) { (server as any).methods = {}; } // Define tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: Object.values(toolDescriptions).map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema as ToolInput, })) }; }); // Custom schema for tool documentation requests const DescribeToolRequestSchema = z.object({ jsonrpc: z.literal('2.0'), method: z.literal('tools/describe'), params: z.object({ name: z.string().describe('Name of the tool to describe') }), id: z.union([z.string(), z.number()]) }); // Implement tool documentation server.setRequestHandler(DescribeToolRequestSchema, async (request) => { const { name } = request.params; const toolInfo = toolDescriptions[name]; if (!toolInfo) { throw new Error(`Tool '${name}' not found`); } return { content: [{ type: "text", text: toolInfo.usage }] }; }); // Implement tool handlers server.setRequestHandler(CallToolRequestSchema, async (request) => { let currentOperationId: string | null = null; try { const { name, arguments: toolInput } = request.params; console.error(`Received request for ${name} with params:`, toolInput); // Create a unique operation ID for tracking progress currentOperationId = createOperation(name, toolInput).id; switch (name) { case "backup_create": { const params = toolInput as z.infer<typeof BackupCreateSchema>; console.error('Received request for backup_create with params:', params); // Validate required parameters validateRequiredParams(params, ['file_path']); const filePath = path.normalize(params.file_path); // Check if file exists await validateFileExists(filePath); // Generate timestamp for the backup const timestamp = generateTimestamp(); // Create backup directory const backupDir = getBackupDir(filePath); await ensureBackupDirectoryExists(backupDir); // Create backup filename const backupFilename = getBackupFilename(filePath, timestamp); const backupPath = path.join(backupDir, backupFilename); // Report progress logProgress(10); // Check if operation was cancelled const cancelCheck = checkOperationCancelled( currentOperationId, operations, () => {} ); if (cancelCheck.isCancelled) return cancelCheck.response; // Copy the file await fsPromises.copyFile(filePath, backupPath); // Report progress logProgress(70); // Check if operation was cancelled const cancelCheck2 = checkOperationCancelled( currentOperationId, operations, () => { // Clean up the partial backup if (fs.existsSync(backupPath)) { fs.unlinkSync(backupPath); } } ); if (cancelCheck2.isCancelled) return cancelCheck2.response; // Create and save metadata const metadata = createBackupMetadata(filePath, timestamp, backupPath, params.agent_context); const metadataPath = getBackupMetadataFilename(backupPath); saveBackupMetadata(metadataPath, metadata); // Report progress logProgress(90); // Clean up old backups const versionsKept = cleanupOldBackups(filePath); // Report completion logProgress(100); // Return result with versionsKept return formatJsonResponse({ ...metadata, versions_kept: versionsKept }); } case "backup_list": { const params = toolInput as z.infer<typeof BackupListSchema>; console.error('Received request for backup_list with params:', params); // Validate required parameters validateRequiredParams(params, ['file_path']); const filePath = path.normalize(params.file_path); // Report initial progress logProgress(0); // Check if file exists await validateFileExists(filePath); // Report progress logProgress(30); const backups = findBackupsByFilePath(filePath); // Report progress logProgress(70); // Sort backups by timestamp (newest first) backups.sort((a, b) => { return b.timestamp.localeCompare(a.timestamp); }); // Report completion logProgress(100); return formatJsonResponse(backups); } case "backup_restore": { const params = toolInput as z.infer<typeof BackupRestoreSchema>; console.error('Received request for backup_restore with params:', params); // Validate required parameters validateRequiredParams(params, ['file_path', 'timestamp']); const filePath = path.normalize(params.file_path); const timestamp = params.timestamp; // Find the backup const backup = await findBackupByTimestamp(filePath, timestamp); if (!backup) { throw new Error(`Backup with timestamp ${timestamp} not found for ${filePath}`); } // Report progress logProgress(20); // Check if operation was cancelled const cancelCheck = checkOperationCancelled( currentOperationId, operations, () => {} ); if (cancelCheck.isCancelled) return cancelCheck.response; // Ensure the target directory exists const targetDir = path.dirname(filePath); await ensureBackupDirectoryExists(targetDir); // Report progress logProgress(50); // Check if operation was cancelled const cancelCheck2 = checkOperationCancelled( currentOperationId, operations, () => {} ); if (cancelCheck2.isCancelled) return cancelCheck2.response; // Create emergency backup if requested if (params.create_emergency_backup) { const emergencyBackupPath = await createEmergencyBackup(filePath); if (emergencyBackupPath) { console.error(`Created emergency backup at ${emergencyBackupPath}`); } } // Copy the backup file to the original location await restoreBackup(filePath, timestamp, params.create_emergency_backup); // Report completion logProgress(100); // Return result return formatJsonResponse({ restored_path: filePath, timestamp: timestamp }); } case "backup_folder_create": { const params = toolInput as z.infer<typeof BackupFolderCreateSchema>; console.error('Received request for backup_folder_create with params:', params); // Validate required parameters validateRequiredParams(params, ['folder_path']); const folderPath = path.normalize(params.folder_path); // Check if folder exists await validateFolderExists(folderPath); // Generate timestamp for the backup const timestamp = generateTimestamp(); // Create backup directory const backupDir = getBackupDir(folderPath); await ensureBackupDirectoryExists(backupDir); // Create backup folder name const backupFolderName = getBackupFolderName(folderPath, timestamp); const backupFolderPath = path.join(backupDir, backupFolderName); // Report progress logProgress(10); // Check if operation was cancelled const cancelCheck = checkOperationCancelled( currentOperationId, operations, () => {} ); if (cancelCheck.isCancelled) return cancelCheck.response; // Copy the folder await copyFolderContents(folderPath, backupFolderPath, params.include_pattern, params.exclude_pattern); // Report progress logProgress(70); // Check if operation was cancelled const cancelCheck2 = checkOperationCancelled( currentOperationId, operations, () => { // Clean up the partial backup if (fs.existsSync(backupFolderPath)) { fs.rmdirSync(backupFolderPath, { recursive: true }); } } ); if (cancelCheck2.isCancelled) return cancelCheck2.response; // Create and save metadata const metadata = createBackupMetadata(folderPath, timestamp, backupFolderPath, params.agent_context); const metadataPath = `${backupFolderPath}.meta.json`; saveBackupMetadata(metadataPath, metadata); // Report progress logProgress(90); // Clean up old backups const versionsKept = cleanupOldBackups(folderPath); // Report completion logProgress(100); // Return result with versionsKept return formatJsonResponse({ ...metadata, versions_kept: versionsKept }); } case "backup_folder_list": { const params = toolInput as z.infer<typeof BackupFolderListSchema>; console.error('Received request for backup_folder_list with params:', params); // Validate required parameters validateRequiredParams(params, ['folder_path']); const folderPath = path.normalize(params.folder_path); // Report initial progress logProgress(0); // Check if folder exists await validateFolderExists(folderPath); // Report progress logProgress(30); const backups = findBackupsByFolderPath(folderPath); // Report progress logProgress(70); // Sort backups by timestamp (newest first) backups.sort((a, b) => { return b.timestamp.localeCompare(a.timestamp); }); // Report completion logProgress(100); return formatJsonResponse(backups); } case "backup_folder_restore": { const params = toolInput as z.infer<typeof BackupFolderRestoreSchema>; console.error('Received request for backup_folder_restore with params:', params); // Validate required parameters validateRequiredParams(params, ['folder_path', 'timestamp']); const { folder_path, timestamp, create_emergency_backup = true } = params; const folderPath = path.normalize(folder_path); // Check if folder exists await validateFolderExists(folderPath); // Report initial progress logProgress(0); try { // Find the backup const backups = findBackupsByFolderPath(folderPath); const backup = backups.find(b => b.timestamp === timestamp); if (!backup) { throw new Error(`Backup with timestamp ${timestamp} not found for ${folderPath}`); } // Report progress logProgress(10); // Create emergency backup if requested let emergencyBackupPath: string | null = null; if (create_emergency_backup) { emergencyBackupPath = await createEmergencyFolderBackup(folderPath); } // Check if backup path exists if (!backup.backup_path || !fs.existsSync(backup.backup_path)) { throw new Error(`Backup folder not found: ${backup.backup_path}`); } // Check if operation was cancelled const cancelCheck = checkOperationCancelled( currentOperationId, operations, () => {} ); if (cancelCheck.isCancelled) return cancelCheck.response; // Copy the backup folder to the original location await copyFolderContents(backup.backup_path, folderPath); // Report completion logProgress(100); return formatJsonResponse({ restored_path: folderPath, timestamp: timestamp, emergency_backup_path: emergencyBackupPath }); } catch (error) { // Update operation status on error const operation = operations.get(currentOperationId); if (operation) { operation.status = 'error'; } throw error; } } case "backup_list_all": { const params = toolInput as z.infer<typeof ListAllBackupsSchema>; console.error('Received request for backup_list_all with params:', params); // Extract parameters const includePattern = params.include_pattern; const excludePattern = params.exclude_pattern; const includeEmergency = params.include_emergency !== false; // Default to true if not specified // Create operation for tracking const operation = operations.get(currentOperationId); if (operation) { operation.status = 'running'; } // Report initial progress logProgress(0); try { // Initialize results object const results: { main_backups: Array<{ path: string; type: string; size: number; created_at: string; original_path: string | null; }>; emergency_backups: Array<{ path: string; type: string; size: number; created_at: string; original_path: string | null; }>; } = { main_backups: [], emergency_backups: [] }; // Function to scan a directory and get all backup files async function scanBackupDirectory(directory: string, isEmergency: boolean = false) { if (!fs.existsSync(directory)) { return []; } // Get all files and folders in the directory recursively const getAllFiles = async (dir: string, fileList: any[] = []) => { const files = await fsPromises.readdir(dir, { withFileTypes: true }); for (const file of files) { const filePath = path.join(dir, file.name); // Check if operation was cancelled if (currentOperationId && operations.get(currentOperationId)?.cancelled) { throw new Error('Operation cancelled'); } // Apply include/exclude patterns if specified if (includePattern && !minimatch(filePath, includePattern)) { continue; } if (excludePattern && minimatch(filePath, excludePattern)) { continue; } if (file.isDirectory()) { fileList = await getAllFiles(filePath, fileList); } else { // Check if this is a backup file (has timestamp format in name) const isBackupFile = /\.\d{8}-\d{6}-\d{3}$/.test(file.name); const isMetadataFile = file.name.endsWith('.meta.json'); if (isBackupFile || isMetadataFile) { try { const stats = await fsPromises.stat(filePath); // Try to get original path from metadata if this is a backup file let originalPath = null; let backupType = 'unknown'; if (isBackupFile) { // Look for corresponding metadata file const metadataPath = `${filePath}.meta.json`; if (await exists(metadataPath)) { try { const metadataContent = await fsPromises.readFile(metadataPath, 'utf8'); const metadata = JSON.parse(metadataContent); originalPath = metadata.original_path; } catch (err) { console.error(`Error reading metadata for ${filePath}:`, err); } } } else if (isMetadataFile) { try { const metadataContent = await fsPromises.readFile(filePath, 'utf8'); const metadata = JSON.parse(metadataContent); originalPath = metadata.original_path; } catch (err) { console.error(`Error reading metadata file ${filePath}:`, err); } } // Add to appropriate list const result = { path: filePath, type: file.isDirectory() ? 'directory' : 'file', size: stats.size, created_at: stats.birthtime.toISOString(), original_path: originalPath }; if (isEmergency) { results.emergency_backups.push(result); } else { results.main_backups.push(result); } // Update progress periodically if (results.main_backups.length % 10 === 0 || results.emergency_backups.length % 10 === 0) { // Calculate progress based on number of files found const totalFiles = results.main_backups.length + results.emergency_backups.length; // Cap progress at 90% until we're completely done const progress = Math.min(90, Math.floor(totalFiles / 10) * 5); logProgress(progress); } } catch (err) { console.error(`Error processing file ${filePath}:`, err); } } } } return fileList; }; await getAllFiles(directory); } // Scan main backup directory await scanBackupDirectory(BACKUP_DIR_NORMALIZED); // Report progress after scanning main directory logProgress(50); // Scan emergency backup directory if requested if (includeEmergency) { console.error('Scanning emergency backup directory:', EMERGENCY_BACKUP_DIR_NORMALIZED); if (!fs.existsSync(EMERGENCY_BACKUP_DIR_NORMALIZED)) { console.error('Emergency backup directory does not exist, creating it'); await fsPromises.mkdir(EMERGENCY_BACKUP_DIR_NORMALIZED, { recursive: true }); } await scanBackupDirectory(EMERGENCY_BACKUP_DIR_NORMALIZED, true); } // Report completion logProgress(100); return formatJsonResponse(results); } catch (error) { // Update operation status on error const operation = operations.get(currentOperationId); if (operation) { operation.status = 'error'; } throw error; } } case "mcp_cancel": { const params = toolInput as z.infer<typeof CancelSchema>; console.error('Received request for mcp_cancel with params:', params); // Validate required parameters validateRequiredParams(params, ['operationId']); const { operationId } = params; const cancelled = cancelOperation(operationId); if (!cancelled) { return formatJsonResponse({ success: false, error: `Operation ${operationId} not found or already completed` }); } return formatJsonResponse({ success: true, operationId, status: 'cancelled' }); } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { console.error('Error handling request:', error); return formatErrorResponse(error, currentOperationId); } }); // Utility functions function generateOperationId(): string { return crypto.randomUUID(); } function generateTimestamp(): string { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); const milliseconds = String(now.getMilliseconds()).padStart(3, '0'); return `${year}${month}${day}-${hours}${minutes}${seconds}-${milliseconds}`; } function getBackupDir(filePath: string): string { // Create a directory structure that mirrors the original file's path const normalizedPath = path.normalize(filePath); const parsedPath = path.parse(normalizedPath); // Remove drive letter (on Windows) and create backup path let relativePath = parsedPath.dir.replace(/^[a-zA-Z]:/, ''); // Ensure the path is safe by removing leading slashes relativePath = relativePath.replace(/^[/\\]+/, ''); // Create the backup directory path return path.join(BACKUP_DIR_NORMALIZED, relativePath); } function getBackupFilename(filePath: string, timestamp: string): string { const parsedPath = path.parse(filePath); return `${parsedPath.name}${parsedPath.ext}.${timestamp}`; } function getBackupMetadataFilename(backupFilePath: string): string { return `${backupFilePath}.meta.json`; } function createBackupMetadata(filePath: string, timestamp: string, backupPath: string, agentContext?: string): BackupMetadata { return { original_path: filePath, original_filename: path.basename(filePath), timestamp: timestamp, created_at: new Date().toISOString(), backup_path: backupPath, relative_path: path.relative(process.cwd(), backupPath), agent_context: agentContext }; } function saveBackupMetadata(metadataPath: string, metadata: BackupMetadata): void { fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2)); } function readBackupMetadata(metadataPath: string): BackupMetadata | BackupFolderMetadata | null { try { const data = fs.readFileSync(metadataPath, 'utf8'); return JSON.parse(data); } catch (err) { console.error(`Error reading metadata: ${err}`); return null; } } function isFolderMetadata(metadata: any): metadata is BackupFolderMetadata { // Check if this is a folder metadata by examining the backup_path // Folder backups have a directory structure, while file backups have a file return metadata && metadata.original_path && metadata.backup_path && !metadata.backup_path.endsWith('.meta.json') && fs.existsSync(metadata.backup_path) && fs.statSync(metadata.backup_path).isDirectory(); } // Helper function to check if a path is a parent of another path function isParentPath(parentPath: string, childPath: string): boolean { const normalizedParent = path.normalize(parentPath).toLowerCase() + path.sep; const normalizedChild = path.normalize(childPath).toLowerCase() + path.sep; return normalizedChild.startsWith(normalizedParent); } // Helper function to recursively search for backup metadata files function findAllBackupMetadataFiles(directory: string): string[] { if (!fs.existsSync(directory)) { return []; } let results: string[] = []; const items = fs.readdirSync(directory); for (const item of items) { const itemPath = path.join(directory, item); const stats = fs.statSync(itemPath); if (stats.isDirectory()) { // Recursively search subdirectories results = results.concat(findAllBackupMetadataFiles(itemPath)); } else if (item.endsWith('.meta.json')) { // Add metadata files to results results.push(itemPath); } } return results; } function findBackupsByFilePath(filePath: string): BackupMetadata[] { const backupDir = getBackupDir(filePath); const backups: BackupMetadata[] = []; // Start at the root of the backup directory to find all possible backups const rootBackupDir = BACKUP_DIR_NORMALIZED; // Find all metadata files recursively const metadataFiles = findAllBackupMetadataFiles(rootBackupDir); // Process each metadata file for (const metadataPath of metadataFiles) { const metadata = readBackupMetadata(metadataPath); // Check if this backup is for the requested file (exact match) if (metadata && metadata.original_path === filePath && !isFolderMetadata(metadata)) { backups.push(metadata); } } // Sort backups by timestamp (newest first) backups.sort((a, b) => { return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(); }); return backups; } function findBackupsByFolderPath(folderPath: string): BackupFolderMetadata[] { const backups: BackupFolderMetadata[] = []; // Start at the root of the backup directory to find all possible backups const rootBackupDir = BACKUP_DIR_NORMALIZED; // Find all metadata files recursively const metadataFiles = findAllBackupMetadataFiles(rootBackupDir); // Process each metadata file for (const metadataPath of metadataFiles) { try { const metadata = readBackupMetadata(metadataPath); // Check if this backup is for the requested folder (exact match) or any subfolder if (metadata && isFolderMetadata(metadata)) { // Include if it's an exact match or if the original path is a parent of the requested path // or if the requested path is a parent of the original path if (metadata.original_path === folderPath || isParentPath(metadata.original_path, folderPath) || isParentPath(folderPath, metadata.original_path)) { backups.push(metadata); } } } catch (error) { console.error(`Error processing metadata file ${metadataPath}:`, error); // Continue processing other metadata files } } // Sort backups by timestamp (newest first) backups.sort((a, b) => { return b.timestamp.localeCompare(a.timestamp); }); return backups; } async function findBackupByTimestamp(filePath: string, timestamp: string): Promise<BackupMetadata | null> { const backupDir = getBackupDir(filePath); const backupFilename = getBackupFilename(filePath, timestamp); const backupPath = path.join(backupDir, backupFilename); const metadataPath = `${backupPath}.meta.json`; if (fs.existsSync(metadataPath)) { const metadata = readBackupMetadata(metadataPath); if (metadata && !isFolderMetadata(metadata)) { return metadata; } } return null; } async function findFolderBackupByTimestamp(folderPath: string, timestamp: string): Promise<BackupFolderMetadata | null> { const backupDir = getBackupDir(folderPath); const backupFolderName = getBackupFolderName(folderPath, timestamp); const backupPath = path.join(backupDir, backupFolderName); const metadataPath = `${backupPath}.meta.json`; if (fs.existsSync(metadataPath)) { const metadata = readBackupMetadata(metadataPath); if (metadata && isFolderMetadata(metadata)) { return metadata; } } return null; } async function listFolderBackups(folderPath: string): Promise<BackupFolderMetadata[]> { return findBackupsByFolderPath(folderPath); } function cleanupOldBackups(filePath: string): number { // Get all backups for this file const backups = findBackupsByFilePath(filePath); // If we have more than MAX_VERSIONS, remove the oldest ones if (backups.length > MAX_VERSIONS) { // Sort backups by timestamp (oldest first) backups.sort((a, b) => { return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(); }); // Remove oldest backups const backupsToRemove = backups.slice(0, backups.length - MAX_VERSIONS); for (const backup of backupsToRemove) { try { fs.unlinkSync(backup.backup_path); console.log(`Removed old backup: ${backup.backup_path}`); } catch (error) { console.error(`Error removing old backup: ${backup.backup_path}`, error); } } return MAX_VERSIONS; } return backups.length; } // Copy folder recursively async function copyFolderRecursive(sourcePath: string, targetPath: string, includePattern?: string, excludePattern?: string): Promise<void> { // Create target folder if it doesn't exist if (!fs.existsSync(targetPath)) { await fsPromises.mkdir(targetPath, { recursive: true }); } // Read source directory const entries = fs.readdirSync(sourcePath, { withFileTypes: true }); // Process each entry for (const entry of entries) { const srcPath = path.join(sourcePath, entry.name); const destPath = path.join(targetPath, entry.name); // Skip excluded files/folders if (excludePattern && minimatch(entry.name, excludePattern)) { continue; } // Only include files/folders matching the include pattern if specified if (includePattern && !minimatch(entry.name, includePattern)) { continue; } if (entry.isDirectory()) { // Recursively copy subdirectories await copyFolderRecursive(srcPath, destPath, includePattern || undefined, excludePattern || undefined); } else { // Copy files await fsPromises.copyFile(srcPath, destPath); } } } // Copy folder contents helper function async function copyFolderContents(sourcePath: string, targetPath: string, includePattern?: string, excludePattern?: string): Promise<void> { if (!sourcePath || !targetPath) { throw new Error('Source and target paths are required'); } // Ensure target directory exists await fsPromises.mkdir(targetPath, { recursive: true }); // Copy folder contents await copyFolderRecursive(sourcePath, targetPath, includePattern, excludePattern); } // Ensure emergency backup directory exists async function ensureEmergencyBackupDir(): Promise<void> { if (!fs.existsSync(EMERGENCY_BACKUP_DIR_NORMALIZED)) { await fsPromises.mkdir(EMERGENCY_BACKUP_DIR_NORMALIZED, { recursive: true }); } } // Create emergency backup of a file before restoration async function createEmergencyBackup(filePath: string): Promise<string | null> { try { if (!fs.existsSync(filePath)) { console.error(`File not found for emergency backup: ${filePath}`); return null; } await ensureEmergencyBackupDir(); const timestamp = generateTimestamp(); const fileName = path.basename(filePath); // Create a directory structure that mirrors the original file's path const normalizedPath = path.normalize(filePath); const parsedPath = path.parse(normalizedPath); // Remove drive letter (on Windows) and create backup path let relativePath = parsedPath.dir.replace(/^[a-zA-Z]:/, ''); // Ensure the path is safe by removing leading slashes relativePath = relativePath.replace(/^[/\\]+/, ''); // Create the emergency backup directory path const emergencyBackupDir = path.join(EMERGENCY_BACKUP_DIR_NORMALIZED, relativePath); // Ensure the directory structure exists await fsPromises.mkdir(emergencyBackupDir, { recursive: true }); // Create the emergency backup file path const backupPath = path.join(emergencyBackupDir, `${parsedPath.name}${parsedPath.ext}.emergency.${timestamp}`); // Copy file to emergency backup location await fsPromises.copyFile(filePath, backupPath); // Create metadata file for the emergency backup const metadata = createBackupMetadata(filePath, timestamp, backupPath, "Emergency backup created before restoration"); const metadataPath = path.join(EMERGENCY_BACKUP_DIR_NORMALIZED, `${parsedPath.name}.emergency.${timestamp}.meta.json`); await fsPromises.writeFile(metadataPath, JSON.stringify(metadata, null, 2)); return backupPath; } catch (error) { console.error('Error creating emergency backup:', error); return null; } } // Create emergency backup of a folder before restoration async function createEmergencyFolderBackup(folderPath: string): Promise<string | null> { try { if (!fs.existsSync(folderPath)) { console.error(`Folder not found for emergency backup: ${folderPath}`); return null; } await ensureEmergencyBackupDir(); const timestamp = generateTimestamp(); // Create a directory structure that mirrors the original folder's path const normalizedPath = path.normalize(folderPath); const parsedPath = path.parse(normalizedPath); // Remove drive letter (on Windows) and create backup path let relativePath = parsedPath.dir.replace(/^[a-zA-Z]:/, ''); // Ensure the path is safe by removing leading slashes relativePath = relativePath.replace(/^[/\\]+/, ''); // Create the emergency backup directory path const emergencyBackupDir = path.join(EMERGENCY_BACKUP_DIR_NORMALIZED, relativePath); // Ensure the directory structure exists await fsPromises.mkdir(emergencyBackupDir, { recursive: true }); // Create the emergency backup folder path const backupPath = path.join(emergencyBackupDir, `${parsedPath.name}.emergency.${timestamp}`); // Copy folder to emergency backup location await copyFolderContents(folderPath, backupPath); // Create metadata file for the emergency backup const metadata = { original_path: folderPath, original_filename: path.basename(folderPath), timestamp: timestamp, created_at: new Date().toISOString(), backup_path: backupPath, relative_path: path.relative(process.cwd(), backupPath), agent_context: "Emergency backup created before restoration" }; const metadataPath = path.join(EMERGENCY_BACKUP_DIR_NORMALIZED, `${parsedPath.name}.emergency.${timestamp}.meta.json`); await fsPromises.writeFile(metadataPath, JSON.stringify(metadata, null, 2)); return backupPath; } catch (error) { console.error('Error creating emergency folder backup:', error); return null; } } // Fix string | null assignment errors async function mcp_backup_status(params: { operationId: string }): Promise<{ progress: number, status: string }> { const { operationId } = params; if (!operationId) { return { progress: 0, status: 'error' }; } // Check if operation exists if (operations.has(operationId)) { const operation = operations.get(operationId); if (operation) { return { progress: operation.progress, status: operation.cancelled ? 'cancelled' : operation.progress >= 100 ? 'completed' : 'in_progress' }; } } return { progress: 0, status: 'not_found' }; } // Restore backup function async function restoreBackup(filePath: string, timestamp: string, createEmergencyBackupFlag: boolean = false): Promise<void> { // Find the backup const backups = findBackupsByFilePath(filePath); const backup = backups.find(b => b.timestamp === timestamp); if (!backup) { throw new Error(`Backup with timestamp ${timestamp} not found for ${filePath}`); } // Create emergency backup if requested if (createEmergencyBackupFlag) { const emergencyBackupPath = await createEmergencyBackup(filePath); console.log(`Created emergency backup at: ${emergencyBackupPath}`); } // Get backup path const backupPath = backup.backup_path; // Check if backup exists if (!backupPath || !fs.existsSync(backupPath)) { throw new Error(`Backup file not found: ${backupPath}`); } // Check if original file exists if (!fs.existsSync(filePath)) { throw new Error(`Original file not found: ${filePath}`); } // Restore backup by copying it to original location await fsPromises.copyFile(backupPath, filePath); } // Start the server with stdio transport const transport = new StdioServerTransport(); server.connect(transport).catch((error: Error) => { console.error("Fatal error running server:", error); process.exit(1); });