Skip to content

Commit b172ab4

Browse files
author
LittleCoinCoin
committed
feat(cli): implement object-action pattern for MCP remove commands
Refactor 'hatch mcp remove' to support object-action CLI pattern: - 'hatch mcp remove server <name> --host <hosts>' for server removal - 'hatch mcp remove host <name>' for complete host configuration removal Key improvements: - Multi-host support with comma-separated lists and 'all' keyword - parse_host_list() function for flexible host specification - handle_mcp_remove_server() and handle_mcp_remove_host() handlers - Cross-platform Unicode character fix for Windows console compatibility - Comprehensive dry-run support and user confirmation Implements Phase 3e specification requirements for Direct MCP Management.
1 parent ca82163 commit b172ab4

File tree

1 file changed

+169
-12
lines changed

1 file changed

+169
-12
lines changed

hatch/cli_hatch.py

Lines changed: 169 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import logging
1313
import sys
1414
from pathlib import Path
15-
from typing import Optional
15+
from typing import Optional, List
1616

1717
from hatch.environment_manager import HatchEnvironmentManager
1818
from hatch_validator import HatchPackageValidator
@@ -632,6 +632,141 @@ def handle_mcp_remove(host: str, server_name: str, no_backup: bool = False,
632632
print(f"Error removing MCP server: {e}")
633633
return 1
634634

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+
635770
def main():
636771
"""Main entry point for Hatch CLI.
637772
@@ -798,13 +933,26 @@ def main():
798933
mcp_configure_parser.add_argument("--dry-run", action="store_true", help="Preview configuration without execution")
799934
mcp_configure_parser.add_argument("--auto-approve", action="store_true", help="Skip confirmation prompts")
800935

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")
808956

809957
# Package management commands
810958
pkg_subparsers = subparsers.add_parser("package", help="Package management commands").add_subparsers(
@@ -1306,10 +1454,19 @@ def main():
13061454
)
13071455

13081456
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
13131470

13141471
else:
13151472
print("Unknown MCP command")

0 commit comments

Comments
 (0)