|
12 | 12 | import logging |
13 | 13 | import sys |
14 | 14 | from pathlib import Path |
15 | | -from typing import Optional |
| 15 | +from typing import Optional, List |
16 | 16 |
|
17 | 17 | from hatch.environment_manager import HatchEnvironmentManager |
18 | 18 | from hatch_validator import HatchPackageValidator |
@@ -632,6 +632,141 @@ def handle_mcp_remove(host: str, server_name: str, no_backup: bool = False, |
632 | 632 | print(f"Error removing MCP server: {e}") |
633 | 633 | return 1 |
634 | 634 |
|
| 635 | +def parse_host_list(host_arg: str) -> List[str]: |
| 636 | + """Parse comma-separated host list or 'all'.""" |
| 637 | + if not host_arg: |
| 638 | + return [] |
| 639 | + |
| 640 | + if host_arg.lower() == 'all': |
| 641 | + from hatch.mcp_host_config.host_management import MCPHostRegistry |
| 642 | + available_hosts = MCPHostRegistry.detect_available_hosts() |
| 643 | + return [host.value for host in available_hosts] |
| 644 | + |
| 645 | + hosts = [] |
| 646 | + for host_str in host_arg.split(','): |
| 647 | + host_str = host_str.strip() |
| 648 | + try: |
| 649 | + host_type = MCPHostType(host_str) |
| 650 | + hosts.append(host_type.value) |
| 651 | + except ValueError: |
| 652 | + available = [h.value for h in MCPHostType] |
| 653 | + raise ValueError(f"Unknown host '{host_str}'. Available: {available}") |
| 654 | + |
| 655 | + return hosts |
| 656 | + |
| 657 | +def handle_mcp_remove_server(server_name: str, hosts: Optional[str] = None, |
| 658 | + env: Optional[str] = None, no_backup: bool = False, |
| 659 | + dry_run: bool = False, auto_approve: bool = False): |
| 660 | + """Handle 'hatch mcp remove server' command.""" |
| 661 | + try: |
| 662 | + # Determine target hosts |
| 663 | + if hosts: |
| 664 | + target_hosts = parse_host_list(hosts) |
| 665 | + elif env: |
| 666 | + # TODO: Implement environment-based server removal |
| 667 | + print("Error: Environment-based removal not yet implemented") |
| 668 | + return 1 |
| 669 | + else: |
| 670 | + print("Error: Must specify either --host or --env") |
| 671 | + return 1 |
| 672 | + |
| 673 | + if not target_hosts: |
| 674 | + print("Error: No valid hosts specified") |
| 675 | + return 1 |
| 676 | + |
| 677 | + if dry_run: |
| 678 | + print(f"[DRY RUN] Would remove MCP server '{server_name}' from hosts: {', '.join(target_hosts)}") |
| 679 | + print(f"[DRY RUN] Backup: {'Disabled' if no_backup else 'Enabled'}") |
| 680 | + return 0 |
| 681 | + |
| 682 | + # Confirm operation unless auto-approved |
| 683 | + hosts_str = ', '.join(target_hosts) |
| 684 | + if not request_confirmation( |
| 685 | + f"Remove MCP server '{server_name}' from hosts: {hosts_str}?", |
| 686 | + auto_approve |
| 687 | + ): |
| 688 | + print("Operation cancelled.") |
| 689 | + return 0 |
| 690 | + |
| 691 | + # Perform removal on each host |
| 692 | + mcp_manager = MCPHostConfigurationManager() |
| 693 | + success_count = 0 |
| 694 | + total_count = len(target_hosts) |
| 695 | + |
| 696 | + for host in target_hosts: |
| 697 | + result = mcp_manager.remove_server( |
| 698 | + server_name=server_name, |
| 699 | + hostname=host, |
| 700 | + no_backup=no_backup |
| 701 | + ) |
| 702 | + |
| 703 | + if result.success: |
| 704 | + print(f"[SUCCESS] Successfully removed '{server_name}' from '{host}'") |
| 705 | + if result.backup_path: |
| 706 | + print(f" Backup created: {result.backup_path}") |
| 707 | + success_count += 1 |
| 708 | + else: |
| 709 | + print(f"[ERROR] Failed to remove '{server_name}' from '{host}': {result.error_message}") |
| 710 | + |
| 711 | + # Summary |
| 712 | + if success_count == total_count: |
| 713 | + print(f"[SUCCESS] Removed '{server_name}' from all {total_count} hosts") |
| 714 | + return 0 |
| 715 | + elif success_count > 0: |
| 716 | + print(f"[PARTIAL SUCCESS] Removed '{server_name}' from {success_count}/{total_count} hosts") |
| 717 | + return 1 |
| 718 | + else: |
| 719 | + print(f"[ERROR] Failed to remove '{server_name}' from any hosts") |
| 720 | + return 1 |
| 721 | + |
| 722 | + except Exception as e: |
| 723 | + print(f"Error removing MCP server: {e}") |
| 724 | + return 1 |
| 725 | + |
| 726 | +def handle_mcp_remove_host(host_name: str, no_backup: bool = False, |
| 727 | + dry_run: bool = False, auto_approve: bool = False): |
| 728 | + """Handle 'hatch mcp remove host' command.""" |
| 729 | + try: |
| 730 | + # Validate host type |
| 731 | + try: |
| 732 | + host_type = MCPHostType(host_name) |
| 733 | + except ValueError: |
| 734 | + print(f"Error: Invalid host '{host_name}'. Supported hosts: {[h.value for h in MCPHostType]}") |
| 735 | + return 1 |
| 736 | + |
| 737 | + if dry_run: |
| 738 | + print(f"[DRY RUN] Would remove entire host configuration for '{host_name}'") |
| 739 | + print(f"[DRY RUN] Backup: {'Disabled' if no_backup else 'Enabled'}") |
| 740 | + return 0 |
| 741 | + |
| 742 | + # Confirm operation unless auto-approved |
| 743 | + if not request_confirmation( |
| 744 | + f"Remove entire host configuration for '{host_name}'? This will remove ALL MCP servers from this host.", |
| 745 | + auto_approve |
| 746 | + ): |
| 747 | + print("Operation cancelled.") |
| 748 | + return 0 |
| 749 | + |
| 750 | + # Perform host configuration removal |
| 751 | + mcp_manager = MCPHostConfigurationManager() |
| 752 | + result = mcp_manager.remove_host_configuration( |
| 753 | + hostname=host_name, |
| 754 | + no_backup=no_backup |
| 755 | + ) |
| 756 | + |
| 757 | + if result.success: |
| 758 | + print(f"[SUCCESS] Successfully removed host configuration for '{host_name}'") |
| 759 | + if result.backup_path: |
| 760 | + print(f" Backup created: {result.backup_path}") |
| 761 | + return 0 |
| 762 | + else: |
| 763 | + print(f"[ERROR] Failed to remove host configuration for '{host_name}': {result.error_message}") |
| 764 | + return 1 |
| 765 | + |
| 766 | + except Exception as e: |
| 767 | + print(f"Error removing host configuration: {e}") |
| 768 | + return 1 |
| 769 | + |
635 | 770 | def main(): |
636 | 771 | """Main entry point for Hatch CLI. |
637 | 772 | |
@@ -798,13 +933,26 @@ def main(): |
798 | 933 | mcp_configure_parser.add_argument("--dry-run", action="store_true", help="Preview configuration without execution") |
799 | 934 | mcp_configure_parser.add_argument("--auto-approve", action="store_true", help="Skip confirmation prompts") |
800 | 935 |
|
801 | | - # Remove MCP server command |
802 | | - mcp_remove_parser = mcp_subparsers.add_parser("remove", help="Remove MCP server from host") |
803 | | - mcp_remove_parser.add_argument("host", help="Host platform to remove from (e.g., claude-desktop, cursor)") |
804 | | - mcp_remove_parser.add_argument("server_name", help="Name of the MCP server to remove") |
805 | | - mcp_remove_parser.add_argument("--no-backup", action="store_true", help="Skip backup creation before removal") |
806 | | - mcp_remove_parser.add_argument("--dry-run", action="store_true", help="Preview removal without execution") |
807 | | - mcp_remove_parser.add_argument("--auto-approve", action="store_true", help="Skip confirmation prompts") |
| 936 | + # Remove MCP commands (object-action pattern) |
| 937 | + mcp_remove_subparsers = mcp_subparsers.add_parser("remove", help="Remove MCP servers or host configurations").add_subparsers( |
| 938 | + dest="remove_command", help="Remove command to execute" |
| 939 | + ) |
| 940 | + |
| 941 | + # Remove server command |
| 942 | + mcp_remove_server_parser = mcp_remove_subparsers.add_parser("server", help="Remove MCP server from hosts") |
| 943 | + mcp_remove_server_parser.add_argument("server_name", help="Name of the MCP server to remove") |
| 944 | + mcp_remove_server_parser.add_argument("--host", help="Target hosts (comma-separated or 'all')") |
| 945 | + mcp_remove_server_parser.add_argument("--env", "-e", help="Environment name (for environment-based removal)") |
| 946 | + mcp_remove_server_parser.add_argument("--no-backup", action="store_true", help="Skip backup creation before removal") |
| 947 | + mcp_remove_server_parser.add_argument("--dry-run", action="store_true", help="Preview removal without execution") |
| 948 | + mcp_remove_server_parser.add_argument("--auto-approve", action="store_true", help="Skip confirmation prompts") |
| 949 | + |
| 950 | + # Remove host command |
| 951 | + mcp_remove_host_parser = mcp_remove_subparsers.add_parser("host", help="Remove entire host configuration") |
| 952 | + mcp_remove_host_parser.add_argument("host_name", help="Host platform to remove (e.g., claude-desktop, cursor)") |
| 953 | + mcp_remove_host_parser.add_argument("--no-backup", action="store_true", help="Skip backup creation before removal") |
| 954 | + mcp_remove_host_parser.add_argument("--dry-run", action="store_true", help="Preview removal without execution") |
| 955 | + mcp_remove_host_parser.add_argument("--auto-approve", action="store_true", help="Skip confirmation prompts") |
808 | 956 |
|
809 | 957 | # Package management commands |
810 | 958 | pkg_subparsers = subparsers.add_parser("package", help="Package management commands").add_subparsers( |
@@ -1306,10 +1454,19 @@ def main(): |
1306 | 1454 | ) |
1307 | 1455 |
|
1308 | 1456 | elif args.mcp_command == "remove": |
1309 | | - return handle_mcp_remove( |
1310 | | - args.host, args.server_name, args.no_backup, |
1311 | | - args.dry_run, args.auto_approve |
1312 | | - ) |
| 1457 | + if args.remove_command == "server": |
| 1458 | + return handle_mcp_remove_server( |
| 1459 | + args.server_name, args.host, args.env, args.no_backup, |
| 1460 | + args.dry_run, args.auto_approve |
| 1461 | + ) |
| 1462 | + elif args.remove_command == "host": |
| 1463 | + return handle_mcp_remove_host( |
| 1464 | + args.host_name, args.no_backup, |
| 1465 | + args.dry_run, args.auto_approve |
| 1466 | + ) |
| 1467 | + else: |
| 1468 | + print("Unknown remove command") |
| 1469 | + return 1 |
1313 | 1470 |
|
1314 | 1471 | else: |
1315 | 1472 | print("Unknown MCP command") |
|
0 commit comments