Skip to content

Commit d098b0b

Browse files
author
LittleCoinCoin
committed
feat: implement environment-scoped list hosts command
Add environment-scoped behavior to 'hatch mcp list hosts' command: - Add --env and --detailed flags for environment-specific listing - Modify handle_mcp_list_hosts() to read from environment data - Update CLI argument parser for new flags - Implement environment validation and data retrieval Add environment tracking synchronization methods: - Add get_environment_data() method for environment data access - Add remove_package_host_configuration() for package-level tracking - Add clear_host_from_all_packages_all_envs() for global host removal - Add apply_restored_host_configuration_to_environments() for backup sync Update CLI handlers for environment tracking integration: - Modify remove server, remove host, backup restore handlers - Add environment manager parameter to handler signatures - Integrate automatic environment tracking updates Resolves command specification alignment requirements
1 parent e855749 commit d098b0b

File tree

2 files changed

+228
-27
lines changed

2 files changed

+228
-27
lines changed

hatch/cli_hatch.py

Lines changed: 102 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -184,31 +184,65 @@ def handle_mcp_discover_servers(env_manager: HatchEnvironmentManager, env_name:
184184
print(f"Error discovering servers: {e}")
185185
return 1
186186

187-
def handle_mcp_list_hosts():
188-
"""Handle 'hatch mcp list hosts' command."""
187+
def handle_mcp_list_hosts(env_manager: HatchEnvironmentManager, env_name: Optional[str] = None, detailed: bool = False):
188+
"""Handle 'hatch mcp list hosts' command - shows configured hosts in environment."""
189189
try:
190-
# Import strategies to trigger registration
191-
import hatch.mcp_host_config.strategies
190+
from collections import defaultdict
192191

193-
available_hosts = MCPHostRegistry.detect_available_hosts()
194-
all_hosts = list(MCPHostType)
192+
# Resolve environment name
193+
target_env = env_name or env_manager.get_current_environment()
195194

196-
print("MCP host platforms status:")
197-
print(f"{'Host Platform':<20} {'Status':<15} {'Config Path'}")
198-
print("-" * 70)
195+
# Validate environment exists
196+
if not env_manager.environment_exists(target_env):
197+
available_envs = env_manager.list_environments()
198+
print(f"Error: Environment '{target_env}' does not exist.")
199+
if available_envs:
200+
print(f"Available environments: {', '.join(available_envs)}")
201+
return 1
199202

200-
for host_type in all_hosts:
201-
try:
202-
strategy = MCPHostRegistry.get_strategy(host_type)
203-
config_path = strategy.get_config_path()
204-
is_available = host_type in available_hosts
203+
# Collect hosts from configured_hosts across all packages in environment
204+
hosts = defaultdict(int)
205+
host_details = defaultdict(list)
205206

206-
status = "Available" if is_available else "Not detected"
207-
config_display = str(config_path) if config_path else "N/A"
207+
try:
208+
env_data = env_manager.get_environment_data(target_env)
209+
packages = env_data.get("packages", [])
208210

209-
print(f"{host_type.value:<20} {status:<15} {config_display}")
210-
except Exception as e:
211-
print(f"{host_type.value:<20} {'Error':<15} {str(e)}")
211+
for package in packages:
212+
package_name = package.get("name", "unknown")
213+
configured_hosts = package.get("configured_hosts", {})
214+
215+
for host_name, host_config in configured_hosts.items():
216+
hosts[host_name] += 1
217+
if detailed:
218+
config_path = host_config.get("config_path", "N/A")
219+
configured_at = host_config.get("configured_at", "N/A")
220+
host_details[host_name].append({
221+
"package": package_name,
222+
"config_path": config_path,
223+
"configured_at": configured_at
224+
})
225+
226+
except Exception as e:
227+
print(f"Error reading environment data: {e}")
228+
return 1
229+
230+
# Display results
231+
if not hosts:
232+
print(f"No configured hosts for environment '{target_env}'")
233+
return 0
234+
235+
print(f"Configured hosts for environment '{target_env}':")
236+
237+
for host_name, package_count in sorted(hosts.items()):
238+
if detailed:
239+
print(f"\n{host_name} ({package_count} packages):")
240+
for detail in host_details[host_name]:
241+
print(f" - Package: {detail['package']}")
242+
print(f" Config path: {detail['config_path']}")
243+
print(f" Configured at: {detail['configured_at']}")
244+
else:
245+
print(f" - {host_name} ({package_count} packages)")
212246

213247
return 0
214248
except Exception as e:
@@ -303,7 +337,7 @@ def __init__(self, data):
303337
print(f"Error listing servers: {e}")
304338
return 1
305339

306-
def handle_mcp_backup_restore(host: str, backup_file: Optional[str] = None, dry_run: bool = False, auto_approve: bool = False):
340+
def handle_mcp_backup_restore(env_manager: HatchEnvironmentManager, host: str, backup_file: Optional[str] = None, dry_run: bool = False, auto_approve: bool = False):
307341
"""Handle 'hatch mcp backup restore' command."""
308342
try:
309343
from hatch.mcp_host_config.backup import MCPHostConfigBackupManager
@@ -349,6 +383,34 @@ def handle_mcp_backup_restore(host: str, backup_file: Optional[str] = None, dry_
349383

350384
if success:
351385
print(f"[SUCCESS] Successfully restored backup '{backup_file}' for host '{host}'")
386+
387+
# Read restored configuration to get actual server list
388+
try:
389+
# Import strategies to trigger registration
390+
import hatch.mcp_host_config.strategies
391+
392+
host_type = MCPHostType(host)
393+
strategy = MCPHostRegistry.get_strategy(host_type)
394+
restored_config = strategy.read_configuration()
395+
396+
# Get servers dict from restored configuration
397+
if hasattr(restored_config, 'get_servers_dict'):
398+
restored_servers = restored_config.get_servers_dict()
399+
elif hasattr(restored_config, 'mcpServers'):
400+
# Handle Claude Desktop format
401+
restored_servers = restored_config.mcpServers or {}
402+
else:
403+
# Fallback - try to get servers as dict
404+
restored_servers = getattr(restored_config, 'servers', {})
405+
406+
# Update environment tracking to match restored state
407+
updates_count = env_manager.apply_restored_host_configuration_to_environments(host, restored_servers)
408+
if updates_count > 0:
409+
print(f"Synchronized {updates_count} package entries with restored configuration")
410+
411+
except Exception as e:
412+
print(f"Warning: Could not synchronize environment tracking: {e}")
413+
352414
return 0
353415
else:
354416
print(f"[ERROR] Failed to restore backup '{backup_file}' for host '{host}'")
@@ -663,7 +725,7 @@ def parse_host_list(host_arg: str) -> List[str]:
663725

664726
return hosts
665727

666-
def handle_mcp_remove_server(server_name: str, hosts: Optional[str] = None,
728+
def handle_mcp_remove_server(env_manager: HatchEnvironmentManager, server_name: str, hosts: Optional[str] = None,
667729
env: Optional[str] = None, no_backup: bool = False,
668730
dry_run: bool = False, auto_approve: bool = False):
669731
"""Handle 'hatch mcp remove server' command."""
@@ -714,6 +776,11 @@ def handle_mcp_remove_server(server_name: str, hosts: Optional[str] = None,
714776
if result.backup_path:
715777
print(f" Backup created: {result.backup_path}")
716778
success_count += 1
779+
780+
# Update environment tracking for current environment only
781+
current_env = env_manager.get_current_environment()
782+
if current_env:
783+
env_manager.remove_package_host_configuration(current_env, server_name, host)
717784
else:
718785
print(f"[ERROR] Failed to remove '{server_name}' from '{host}': {result.error_message}")
719786

@@ -732,7 +799,7 @@ def handle_mcp_remove_server(server_name: str, hosts: Optional[str] = None,
732799
print(f"Error removing MCP server: {e}")
733800
return 1
734801

735-
def handle_mcp_remove_host(host_name: str, no_backup: bool = False,
802+
def handle_mcp_remove_host(env_manager: HatchEnvironmentManager, host_name: str, no_backup: bool = False,
736803
dry_run: bool = False, auto_approve: bool = False):
737804
"""Handle 'hatch mcp remove host' command."""
738805
try:
@@ -767,6 +834,12 @@ def handle_mcp_remove_host(host_name: str, no_backup: bool = False,
767834
print(f"[SUCCESS] Successfully removed host configuration for '{host_name}'")
768835
if result.backup_path:
769836
print(f" Backup created: {result.backup_path}")
837+
838+
# Update environment tracking across all environments
839+
updates_count = env_manager.clear_host_from_all_packages_all_envs(host_name)
840+
if updates_count > 0:
841+
print(f"Updated {updates_count} package entries across environments")
842+
770843
return 0
771844
else:
772845
print(f"[ERROR] Failed to remove host configuration for '{host_name}': {result.error_message}")
@@ -982,7 +1055,9 @@ def main():
9821055
)
9831056

9841057
# List hosts command
985-
mcp_list_hosts_parser = mcp_list_subparsers.add_parser("hosts", help="List detected MCP host platforms with status")
1058+
mcp_list_hosts_parser = mcp_list_subparsers.add_parser("hosts", help="List configured MCP hosts from environment")
1059+
mcp_list_hosts_parser.add_argument("--env", "-e", default=None, help="Environment name (default: current environment)")
1060+
mcp_list_hosts_parser.add_argument("--detailed", action="store_true", help="Show detailed host configuration information")
9861061

9871062
# List servers command
9881063
mcp_list_servers_parser = mcp_list_subparsers.add_parser("servers", help="List configured MCP servers from environment")
@@ -1541,7 +1616,7 @@ def main():
15411616

15421617
elif args.mcp_command == "list":
15431618
if args.list_command == "hosts":
1544-
return handle_mcp_list_hosts()
1619+
return handle_mcp_list_hosts(env_manager, args.env, args.detailed)
15451620
elif args.list_command == "servers":
15461621
return handle_mcp_list_servers(env_manager, args.env)
15471622
else:
@@ -1551,7 +1626,7 @@ def main():
15511626
elif args.mcp_command == "backup":
15521627
if args.backup_command == "restore":
15531628
return handle_mcp_backup_restore(
1554-
args.host, args.backup_file, args.dry_run, args.auto_approve
1629+
env_manager, args.host, args.backup_file, args.dry_run, args.auto_approve
15551630
)
15561631
elif args.backup_command == "list":
15571632
return handle_mcp_backup_list(args.host, args.detailed)
@@ -1574,12 +1649,12 @@ def main():
15741649
elif args.mcp_command == "remove":
15751650
if args.remove_command == "server":
15761651
return handle_mcp_remove_server(
1577-
args.server_name, args.host, args.env, args.no_backup,
1652+
env_manager, args.server_name, args.host, args.env, args.no_backup,
15781653
args.dry_run, args.auto_approve
15791654
)
15801655
elif args.remove_command == "host":
15811656
return handle_mcp_remove_host(
1582-
args.host_name, args.no_backup,
1657+
env_manager, args.host_name, args.no_backup,
15831658
args.dry_run, args.auto_approve
15841659
)
15851660
else:

hatch/environment_manager.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,20 @@ def get_current_environment(self) -> str:
170170
def get_current_environment_data(self) -> Dict:
171171
"""Get the data for the current environment."""
172172
return self._environments[self._current_env_name]
173+
174+
def get_environment_data(self, env_name: str) -> Dict:
175+
"""Get the data for a specific environment.
176+
177+
Args:
178+
env_name: Name of the environment
179+
180+
Returns:
181+
Dict: Environment data
182+
183+
Raises:
184+
KeyError: If environment doesn't exist
185+
"""
186+
return self._environments[env_name]
173187

174188
def set_current_environment(self, env_name: str) -> bool:
175189
"""
@@ -648,6 +662,118 @@ def update_package_host_configuration(self, env_name: str, package_name: str,
648662
self.logger.error(f"Failed to update package host configuration: {e}")
649663
return False
650664

665+
def remove_package_host_configuration(self, env_name: str, package_name: str, hostname: str) -> bool:
666+
"""Remove host configuration tracking for a specific package.
667+
668+
Args:
669+
env_name: Environment name
670+
package_name: Package name (maps to server name in current 1:1 design)
671+
hostname: Host identifier to remove
672+
673+
Returns:
674+
bool: True if removal occurred, False if package/host not found
675+
"""
676+
try:
677+
if env_name not in self._environments:
678+
self.logger.warning(f"Environment {env_name} does not exist")
679+
return False
680+
681+
packages = self._environments[env_name].get("packages", [])
682+
for pkg in packages:
683+
if pkg.get("name") == package_name:
684+
configured_hosts = pkg.get("configured_hosts", {})
685+
if hostname in configured_hosts:
686+
del configured_hosts[hostname]
687+
self._save_environments()
688+
self.logger.info(f"Removed host {hostname} from package {package_name} in env {env_name}")
689+
return True
690+
691+
return False
692+
693+
except Exception as e:
694+
self.logger.error(f"Failed to remove package host configuration: {e}")
695+
return False
696+
697+
def clear_host_from_all_packages_all_envs(self, hostname: str) -> int:
698+
"""Remove host from all packages across all environments.
699+
700+
Args:
701+
hostname: Host identifier to remove globally
702+
703+
Returns:
704+
int: Number of package entries updated
705+
"""
706+
updates_count = 0
707+
708+
try:
709+
for env_name, env_data in self._environments.items():
710+
packages = env_data.get("packages", [])
711+
for pkg in packages:
712+
configured_hosts = pkg.get("configured_hosts", {})
713+
if hostname in configured_hosts:
714+
del configured_hosts[hostname]
715+
updates_count += 1
716+
self.logger.info(f"Removed host {hostname} from package {pkg.get('name')} in env {env_name}")
717+
718+
if updates_count > 0:
719+
self._save_environments()
720+
721+
return updates_count
722+
723+
except Exception as e:
724+
self.logger.error(f"Failed to clear host from all packages: {e}")
725+
return 0
726+
727+
def apply_restored_host_configuration_to_environments(self, hostname: str, restored_servers: dict) -> int:
728+
"""Update environment tracking to match restored host configuration.
729+
730+
Args:
731+
hostname: Host that was restored
732+
restored_servers: Dict mapping server_name -> server_config from restored host file
733+
734+
Returns:
735+
int: Number of package entries updated across all environments
736+
"""
737+
updates_count = 0
738+
739+
try:
740+
from datetime import datetime
741+
current_time = datetime.now().isoformat()
742+
743+
for env_name, env_data in self._environments.items():
744+
packages = env_data.get("packages", [])
745+
for pkg in packages:
746+
package_name = pkg.get("name")
747+
configured_hosts = pkg.get("configured_hosts", {})
748+
749+
# Check if this package corresponds to a restored server
750+
if package_name in restored_servers:
751+
# Server exists in restored config - ensure tracking exists and is current
752+
server_config = restored_servers[package_name]
753+
configured_hosts[hostname] = {
754+
"config_path": self._get_host_config_path(hostname),
755+
"configured_at": configured_hosts.get(hostname, {}).get("configured_at", current_time),
756+
"last_synced": current_time,
757+
"server_config": server_config
758+
}
759+
updates_count += 1
760+
self.logger.info(f"Updated host {hostname} tracking for package {package_name} in env {env_name}")
761+
762+
elif hostname in configured_hosts:
763+
# Server not in restored config but was previously tracked - remove stale tracking
764+
del configured_hosts[hostname]
765+
updates_count += 1
766+
self.logger.info(f"Removed stale host {hostname} tracking for package {package_name} in env {env_name}")
767+
768+
if updates_count > 0:
769+
self._save_environments()
770+
771+
return updates_count
772+
773+
except Exception as e:
774+
self.logger.error(f"Failed to apply restored host configuration: {e}")
775+
return 0
776+
651777
def _get_host_config_path(self, hostname: str) -> str:
652778
"""Get configuration file path for a host.
653779

0 commit comments

Comments
 (0)