@@ -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+
243422def 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