Skip to content

Commit ee04223

Browse files
author
LittleCoinCoin
committed
feat: implement MCP backup management commands (Phase 3d)
Add comprehensive backup management functionality for MCP host configurations: Core Commands: - hatch mcp backup restore: Restore host configuration from backup with confirmation - hatch mcp backup list: List available backups with detailed/summary views - hatch mcp backup clean: Clean old backups with age/count-based criteria Implementation Details: - Integration with MCPHostConfigBackupManager for all backup operations - Support for dry-run mode across all backup commands - Auto-approval flags to skip confirmation prompts for automation - Detailed backup information display with timestamps, sizes, and ages - Flexible cleanup criteria (older-than-days, keep-count) with safety confirmations - Comprehensive error handling for invalid hosts and missing backups Testing: - 15 comprehensive test cases covering all backup command scenarios - Component-level integration tests with mocked backup manager - Error scenario validation and confirmation prompt testing - Dry-run functionality verification and output format validation Next: Implement direct MCP management commands (Phase 3e) for server and host configuration without package dependencies.
1 parent f8fdbe9 commit ee04223

File tree

2 files changed

+504
-0
lines changed

2 files changed

+504
-0
lines changed

hatch/cli_hatch.py

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,185 @@ def handle_mcp_list_servers(env_manager: HatchEnvironmentManager, env_name: Opti
240240
print(f"Error listing servers: {e}")
241241
return 1
242242

243+
def handle_mcp_backup_restore(host: str, backup_file: Optional[str] = None, dry_run: bool = False, auto_approve: bool = False):
244+
"""Handle 'hatch mcp backup restore' command."""
245+
try:
246+
from hatch.mcp_host_config.backup import MCPHostConfigBackupManager
247+
248+
# Validate host type
249+
try:
250+
host_type = MCPHostType(host)
251+
except ValueError:
252+
print(f"Error: Invalid host '{host}'. Supported hosts: {[h.value for h in MCPHostType]}")
253+
return 1
254+
255+
backup_manager = MCPHostConfigBackupManager()
256+
257+
# Get backup file path
258+
if backup_file:
259+
backup_path = backup_manager.backup_root / host / backup_file
260+
if not backup_path.exists():
261+
print(f"Error: Backup file '{backup_file}' not found for host '{host}'")
262+
return 1
263+
else:
264+
backup_path = backup_manager._get_latest_backup(host)
265+
if not backup_path:
266+
print(f"Error: No backups found for host '{host}'")
267+
return 1
268+
backup_file = backup_path.name
269+
270+
if dry_run:
271+
print(f"[DRY RUN] Would restore backup for host '{host}':")
272+
print(f"[DRY RUN] Backup file: {backup_file}")
273+
print(f"[DRY RUN] Backup path: {backup_path}")
274+
return 0
275+
276+
# Confirm operation unless auto-approved
277+
if not request_confirmation(
278+
f"Restore backup '{backup_file}' for host '{host}'? This will overwrite current configuration.",
279+
auto_approve
280+
):
281+
print("Operation cancelled.")
282+
return 0
283+
284+
# Perform restoration
285+
success = backup_manager.restore_backup(host, backup_file)
286+
287+
if success:
288+
print(f"✓ Successfully restored backup '{backup_file}' for host '{host}'")
289+
return 0
290+
else:
291+
print(f"✗ Failed to restore backup '{backup_file}' for host '{host}'")
292+
return 1
293+
294+
except Exception as e:
295+
print(f"Error restoring backup: {e}")
296+
return 1
297+
298+
def handle_mcp_backup_list(host: str, detailed: bool = False):
299+
"""Handle 'hatch mcp backup list' command."""
300+
try:
301+
from hatch.mcp_host_config.backup import MCPHostConfigBackupManager
302+
303+
# Validate host type
304+
try:
305+
host_type = MCPHostType(host)
306+
except ValueError:
307+
print(f"Error: Invalid host '{host}'. Supported hosts: {[h.value for h in MCPHostType]}")
308+
return 1
309+
310+
backup_manager = MCPHostConfigBackupManager()
311+
backups = backup_manager.list_backups(host)
312+
313+
if not backups:
314+
print(f"No backups found for host '{host}'")
315+
return 0
316+
317+
print(f"Backups for host '{host}' ({len(backups)} found):")
318+
319+
if detailed:
320+
print(f"{'Backup File':<40} {'Created':<20} {'Size':<10} {'Age (days)'}")
321+
print("-" * 80)
322+
323+
for backup in backups:
324+
created = backup.timestamp.strftime("%Y-%m-%d %H:%M:%S")
325+
size = f"{backup.file_size:,} B"
326+
age = backup.age_days
327+
328+
print(f"{backup.file_path.name:<40} {created:<20} {size:<10} {age}")
329+
else:
330+
for backup in backups:
331+
created = backup.timestamp.strftime("%Y-%m-%d %H:%M:%S")
332+
print(f" {backup.file_path.name} (created: {created}, {backup.age_days} days ago)")
333+
334+
return 0
335+
except Exception as e:
336+
print(f"Error listing backups: {e}")
337+
return 1
338+
339+
def handle_mcp_backup_clean(host: str, older_than_days: Optional[int] = None, keep_count: Optional[int] = None,
340+
dry_run: bool = False, auto_approve: bool = False):
341+
"""Handle 'hatch mcp backup clean' command."""
342+
try:
343+
from hatch.mcp_host_config.backup import MCPHostConfigBackupManager
344+
345+
# Validate host type
346+
try:
347+
host_type = MCPHostType(host)
348+
except ValueError:
349+
print(f"Error: Invalid host '{host}'. Supported hosts: {[h.value for h in MCPHostType]}")
350+
return 1
351+
352+
# Validate cleanup criteria
353+
if not older_than_days and not keep_count:
354+
print("Error: Must specify either --older-than-days or --keep-count")
355+
return 1
356+
357+
backup_manager = MCPHostConfigBackupManager()
358+
backups = backup_manager.list_backups(host)
359+
360+
if not backups:
361+
print(f"No backups found for host '{host}'")
362+
return 0
363+
364+
# Determine which backups would be cleaned
365+
to_clean = []
366+
367+
if older_than_days:
368+
for backup in backups:
369+
if backup.age_days > older_than_days:
370+
to_clean.append(backup)
371+
372+
if keep_count and len(backups) > keep_count:
373+
# Keep newest backups, remove oldest
374+
to_clean.extend(backups[keep_count:])
375+
376+
# Remove duplicates while preserving order
377+
seen = set()
378+
unique_to_clean = []
379+
for backup in to_clean:
380+
if backup.file_path not in seen:
381+
seen.add(backup.file_path)
382+
unique_to_clean.append(backup)
383+
384+
if not unique_to_clean:
385+
print(f"No backups match cleanup criteria for host '{host}'")
386+
return 0
387+
388+
if dry_run:
389+
print(f"[DRY RUN] Would clean {len(unique_to_clean)} backup(s) for host '{host}':")
390+
for backup in unique_to_clean:
391+
print(f"[DRY RUN] {backup.file_path.name} (age: {backup.age_days} days)")
392+
return 0
393+
394+
# Confirm operation unless auto-approved
395+
if not request_confirmation(
396+
f"Clean {len(unique_to_clean)} backup(s) for host '{host}'?",
397+
auto_approve
398+
):
399+
print("Operation cancelled.")
400+
return 0
401+
402+
# Perform cleanup
403+
filters = {}
404+
if older_than_days:
405+
filters['older_than_days'] = older_than_days
406+
if keep_count:
407+
filters['keep_count'] = keep_count
408+
409+
cleaned_count = backup_manager.clean_backups(host, **filters)
410+
411+
if cleaned_count > 0:
412+
print(f"✓ Successfully cleaned {cleaned_count} backup(s) for host '{host}'")
413+
return 0
414+
else:
415+
print(f"No backups were cleaned for host '{host}'")
416+
return 0
417+
418+
except Exception as e:
419+
print(f"Error cleaning backups: {e}")
420+
return 1
421+
243422
def main():
244423
"""Main entry point for Hatch CLI.
245424
@@ -368,6 +547,31 @@ def main():
368547
mcp_list_servers_parser = mcp_list_subparsers.add_parser("servers", help="List configured MCP servers from environment")
369548
mcp_list_servers_parser.add_argument("--env", "-e", default=None, help="Environment name (default: current environment)")
370549

550+
# MCP backup commands
551+
mcp_backup_subparsers = mcp_subparsers.add_parser("backup", help="Backup management commands").add_subparsers(
552+
dest="backup_command", help="Backup command to execute"
553+
)
554+
555+
# Restore backup command
556+
mcp_backup_restore_parser = mcp_backup_subparsers.add_parser("restore", help="Restore MCP host configuration from backup")
557+
mcp_backup_restore_parser.add_argument("host", help="Host platform to restore (e.g., claude-desktop, cursor)")
558+
mcp_backup_restore_parser.add_argument("--backup-file", "-f", default=None, help="Specific backup file to restore (default: latest)")
559+
mcp_backup_restore_parser.add_argument("--dry-run", action="store_true", help="Preview restore operation without execution")
560+
mcp_backup_restore_parser.add_argument("--auto-approve", action="store_true", help="Skip confirmation prompts")
561+
562+
# List backups command
563+
mcp_backup_list_parser = mcp_backup_subparsers.add_parser("list", help="List available backups for MCP host")
564+
mcp_backup_list_parser.add_argument("host", help="Host platform to list backups for (e.g., claude-desktop, cursor)")
565+
mcp_backup_list_parser.add_argument("--detailed", "-d", action="store_true", help="Show detailed backup information")
566+
567+
# Clean backups command
568+
mcp_backup_clean_parser = mcp_backup_subparsers.add_parser("clean", help="Clean old backups based on criteria")
569+
mcp_backup_clean_parser.add_argument("host", help="Host platform to clean backups for (e.g., claude-desktop, cursor)")
570+
mcp_backup_clean_parser.add_argument("--older-than-days", type=int, help="Remove backups older than specified days")
571+
mcp_backup_clean_parser.add_argument("--keep-count", type=int, help="Keep only the specified number of newest backups")
572+
mcp_backup_clean_parser.add_argument("--dry-run", action="store_true", help="Preview cleanup operation without execution")
573+
mcp_backup_clean_parser.add_argument("--auto-approve", action="store_true", help="Skip confirmation prompts")
574+
371575
# Package management commands
372576
pkg_subparsers = subparsers.add_parser("package", help="Package management commands").add_subparsers(
373577
dest="pkg_command", help="Package command to execute"
@@ -826,6 +1030,22 @@ def main():
8261030
else:
8271031
print("Unknown list command")
8281032
return 1
1033+
1034+
elif args.mcp_command == "backup":
1035+
if args.backup_command == "restore":
1036+
return handle_mcp_backup_restore(
1037+
args.host, args.backup_file, args.dry_run, args.auto_approve
1038+
)
1039+
elif args.backup_command == "list":
1040+
return handle_mcp_backup_list(args.host, args.detailed)
1041+
elif args.backup_command == "clean":
1042+
return handle_mcp_backup_clean(
1043+
args.host, args.older_than_days, args.keep_count,
1044+
args.dry_run, args.auto_approve
1045+
)
1046+
else:
1047+
print("Unknown backup command")
1048+
return 1
8291049
else:
8301050
print("Unknown MCP command")
8311051
return 1

0 commit comments

Comments
 (0)