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);
});