Skip to content

Commit ac88a84

Browse files
refactor(cli): rewrite mcp list hosts for host-centric design
1 parent 3ec0617 commit ac88a84

File tree

1 file changed

+106
-66
lines changed

1 file changed

+106
-66
lines changed

hatch/cli/cli_mcp.py

Lines changed: 106 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -219,94 +219,134 @@ def handle_mcp_discover_servers(args: Namespace) -> int:
219219

220220

221221
def handle_mcp_list_hosts(args: Namespace) -> int:
222-
"""Handle 'hatch mcp list hosts' command - shows configured hosts in environment.
222+
"""Handle 'hatch mcp list hosts' command - host-centric design.
223+
224+
Lists host/server pairs from host configuration files. Shows ALL servers
225+
on hosts (both Hatch-managed and 3rd party) with Hatch management status.
223226
224227
Args:
225228
args: Parsed command-line arguments containing:
226229
- env_manager: HatchEnvironmentManager instance
227-
- env: Optional environment name (uses current if not specified)
228-
- detailed: Whether to show detailed host information
230+
- server: Optional regex pattern to filter by server name
229231
- json: Optional flag for JSON output
230232
231233
Returns:
232234
int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
235+
236+
Reference: R10 §3.1 (10-namespace_consistency_specification_v2.md)
233237
"""
234238
try:
235239
import json as json_module
236-
from collections import defaultdict
237-
240+
import re
241+
# Import strategies to trigger registration
242+
import hatch.mcp_host_config.strategies
243+
238244
env_manager: HatchEnvironmentManager = args.env_manager
239-
env_name: Optional[str] = getattr(args, 'env', None)
240-
detailed: bool = getattr(args, 'detailed', False)
245+
server_pattern: Optional[str] = getattr(args, 'server', None)
241246
json_output: bool = getattr(args, 'json', False)
242-
243-
# Resolve environment name
244-
target_env = env_name or env_manager.get_current_environment()
245-
246-
# Validate environment exists
247-
if not env_manager.environment_exists(target_env):
248-
available_envs = env_manager.list_environments()
249-
print(f"Error: Environment '{target_env}' does not exist.")
250-
if available_envs:
251-
print(f"Available environments: {', '.join(available_envs)}")
252-
return EXIT_ERROR
253-
254-
# Collect hosts from configured_hosts across all packages in environment
255-
hosts = defaultdict(int)
256-
host_last_sync = {}
257-
258-
try:
259-
env_data = env_manager.get_environment_data(target_env)
260-
packages = env_data.get("packages", [])
261-
262-
for package in packages:
263-
configured_hosts = package.get("configured_hosts", {})
264-
265-
for host_name, host_config in configured_hosts.items():
266-
hosts[host_name] += 1
267-
# Track most recent sync time
268-
configured_at = host_config.get("configured_at", "N/A")
269-
if host_name not in host_last_sync or configured_at > host_last_sync.get(host_name, ""):
270-
host_last_sync[host_name] = configured_at
271-
272-
except Exception as e:
273-
print(f"Error reading environment data: {e}")
274-
return EXIT_ERROR
275-
276-
# JSON output
247+
248+
# Compile regex pattern if provided
249+
pattern_re = None
250+
if server_pattern:
251+
try:
252+
pattern_re = re.compile(server_pattern)
253+
except re.error as e:
254+
print(f"Error: Invalid regex pattern '{server_pattern}': {e}")
255+
return EXIT_ERROR
256+
257+
# Build Hatch management lookup: {server_name: {host: env_name}}
258+
hatch_managed = {}
259+
for env_info in env_manager.list_environments():
260+
env_name = env_info.get("name", env_info) if isinstance(env_info, dict) else env_info
261+
try:
262+
env_data = env_manager.get_environment_data(env_name)
263+
packages = env_data.get("packages", []) if isinstance(env_data, dict) else getattr(env_data, 'packages', [])
264+
265+
for pkg in packages:
266+
pkg_name = pkg.get("name") if isinstance(pkg, dict) else getattr(pkg, 'name', None)
267+
configured_hosts = pkg.get("configured_hosts", {}) if isinstance(pkg, dict) else getattr(pkg, 'configured_hosts', {})
268+
269+
if pkg_name:
270+
if pkg_name not in hatch_managed:
271+
hatch_managed[pkg_name] = {}
272+
for host_name in configured_hosts.keys():
273+
hatch_managed[pkg_name][host_name] = env_name
274+
except Exception:
275+
continue
276+
277+
# Get all available hosts and read their configurations
278+
available_hosts = MCPHostRegistry.detect_available_hosts()
279+
280+
# Collect host/server pairs from host config files
281+
# Format: (host, server, is_hatch_managed, env_name)
282+
host_rows = []
283+
284+
for host_type in available_hosts:
285+
try:
286+
strategy = MCPHostRegistry.get_strategy(host_type)
287+
host_config = strategy.read_configuration()
288+
host_name = host_type.value
289+
290+
for server_name, server_config in host_config.servers.items():
291+
# Apply server pattern filter if specified
292+
if pattern_re and not pattern_re.search(server_name):
293+
continue
294+
295+
# Check if Hatch-managed
296+
is_hatch_managed = False
297+
env_name = None
298+
299+
if server_name in hatch_managed:
300+
host_info = hatch_managed[server_name].get(host_name)
301+
if host_info:
302+
is_hatch_managed = True
303+
env_name = host_info
304+
305+
host_rows.append((host_name, server_name, is_hatch_managed, env_name))
306+
except Exception:
307+
# Skip hosts that can't be read
308+
continue
309+
310+
# Sort rows by host (alphabetically), then by server
311+
host_rows.sort(key=lambda x: (x[0], x[1]))
312+
313+
# JSON output per R10 §8
277314
if json_output:
278-
hosts_data = []
279-
for host_name, package_count in sorted(hosts.items()):
280-
hosts_data.append({
281-
"host": host_name,
282-
"package_count": package_count,
283-
"last_synced": host_last_sync.get(host_name, None)
315+
rows_data = []
316+
for host, server, is_hatch, env in host_rows:
317+
rows_data.append({
318+
"host": host,
319+
"server": server,
320+
"hatch_managed": is_hatch,
321+
"environment": env
284322
})
285-
print(json_module.dumps({
286-
"environment": target_env,
287-
"hosts": hosts_data
288-
}, indent=2))
323+
print(json_module.dumps({"rows": rows_data}, indent=2))
289324
return EXIT_SUCCESS
290-
325+
291326
# Display results
292-
if not hosts:
293-
print(f"No configured hosts for environment '{target_env}'")
327+
if not host_rows:
328+
if server_pattern:
329+
print(f"No MCP servers matching '{server_pattern}' on any host")
330+
else:
331+
print("No MCP servers found on any available hosts")
294332
return EXIT_SUCCESS
295-
296-
print(f"Configured Hosts (environment: {target_env}):")
297333

298-
# Define table columns per R02 §2.4
334+
print("MCP Hosts:")
335+
336+
# Define table columns per R10 §3.1: Host → Server → Hatch → Environment
299337
columns = [
300-
ColumnDef(name="Host", width=20),
301-
ColumnDef(name="Packages", width=10, align="right"),
302-
ColumnDef(name="Last Synced", width="auto"),
338+
ColumnDef(name="Host", width=18),
339+
ColumnDef(name="Server", width=18),
340+
ColumnDef(name="Hatch", width=8),
341+
ColumnDef(name="Environment", width=15),
303342
]
304343
formatter = TableFormatter(columns)
305-
306-
for host_name, package_count in sorted(hosts.items()):
307-
last_sync = host_last_sync.get(host_name, "N/A")
308-
formatter.add_row([host_name, str(package_count), last_sync])
309-
344+
345+
for host, server, is_hatch, env in host_rows:
346+
hatch_status = "✅" if is_hatch else "❌"
347+
env_display = env if env else "-"
348+
formatter.add_row([host, server, hatch_status, env_display])
349+
310350
print(formatter.render())
311351
return EXIT_SUCCESS
312352
except Exception as e:

0 commit comments

Comments
 (0)