hatch-xclam 0.7.1.dev3__py3-none-any.whl → 0.8.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. hatch/__init__.py +1 -1
  2. hatch/cli/__init__.py +71 -0
  3. hatch/cli/__main__.py +1035 -0
  4. hatch/cli/cli_env.py +865 -0
  5. hatch/cli/cli_mcp.py +1965 -0
  6. hatch/cli/cli_package.py +566 -0
  7. hatch/cli/cli_system.py +136 -0
  8. hatch/cli/cli_utils.py +1289 -0
  9. hatch/cli_hatch.py +160 -2838
  10. hatch/mcp_host_config/__init__.py +10 -10
  11. hatch/mcp_host_config/adapters/__init__.py +34 -0
  12. hatch/mcp_host_config/adapters/base.py +170 -0
  13. hatch/mcp_host_config/adapters/claude.py +105 -0
  14. hatch/mcp_host_config/adapters/codex.py +104 -0
  15. hatch/mcp_host_config/adapters/cursor.py +83 -0
  16. hatch/mcp_host_config/adapters/gemini.py +75 -0
  17. hatch/mcp_host_config/adapters/kiro.py +78 -0
  18. hatch/mcp_host_config/adapters/lmstudio.py +79 -0
  19. hatch/mcp_host_config/adapters/registry.py +149 -0
  20. hatch/mcp_host_config/adapters/vscode.py +83 -0
  21. hatch/mcp_host_config/backup.py +5 -3
  22. hatch/mcp_host_config/fields.py +126 -0
  23. hatch/mcp_host_config/models.py +161 -456
  24. hatch/mcp_host_config/reporting.py +57 -16
  25. hatch/mcp_host_config/strategies.py +155 -87
  26. hatch/template_generator.py +1 -1
  27. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/METADATA +3 -2
  28. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/RECORD +52 -43
  29. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/WHEEL +1 -1
  30. hatch_xclam-0.8.0.dev1.dist-info/entry_points.txt +2 -0
  31. tests/cli_test_utils.py +280 -0
  32. tests/integration/cli/__init__.py +14 -0
  33. tests/integration/cli/test_cli_reporter_integration.py +2439 -0
  34. tests/integration/mcp/__init__.py +0 -0
  35. tests/integration/mcp/test_adapter_serialization.py +173 -0
  36. tests/regression/cli/__init__.py +16 -0
  37. tests/regression/cli/test_color_logic.py +268 -0
  38. tests/regression/cli/test_consequence_type.py +298 -0
  39. tests/regression/cli/test_error_formatting.py +328 -0
  40. tests/regression/cli/test_result_reporter.py +586 -0
  41. tests/regression/cli/test_table_formatter.py +211 -0
  42. tests/regression/mcp/__init__.py +0 -0
  43. tests/regression/mcp/test_field_filtering.py +162 -0
  44. tests/test_cli_version.py +7 -5
  45. tests/test_data/fixtures/cli_reporter_fixtures.py +184 -0
  46. tests/unit/__init__.py +0 -0
  47. tests/unit/mcp/__init__.py +0 -0
  48. tests/unit/mcp/test_adapter_protocol.py +138 -0
  49. tests/unit/mcp/test_adapter_registry.py +158 -0
  50. tests/unit/mcp/test_config_model.py +146 -0
  51. hatch_xclam-0.7.1.dev3.dist-info/entry_points.txt +0 -2
  52. tests/integration/test_mcp_kiro_integration.py +0 -153
  53. tests/regression/test_mcp_codex_backup_integration.py +0 -162
  54. tests/regression/test_mcp_codex_host_strategy.py +0 -163
  55. tests/regression/test_mcp_codex_model_validation.py +0 -117
  56. tests/regression/test_mcp_kiro_backup_integration.py +0 -241
  57. tests/regression/test_mcp_kiro_cli_integration.py +0 -141
  58. tests/regression/test_mcp_kiro_decorator_registration.py +0 -71
  59. tests/regression/test_mcp_kiro_host_strategy.py +0 -214
  60. tests/regression/test_mcp_kiro_model_validation.py +0 -116
  61. tests/regression/test_mcp_kiro_omni_conversion.py +0 -104
  62. tests/test_mcp_atomic_operations.py +0 -276
  63. tests/test_mcp_backup_integration.py +0 -308
  64. tests/test_mcp_cli_all_host_specific_args.py +0 -496
  65. tests/test_mcp_cli_backup_management.py +0 -295
  66. tests/test_mcp_cli_direct_management.py +0 -456
  67. tests/test_mcp_cli_discovery_listing.py +0 -582
  68. tests/test_mcp_cli_host_config_integration.py +0 -823
  69. tests/test_mcp_cli_package_management.py +0 -360
  70. tests/test_mcp_cli_partial_updates.py +0 -859
  71. tests/test_mcp_environment_integration.py +0 -520
  72. tests/test_mcp_host_config_backup.py +0 -257
  73. tests/test_mcp_host_configuration_manager.py +0 -331
  74. tests/test_mcp_host_registry_decorator.py +0 -348
  75. tests/test_mcp_pydantic_architecture_v4.py +0 -603
  76. tests/test_mcp_server_config_models.py +0 -242
  77. tests/test_mcp_server_config_type_field.py +0 -221
  78. tests/test_mcp_sync_functionality.py +0 -316
  79. tests/test_mcp_user_feedback_reporting.py +0 -359
  80. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/licenses/LICENSE +0 -0
  81. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/top_level.txt +0 -0
hatch/cli/cli_mcp.py ADDED
@@ -0,0 +1,1965 @@
1
+ """MCP host configuration handlers for Hatch CLI.
2
+
3
+ This module provides handlers for MCP (Model Context Protocol) host configuration
4
+ commands. MCP enables AI assistants to interact with external tools and services
5
+ through a standardized protocol.
6
+
7
+ Supported Hosts:
8
+ - claude-desktop: Claude Desktop application
9
+ - claude-code: Claude Code extension
10
+ - cursor: Cursor IDE
11
+ - vscode: Visual Studio Code with Copilot
12
+ - kiro: Kiro IDE
13
+ - codex: OpenAI Codex
14
+ - lm-studio: LM Studio
15
+ - gemini: Google Gemini
16
+
17
+ Command Groups:
18
+ Discovery:
19
+ - hatch mcp discover hosts: Detect available MCP host platforms
20
+ - hatch mcp discover servers: Find MCP servers in packages
21
+
22
+ Listing:
23
+ - hatch mcp list hosts: Show configured hosts in environment
24
+ - hatch mcp list servers: Show configured servers
25
+
26
+ Backup:
27
+ - hatch mcp backup restore: Restore configuration from backup
28
+ - hatch mcp backup list: List available backups
29
+ - hatch mcp backup clean: Clean old backups
30
+
31
+ Configuration:
32
+ - hatch mcp configure: Add or update MCP server configuration
33
+ - hatch mcp remove: Remove server from specific host
34
+ - hatch mcp remove-server: Remove server from multiple hosts
35
+ - hatch mcp remove-host: Remove all servers from a host
36
+
37
+ Synchronization:
38
+ - hatch mcp sync: Sync package servers to hosts
39
+
40
+ Handler Signature:
41
+ All handlers follow: (args: Namespace) -> int
42
+ Returns EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure.
43
+
44
+ Example:
45
+ $ hatch mcp discover hosts
46
+ $ hatch mcp configure claude-desktop my-server --command python --args server.py
47
+ $ hatch mcp backup list claude-desktop --detailed
48
+ """
49
+
50
+ from argparse import Namespace
51
+ from pathlib import Path
52
+ from typing import Optional
53
+
54
+ from hatch.environment_manager import HatchEnvironmentManager
55
+ from hatch.mcp_host_config import (
56
+ MCPHostConfigurationManager,
57
+ MCPHostRegistry,
58
+ MCPHostType,
59
+ MCPServerConfig,
60
+ )
61
+
62
+ from hatch.cli.cli_utils import (
63
+ EXIT_SUCCESS,
64
+ EXIT_ERROR,
65
+ get_package_mcp_server_config,
66
+ TableFormatter,
67
+ ColumnDef,
68
+ ValidationError,
69
+ format_validation_error,
70
+ format_info,
71
+ ResultReporter,
72
+ )
73
+
74
+
75
+ def handle_mcp_discover_hosts(args: Namespace) -> int:
76
+ """Handle 'hatch mcp discover hosts' command.
77
+
78
+ Detects and displays available MCP host platforms on the system.
79
+
80
+ Args:
81
+ args: Parsed command-line arguments containing:
82
+ - json: Optional flag for JSON output
83
+
84
+ Returns:
85
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
86
+ """
87
+ try:
88
+ import json as json_module
89
+
90
+ # Import strategies to trigger registration
91
+ import hatch.mcp_host_config.strategies
92
+
93
+ json_output: bool = getattr(args, 'json', False)
94
+ available_hosts = MCPHostRegistry.detect_available_hosts()
95
+
96
+ if json_output:
97
+ # JSON output
98
+ hosts_data = []
99
+ for host_type in MCPHostType:
100
+ try:
101
+ strategy = MCPHostRegistry.get_strategy(host_type)
102
+ config_path = strategy.get_config_path()
103
+ is_available = host_type in available_hosts
104
+
105
+ hosts_data.append({
106
+ "host": host_type.value,
107
+ "available": is_available,
108
+ "config_path": str(config_path) if config_path else None
109
+ })
110
+ except Exception as e:
111
+ hosts_data.append({
112
+ "host": host_type.value,
113
+ "available": False,
114
+ "error": str(e)
115
+ })
116
+
117
+ print(json_module.dumps({"hosts": hosts_data}, indent=2))
118
+ return EXIT_SUCCESS
119
+
120
+ # Table output
121
+ print("Available MCP Host Platforms:")
122
+
123
+ # Define table columns per R02 §2.3
124
+ columns = [
125
+ ColumnDef(name="Host", width=18),
126
+ ColumnDef(name="Status", width=15),
127
+ ColumnDef(name="Config Path", width="auto"),
128
+ ]
129
+ formatter = TableFormatter(columns)
130
+
131
+ for host_type in MCPHostType:
132
+ try:
133
+ strategy = MCPHostRegistry.get_strategy(host_type)
134
+ config_path = strategy.get_config_path()
135
+ is_available = host_type in available_hosts
136
+
137
+ status = "✓ Available" if is_available else "✗ Not Found"
138
+ path_str = str(config_path) if config_path else "-"
139
+ formatter.add_row([host_type.value, status, path_str])
140
+ except Exception as e:
141
+ formatter.add_row([host_type.value, f"Error", str(e)[:30]])
142
+
143
+ print(formatter.render())
144
+ return EXIT_SUCCESS
145
+ except Exception as e:
146
+ reporter = ResultReporter("hatch mcp discover hosts")
147
+ reporter.report_error("Failed to discover hosts", details=[f"Reason: {str(e)}"])
148
+ return EXIT_ERROR
149
+
150
+
151
+ def handle_mcp_discover_servers(args: Namespace) -> int:
152
+ """Handle 'hatch mcp discover servers' command.
153
+
154
+ .. deprecated::
155
+ This command is deprecated. Use 'hatch mcp list servers' instead.
156
+
157
+ Discovers MCP servers available in packages within an environment.
158
+
159
+ Args:
160
+ args: Parsed command-line arguments containing:
161
+ - env_manager: HatchEnvironmentManager instance
162
+ - env: Optional environment name (uses current if not specified)
163
+
164
+ Returns:
165
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
166
+ """
167
+ import warnings
168
+ import sys
169
+
170
+ # Emit deprecation warning to stderr
171
+ print(
172
+ "Warning: 'hatch mcp discover servers' is deprecated. "
173
+ "Use 'hatch mcp list servers' instead.",
174
+ file=sys.stderr
175
+ )
176
+
177
+ try:
178
+ env_manager: HatchEnvironmentManager = args.env_manager
179
+ env_name: Optional[str] = getattr(args, 'env', None)
180
+
181
+ env_name = env_name or env_manager.get_current_environment()
182
+
183
+ if not env_manager.environment_exists(env_name):
184
+ format_validation_error(ValidationError(
185
+ f"Environment '{env_name}' does not exist",
186
+ field="--env",
187
+ suggestion="Use 'hatch env list' to see available environments"
188
+ ))
189
+ return EXIT_ERROR
190
+
191
+ packages = env_manager.list_packages(env_name)
192
+ mcp_packages = []
193
+
194
+ for package in packages:
195
+ try:
196
+ # Check if package has MCP server entry point
197
+ server_config = get_package_mcp_server_config(
198
+ env_manager, env_name, package["name"]
199
+ )
200
+ mcp_packages.append(
201
+ {"package": package, "server_config": server_config}
202
+ )
203
+ except ValueError:
204
+ # Package doesn't have MCP server
205
+ continue
206
+
207
+ if not mcp_packages:
208
+ print(f"No MCP servers found in environment '{env_name}'")
209
+ return EXIT_SUCCESS
210
+
211
+ print(f"MCP servers in environment '{env_name}':")
212
+ for item in mcp_packages:
213
+ package = item["package"]
214
+ server_config = item["server_config"]
215
+ print(f" {server_config.name}:")
216
+ print(
217
+ f" Package: {package['name']} v{package.get('version', 'unknown')}"
218
+ )
219
+ print(f" Command: {server_config.command}")
220
+ print(f" Args: {server_config.args}")
221
+ if server_config.env:
222
+ print(f" Environment: {server_config.env}")
223
+
224
+ return EXIT_SUCCESS
225
+ except Exception as e:
226
+ reporter = ResultReporter("hatch mcp discover servers")
227
+ reporter.report_error("Failed to discover servers", details=[f"Reason: {str(e)}"])
228
+ return EXIT_ERROR
229
+
230
+
231
+ def handle_mcp_list_hosts(args: Namespace) -> int:
232
+ """Handle 'hatch mcp list hosts' command - host-centric design.
233
+
234
+ Lists host/server pairs from host configuration files. Shows ALL servers
235
+ on hosts (both Hatch-managed and 3rd party) with Hatch management status.
236
+
237
+ Args:
238
+ args: Parsed command-line arguments containing:
239
+ - env_manager: HatchEnvironmentManager instance
240
+ - server: Optional regex pattern to filter by server name
241
+ - json: Optional flag for JSON output
242
+
243
+ Returns:
244
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
245
+
246
+ Reference: R10 §3.1 (10-namespace_consistency_specification_v2.md)
247
+ """
248
+ try:
249
+ import json as json_module
250
+ import re
251
+ # Import strategies to trigger registration
252
+ import hatch.mcp_host_config.strategies
253
+
254
+ env_manager: HatchEnvironmentManager = args.env_manager
255
+ server_pattern: Optional[str] = getattr(args, 'server', None)
256
+ json_output: bool = getattr(args, 'json', False)
257
+
258
+ # Compile regex pattern if provided
259
+ pattern_re = None
260
+ if server_pattern:
261
+ try:
262
+ pattern_re = re.compile(server_pattern)
263
+ except re.error as e:
264
+ format_validation_error(ValidationError(
265
+ f"Invalid regex pattern '{server_pattern}': {e}",
266
+ field="--server",
267
+ suggestion="Use a valid Python regex pattern"
268
+ ))
269
+ return EXIT_ERROR
270
+
271
+ # Build Hatch management lookup: {server_name: {host: env_name}}
272
+ hatch_managed = {}
273
+ for env_info in env_manager.list_environments():
274
+ env_name = env_info.get("name", env_info) if isinstance(env_info, dict) else env_info
275
+ try:
276
+ env_data = env_manager.get_environment_data(env_name)
277
+ packages = env_data.get("packages", []) if isinstance(env_data, dict) else getattr(env_data, 'packages', [])
278
+
279
+ for pkg in packages:
280
+ pkg_name = pkg.get("name") if isinstance(pkg, dict) else getattr(pkg, 'name', None)
281
+ configured_hosts = pkg.get("configured_hosts", {}) if isinstance(pkg, dict) else getattr(pkg, 'configured_hosts', {})
282
+
283
+ if pkg_name:
284
+ if pkg_name not in hatch_managed:
285
+ hatch_managed[pkg_name] = {}
286
+ for host_name in configured_hosts.keys():
287
+ hatch_managed[pkg_name][host_name] = env_name
288
+ except Exception:
289
+ continue
290
+
291
+ # Get all available hosts and read their configurations
292
+ available_hosts = MCPHostRegistry.detect_available_hosts()
293
+
294
+ # Collect host/server pairs from host config files
295
+ # Format: (host, server, is_hatch_managed, env_name)
296
+ host_rows = []
297
+
298
+ for host_type in available_hosts:
299
+ try:
300
+ strategy = MCPHostRegistry.get_strategy(host_type)
301
+ host_config = strategy.read_configuration()
302
+ host_name = host_type.value
303
+
304
+ for server_name, server_config in host_config.servers.items():
305
+ # Apply server pattern filter if specified
306
+ if pattern_re and not pattern_re.search(server_name):
307
+ continue
308
+
309
+ # Check if Hatch-managed
310
+ is_hatch_managed = False
311
+ env_name = None
312
+
313
+ if server_name in hatch_managed:
314
+ host_info = hatch_managed[server_name].get(host_name)
315
+ if host_info:
316
+ is_hatch_managed = True
317
+ env_name = host_info
318
+
319
+ host_rows.append((host_name, server_name, is_hatch_managed, env_name))
320
+ except Exception:
321
+ # Skip hosts that can't be read
322
+ continue
323
+
324
+ # Sort rows by host (alphabetically), then by server
325
+ host_rows.sort(key=lambda x: (x[0], x[1]))
326
+
327
+ # JSON output per R10 §8
328
+ if json_output:
329
+ rows_data = []
330
+ for host, server, is_hatch, env in host_rows:
331
+ rows_data.append({
332
+ "host": host,
333
+ "server": server,
334
+ "hatch_managed": is_hatch,
335
+ "environment": env
336
+ })
337
+ print(json_module.dumps({"rows": rows_data}, indent=2))
338
+ return EXIT_SUCCESS
339
+
340
+ # Display results
341
+ if not host_rows:
342
+ if server_pattern:
343
+ print(f"No MCP servers matching '{server_pattern}' on any host")
344
+ else:
345
+ print("No MCP servers found on any available hosts")
346
+ return EXIT_SUCCESS
347
+
348
+ print("MCP Hosts:")
349
+
350
+ # Define table columns per R10 §3.1: Host → Server → Hatch → Environment
351
+ columns = [
352
+ ColumnDef(name="Host", width=18),
353
+ ColumnDef(name="Server", width=18),
354
+ ColumnDef(name="Hatch", width=8),
355
+ ColumnDef(name="Environment", width=15),
356
+ ]
357
+ formatter = TableFormatter(columns)
358
+
359
+ for host, server, is_hatch, env in host_rows:
360
+ hatch_status = "✅" if is_hatch else "❌"
361
+ env_display = env if env else "-"
362
+ formatter.add_row([host, server, hatch_status, env_display])
363
+
364
+ print(formatter.render())
365
+ return EXIT_SUCCESS
366
+ except Exception as e:
367
+ reporter = ResultReporter("hatch mcp list hosts")
368
+ reporter.report_error("Failed to list hosts", details=[f"Reason: {str(e)}"])
369
+ return EXIT_ERROR
370
+
371
+
372
+ def handle_mcp_list_servers(args: Namespace) -> int:
373
+ """Handle 'hatch mcp list servers' command.
374
+
375
+ Lists server/host pairs from host configuration files. Shows ALL servers
376
+ on hosts (both Hatch-managed and 3rd party) with Hatch management status.
377
+
378
+ Args:
379
+ args: Parsed command-line arguments containing:
380
+ - env_manager: HatchEnvironmentManager instance
381
+ - host: Optional regex pattern to filter by host name
382
+ - json: Optional flag for JSON output
383
+
384
+ Returns:
385
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
386
+
387
+ Reference: R10 §3.2 (10-namespace_consistency_specification_v2.md)
388
+ """
389
+ try:
390
+ import json as json_module
391
+ import re
392
+ # Import strategies to trigger registration
393
+ import hatch.mcp_host_config.strategies
394
+
395
+ env_manager: HatchEnvironmentManager = args.env_manager
396
+ host_pattern: Optional[str] = getattr(args, 'host', None)
397
+ json_output: bool = getattr(args, 'json', False)
398
+
399
+ # Compile host regex pattern if provided
400
+ host_re = None
401
+ if host_pattern:
402
+ try:
403
+ host_re = re.compile(host_pattern)
404
+ except re.error as e:
405
+ format_validation_error(ValidationError(
406
+ f"Invalid regex pattern '{host_pattern}': {e}",
407
+ field="--host",
408
+ suggestion="Use a valid Python regex pattern"
409
+ ))
410
+ return EXIT_ERROR
411
+
412
+ # Get all available hosts
413
+ available_hosts = MCPHostRegistry.detect_available_hosts()
414
+
415
+ # Build Hatch management lookup: {server_name: {host: (env_name, version)}}
416
+ hatch_managed = {}
417
+ for env_info in env_manager.list_environments():
418
+ env_name = env_info.get("name", env_info) if isinstance(env_info, dict) else env_info
419
+ try:
420
+ env_data = env_manager.get_environment_data(env_name)
421
+ packages = env_data.get("packages", []) if isinstance(env_data, dict) else getattr(env_data, 'packages', [])
422
+
423
+ for pkg in packages:
424
+ pkg_name = pkg.get("name") if isinstance(pkg, dict) else getattr(pkg, 'name', None)
425
+ pkg_version = pkg.get("version", "-") if isinstance(pkg, dict) else getattr(pkg, 'version', '-')
426
+ configured_hosts = pkg.get("configured_hosts", {}) if isinstance(pkg, dict) else getattr(pkg, 'configured_hosts', {})
427
+
428
+ if pkg_name:
429
+ if pkg_name not in hatch_managed:
430
+ hatch_managed[pkg_name] = {}
431
+ for host_name in configured_hosts.keys():
432
+ hatch_managed[pkg_name][host_name] = (env_name, pkg_version)
433
+ except Exception:
434
+ continue
435
+
436
+ # Collect server data from host config files
437
+ # Format: (server_name, host, is_hatch_managed, env_name, version)
438
+ server_rows = []
439
+
440
+ for host_type in available_hosts:
441
+ try:
442
+ strategy = MCPHostRegistry.get_strategy(host_type)
443
+ host_config = strategy.read_configuration()
444
+ host_name = host_type.value
445
+
446
+ # Apply host pattern filter if specified
447
+ if host_re and not host_re.search(host_name):
448
+ continue
449
+
450
+ for server_name, server_config in host_config.servers.items():
451
+ # Check if Hatch-managed
452
+ is_hatch_managed = False
453
+ env_name = "-"
454
+ version = "-"
455
+
456
+ if server_name in hatch_managed:
457
+ host_info = hatch_managed[server_name].get(host_name)
458
+ if host_info:
459
+ is_hatch_managed = True
460
+ env_name, version = host_info
461
+
462
+ server_rows.append((server_name, host_name, is_hatch_managed, env_name, version))
463
+ except Exception:
464
+ # Skip hosts that can't be read
465
+ continue
466
+
467
+ # Sort rows by server (alphabetically), then by host per R10 §3.2
468
+ server_rows.sort(key=lambda x: (x[0], x[1]))
469
+
470
+ # JSON output
471
+ if json_output:
472
+ servers_data = []
473
+ for server_name, host, is_hatch, env, version in server_rows:
474
+ server_entry = {
475
+ "server": server_name,
476
+ "host": host,
477
+ "hatch_managed": is_hatch,
478
+ "environment": env if is_hatch else None,
479
+ }
480
+ servers_data.append(server_entry)
481
+
482
+ print(json_module.dumps({"rows": servers_data}, indent=2))
483
+ return EXIT_SUCCESS
484
+
485
+ if not server_rows:
486
+ if host_pattern:
487
+ print(f"No MCP servers on hosts matching '{host_pattern}'")
488
+ else:
489
+ print("No MCP servers found on any available hosts")
490
+ return EXIT_SUCCESS
491
+
492
+ print("MCP Servers:")
493
+
494
+ # Define table columns per R10 §3.2: Server → Host → Hatch → Environment
495
+ columns = [
496
+ ColumnDef(name="Server", width=18),
497
+ ColumnDef(name="Host", width=18),
498
+ ColumnDef(name="Hatch", width=8),
499
+ ColumnDef(name="Environment", width=15),
500
+ ]
501
+ formatter = TableFormatter(columns)
502
+
503
+ for server_name, host, is_hatch, env, version in server_rows:
504
+ hatch_status = "✅" if is_hatch else "❌"
505
+ env_display = env if is_hatch else "-"
506
+ formatter.add_row([server_name, host, hatch_status, env_display])
507
+
508
+ print(formatter.render())
509
+ return EXIT_SUCCESS
510
+ except Exception as e:
511
+ reporter = ResultReporter("hatch mcp list servers")
512
+ reporter.report_error("Failed to list servers", details=[f"Reason: {str(e)}"])
513
+ return EXIT_ERROR
514
+
515
+
516
+
517
+ def handle_mcp_show_hosts(args: Namespace) -> int:
518
+ """Handle 'hatch mcp show hosts' command.
519
+
520
+ Shows detailed hierarchical view of all MCP host configurations.
521
+ Supports --server filter for regex pattern matching.
522
+
523
+ Args:
524
+ args: Parsed command-line arguments containing:
525
+ - env_manager: HatchEnvironmentManager instance
526
+ - server: Optional regex pattern to filter by server name
527
+ - json: Optional flag for JSON output
528
+
529
+ Returns:
530
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
531
+
532
+ Reference: R11 §2.1 (11-enhancing_show_command_v0.md)
533
+ """
534
+ try:
535
+ import json as json_module
536
+ import re
537
+ import os
538
+ import datetime
539
+ # Import strategies to trigger registration
540
+ import hatch.mcp_host_config.strategies
541
+ from hatch.mcp_host_config.backup import MCPHostConfigBackupManager
542
+ from hatch.cli.cli_utils import highlight
543
+
544
+ env_manager: HatchEnvironmentManager = args.env_manager
545
+ server_pattern: Optional[str] = getattr(args, 'server', None)
546
+ json_output: bool = getattr(args, 'json', False)
547
+
548
+ # Compile regex pattern if provided
549
+ pattern_re = None
550
+ if server_pattern:
551
+ try:
552
+ pattern_re = re.compile(server_pattern)
553
+ except re.error as e:
554
+ format_validation_error(ValidationError(
555
+ f"Invalid regex pattern '{server_pattern}': {e}",
556
+ field="--server",
557
+ suggestion="Use a valid Python regex pattern"
558
+ ))
559
+ return EXIT_ERROR
560
+
561
+ # Build Hatch management lookup: {server_name: {host: (env_name, version, last_synced)}}
562
+ hatch_managed = {}
563
+ for env_info in env_manager.list_environments():
564
+ env_name = env_info.get("name", env_info) if isinstance(env_info, dict) else env_info
565
+ try:
566
+ env_data = env_manager.get_environment_data(env_name)
567
+ packages = env_data.get("packages", []) if isinstance(env_data, dict) else getattr(env_data, 'packages', [])
568
+
569
+ for pkg in packages:
570
+ pkg_name = pkg.get("name") if isinstance(pkg, dict) else getattr(pkg, 'name', None)
571
+ pkg_version = pkg.get("version", "unknown") if isinstance(pkg, dict) else getattr(pkg, 'version', 'unknown')
572
+ configured_hosts = pkg.get("configured_hosts", {}) if isinstance(pkg, dict) else getattr(pkg, 'configured_hosts', {})
573
+
574
+ if pkg_name:
575
+ if pkg_name not in hatch_managed:
576
+ hatch_managed[pkg_name] = {}
577
+ for host_name, host_info in configured_hosts.items():
578
+ last_synced = host_info.get("configured_at", "N/A") if isinstance(host_info, dict) else "N/A"
579
+ hatch_managed[pkg_name][host_name] = (env_name, pkg_version, last_synced)
580
+ except Exception:
581
+ continue
582
+
583
+ # Get all available hosts
584
+ available_hosts = MCPHostRegistry.detect_available_hosts()
585
+
586
+ # Sort hosts alphabetically
587
+ sorted_hosts = sorted(available_hosts, key=lambda h: h.value)
588
+
589
+ # Collect host data for output
590
+ hosts_data = []
591
+
592
+ for host_type in sorted_hosts:
593
+ try:
594
+ strategy = MCPHostRegistry.get_strategy(host_type)
595
+ host_config = strategy.read_configuration()
596
+ host_name = host_type.value
597
+ config_path = strategy.get_config_path()
598
+
599
+ # Filter servers by pattern if specified
600
+ filtered_servers = {}
601
+ for server_name, server_config in host_config.servers.items():
602
+ if pattern_re and not pattern_re.search(server_name):
603
+ continue
604
+ filtered_servers[server_name] = server_config
605
+
606
+ # Skip host if no matching servers
607
+ if not filtered_servers:
608
+ continue
609
+
610
+ # Get host metadata
611
+ last_modified = None
612
+ if config_path and config_path.exists():
613
+ mtime = os.path.getmtime(config_path)
614
+ last_modified = datetime.datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
615
+
616
+ backup_manager = MCPHostConfigBackupManager()
617
+ backups = backup_manager.list_backups(host_name)
618
+ backup_count = len(backups) if backups else 0
619
+
620
+ # Build server data
621
+ servers_data = []
622
+ for server_name in sorted(filtered_servers.keys()):
623
+ server_config = filtered_servers[server_name]
624
+
625
+ # Check if Hatch-managed
626
+ hatch_info = hatch_managed.get(server_name, {}).get(host_name)
627
+ is_hatch_managed = hatch_info is not None
628
+ env_name = hatch_info[0] if hatch_info else None
629
+ pkg_version = hatch_info[1] if hatch_info else None
630
+ last_synced = hatch_info[2] if hatch_info else None
631
+
632
+ server_data = {
633
+ "name": server_name,
634
+ "hatch_managed": is_hatch_managed,
635
+ "environment": env_name,
636
+ "version": pkg_version,
637
+ "command": getattr(server_config, 'command', None),
638
+ "args": getattr(server_config, 'args', None),
639
+ "url": getattr(server_config, 'url', None),
640
+ "env": {},
641
+ "last_synced": last_synced,
642
+ }
643
+
644
+ # Get environment variables (hide sensitive values for display)
645
+ env_vars = getattr(server_config, 'env', None)
646
+ if env_vars:
647
+ for key, value in env_vars.items():
648
+ if any(sensitive in key.upper() for sensitive in ['KEY', 'SECRET', 'TOKEN', 'PASSWORD', 'CREDENTIAL']):
649
+ server_data["env"][key] = "****** (hidden)"
650
+ else:
651
+ server_data["env"][key] = value
652
+
653
+ servers_data.append(server_data)
654
+
655
+ hosts_data.append({
656
+ "host": host_name,
657
+ "config_path": str(config_path) if config_path else None,
658
+ "last_modified": last_modified,
659
+ "backup_count": backup_count,
660
+ "servers": servers_data,
661
+ })
662
+ except Exception:
663
+ continue
664
+
665
+ # JSON output
666
+ if json_output:
667
+ print(json_module.dumps({"hosts": hosts_data}, indent=2))
668
+ return EXIT_SUCCESS
669
+
670
+ # Human-readable output
671
+ if not hosts_data:
672
+ if server_pattern:
673
+ print(f"No hosts with servers matching '{server_pattern}'")
674
+ else:
675
+ print("No MCP hosts found")
676
+ return EXIT_SUCCESS
677
+
678
+ separator = "═" * 79
679
+
680
+ for host_data in hosts_data:
681
+ # Horizontal separator
682
+ print(separator)
683
+
684
+ # Host header with highlight
685
+ print(f"MCP Host: {highlight(host_data['host'])}")
686
+ print(f" Config Path: {host_data['config_path'] or 'N/A'}")
687
+ print(f" Last Modified: {host_data['last_modified'] or 'N/A'}")
688
+ if host_data['backup_count'] > 0:
689
+ print(f" Backup Available: Yes ({host_data['backup_count']} backups)")
690
+ else:
691
+ print(f" Backup Available: No")
692
+ print()
693
+
694
+ # Configured Servers section
695
+ print(f" Configured Servers ({len(host_data['servers'])}):")
696
+
697
+ for server in host_data['servers']:
698
+ # Server header with highlight
699
+ if server['hatch_managed']:
700
+ print(f" {highlight(server['name'])} (Hatch-managed: {server['environment']})")
701
+ else:
702
+ print(f" {highlight(server['name'])} (Not Hatch-managed)")
703
+
704
+ # Command and args
705
+ if server['command']:
706
+ print(f" Command: {server['command']}")
707
+ if server['args']:
708
+ print(f" Args: {server['args']}")
709
+
710
+ # URL for remote servers
711
+ if server['url']:
712
+ print(f" URL: {server['url']}")
713
+
714
+ # Environment variables
715
+ if server['env']:
716
+ print(f" Environment Variables:")
717
+ for key, value in server['env'].items():
718
+ print(f" {key}: {value}")
719
+
720
+ # Hatch-specific info
721
+ if server['hatch_managed']:
722
+ if server['last_synced']:
723
+ print(f" Last Synced: {server['last_synced']}")
724
+ if server['version']:
725
+ print(f" Package Version: {server['version']}")
726
+
727
+ print()
728
+
729
+ return EXIT_SUCCESS
730
+ except Exception as e:
731
+ reporter = ResultReporter("hatch mcp show hosts")
732
+ reporter.report_error("Failed to show host configurations", details=[f"Reason: {str(e)}"])
733
+ return EXIT_ERROR
734
+
735
+
736
+ def handle_mcp_show_servers(args: Namespace) -> int:
737
+ """Handle 'hatch mcp show servers' command.
738
+
739
+ Shows detailed hierarchical view of all MCP server configurations across hosts.
740
+ Supports --host filter for regex pattern matching.
741
+
742
+ Args:
743
+ args: Parsed command-line arguments containing:
744
+ - env_manager: HatchEnvironmentManager instance
745
+ - host: Optional regex pattern to filter by host name
746
+ - json: Optional flag for JSON output
747
+
748
+ Returns:
749
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
750
+
751
+ Reference: R11 §2.2 (11-enhancing_show_command_v0.md)
752
+ """
753
+ try:
754
+ import json as json_module
755
+ import re
756
+ # Import strategies to trigger registration
757
+ import hatch.mcp_host_config.strategies
758
+ from hatch.cli.cli_utils import highlight
759
+
760
+ env_manager: HatchEnvironmentManager = args.env_manager
761
+ host_pattern: Optional[str] = getattr(args, 'host', None)
762
+ json_output: bool = getattr(args, 'json', False)
763
+
764
+ # Compile regex pattern if provided
765
+ pattern_re = None
766
+ if host_pattern:
767
+ try:
768
+ pattern_re = re.compile(host_pattern)
769
+ except re.error as e:
770
+ format_validation_error(ValidationError(
771
+ f"Invalid regex pattern '{host_pattern}': {e}",
772
+ field="--host",
773
+ suggestion="Use a valid Python regex pattern"
774
+ ))
775
+ return EXIT_ERROR
776
+
777
+ # Build Hatch management lookup: {server_name: {host: (env_name, version, last_synced)}}
778
+ hatch_managed = {}
779
+ for env_info in env_manager.list_environments():
780
+ env_name = env_info.get("name", env_info) if isinstance(env_info, dict) else env_info
781
+ try:
782
+ env_data = env_manager.get_environment_data(env_name)
783
+ packages = env_data.get("packages", []) if isinstance(env_data, dict) else getattr(env_data, 'packages', [])
784
+
785
+ for pkg in packages:
786
+ pkg_name = pkg.get("name") if isinstance(pkg, dict) else getattr(pkg, 'name', None)
787
+ pkg_version = pkg.get("version", "unknown") if isinstance(pkg, dict) else getattr(pkg, 'version', 'unknown')
788
+ configured_hosts = pkg.get("configured_hosts", {}) if isinstance(pkg, dict) else getattr(pkg, 'configured_hosts', {})
789
+
790
+ if pkg_name:
791
+ if pkg_name not in hatch_managed:
792
+ hatch_managed[pkg_name] = {}
793
+ for host_name, host_info in configured_hosts.items():
794
+ last_synced = host_info.get("configured_at", "N/A") if isinstance(host_info, dict) else "N/A"
795
+ hatch_managed[pkg_name][host_name] = (env_name, pkg_version, last_synced)
796
+ except Exception:
797
+ continue
798
+
799
+ # Get all available hosts
800
+ available_hosts = MCPHostRegistry.detect_available_hosts()
801
+
802
+ # Build server → hosts mapping
803
+ # Format: {server_name: [(host_name, server_config, hatch_info), ...]}
804
+ server_hosts_map = {}
805
+
806
+ for host_type in available_hosts:
807
+ host_name = host_type.value
808
+
809
+ # Apply host pattern filter if specified
810
+ if pattern_re and not pattern_re.search(host_name):
811
+ continue
812
+
813
+ try:
814
+ strategy = MCPHostRegistry.get_strategy(host_type)
815
+ host_config = strategy.read_configuration()
816
+
817
+ for server_name, server_config in host_config.servers.items():
818
+ if server_name not in server_hosts_map:
819
+ server_hosts_map[server_name] = []
820
+
821
+ # Get Hatch management info for this server on this host
822
+ hatch_info = hatch_managed.get(server_name, {}).get(host_name)
823
+
824
+ server_hosts_map[server_name].append((host_name, server_config, hatch_info))
825
+ except Exception:
826
+ continue
827
+
828
+ # Sort servers alphabetically
829
+ sorted_servers = sorted(server_hosts_map.keys())
830
+
831
+ # Collect server data for output
832
+ servers_data = []
833
+
834
+ for server_name in sorted_servers:
835
+ host_entries = server_hosts_map[server_name]
836
+
837
+ # Skip server if no matching hosts (after filter)
838
+ if not host_entries:
839
+ continue
840
+
841
+ # Determine overall Hatch management status
842
+ # A server is Hatch-managed if it's managed on ANY host
843
+ any_hatch_managed = any(h[2] is not None for h in host_entries)
844
+
845
+ # Get version from first Hatch-managed entry (if any)
846
+ pkg_version = None
847
+ pkg_env = None
848
+ for _, _, hatch_info in host_entries:
849
+ if hatch_info:
850
+ pkg_env = hatch_info[0]
851
+ pkg_version = hatch_info[1]
852
+ break
853
+
854
+ # Build host configurations data
855
+ hosts_data = []
856
+ for host_name, server_config, hatch_info in sorted(host_entries, key=lambda x: x[0]):
857
+ host_data = {
858
+ "host": host_name,
859
+ "command": getattr(server_config, 'command', None),
860
+ "args": getattr(server_config, 'args', None),
861
+ "url": getattr(server_config, 'url', None),
862
+ "env": {},
863
+ "last_synced": hatch_info[2] if hatch_info else None,
864
+ }
865
+
866
+ # Get environment variables (hide sensitive values)
867
+ env_vars = getattr(server_config, 'env', None)
868
+ if env_vars:
869
+ for key, value in env_vars.items():
870
+ if any(sensitive in key.upper() for sensitive in ['KEY', 'SECRET', 'TOKEN', 'PASSWORD', 'CREDENTIAL']):
871
+ host_data["env"][key] = "****** (hidden)"
872
+ else:
873
+ host_data["env"][key] = value
874
+
875
+ hosts_data.append(host_data)
876
+
877
+ servers_data.append({
878
+ "name": server_name,
879
+ "hatch_managed": any_hatch_managed,
880
+ "environment": pkg_env,
881
+ "version": pkg_version,
882
+ "hosts": hosts_data,
883
+ })
884
+
885
+ # JSON output
886
+ if json_output:
887
+ print(json_module.dumps({"servers": servers_data}, indent=2))
888
+ return EXIT_SUCCESS
889
+
890
+ # Human-readable output
891
+ if not servers_data:
892
+ if host_pattern:
893
+ print(f"No servers on hosts matching '{host_pattern}'")
894
+ else:
895
+ print("No MCP servers found")
896
+ return EXIT_SUCCESS
897
+
898
+ separator = "═" * 79
899
+
900
+ for server_data in servers_data:
901
+ # Horizontal separator
902
+ print(separator)
903
+
904
+ # Server header with highlight
905
+ print(f"MCP Server: {highlight(server_data['name'])}")
906
+ if server_data['hatch_managed']:
907
+ print(f" Hatch Managed: Yes ({server_data['environment']})")
908
+ if server_data['version']:
909
+ print(f" Package Version: {server_data['version']}")
910
+ else:
911
+ print(f" Hatch Managed: No")
912
+ print()
913
+
914
+ # Host Configurations section
915
+ print(f" Host Configurations ({len(server_data['hosts'])}):")
916
+
917
+ for host in server_data['hosts']:
918
+ # Host header with highlight
919
+ print(f" {highlight(host['host'])}:")
920
+
921
+ # Command and args
922
+ if host['command']:
923
+ print(f" Command: {host['command']}")
924
+ if host['args']:
925
+ print(f" Args: {host['args']}")
926
+
927
+ # URL for remote servers
928
+ if host['url']:
929
+ print(f" URL: {host['url']}")
930
+
931
+ # Environment variables
932
+ if host['env']:
933
+ print(f" Environment Variables:")
934
+ for key, value in host['env'].items():
935
+ print(f" {key}: {value}")
936
+
937
+ # Last synced (if Hatch-managed)
938
+ if host['last_synced']:
939
+ print(f" Last Synced: {host['last_synced']}")
940
+
941
+ print()
942
+
943
+ return EXIT_SUCCESS
944
+ except Exception as e:
945
+ reporter = ResultReporter("hatch mcp show servers")
946
+ reporter.report_error("Failed to show server configurations", details=[f"Reason: {str(e)}"])
947
+ return EXIT_ERROR
948
+
949
+
950
+ def handle_mcp_backup_restore(args: Namespace) -> int:
951
+ """Handle 'hatch mcp backup restore' command.
952
+
953
+ Args:
954
+ args: Parsed command-line arguments containing:
955
+ - env_manager: HatchEnvironmentManager instance
956
+ - host: Host platform to restore
957
+ - backup_file: Optional specific backup file (default: latest)
958
+ - dry_run: Preview without execution
959
+ - auto_approve: Skip confirmation prompts
960
+
961
+ Returns:
962
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
963
+ """
964
+ from hatch.cli.cli_utils import (
965
+ request_confirmation,
966
+ ResultReporter,
967
+ ConsequenceType,
968
+ )
969
+
970
+ try:
971
+ from hatch.mcp_host_config.backup import MCPHostConfigBackupManager
972
+
973
+ env_manager: HatchEnvironmentManager = args.env_manager
974
+ host: str = args.host
975
+ backup_file: Optional[str] = getattr(args, 'backup_file', None)
976
+ dry_run: bool = getattr(args, 'dry_run', False)
977
+ auto_approve: bool = getattr(args, 'auto_approve', False)
978
+
979
+ # Validate host type
980
+ try:
981
+ host_type = MCPHostType(host)
982
+ except ValueError:
983
+ format_validation_error(ValidationError(
984
+ f"Invalid host '{host}'",
985
+ field="--host",
986
+ suggestion=f"Supported hosts: {', '.join(h.value for h in MCPHostType)}"
987
+ ))
988
+ return EXIT_ERROR
989
+
990
+ backup_manager = MCPHostConfigBackupManager()
991
+
992
+ # Get backup file path
993
+ if backup_file:
994
+ backup_path = backup_manager.backup_root / host / backup_file
995
+ if not backup_path.exists():
996
+ format_validation_error(ValidationError(
997
+ f"Backup file '{backup_file}' not found for host '{host}'",
998
+ field="backup_file",
999
+ suggestion=f"Use 'hatch mcp backup list {host}' to see available backups"
1000
+ ))
1001
+ return EXIT_ERROR
1002
+ else:
1003
+ backup_path = backup_manager._get_latest_backup(host)
1004
+ if not backup_path:
1005
+ format_validation_error(ValidationError(
1006
+ f"No backups found for host '{host}'",
1007
+ field="--host",
1008
+ suggestion="Create a backup first with 'hatch mcp configure' which auto-creates backups"
1009
+ ))
1010
+ return EXIT_ERROR
1011
+ backup_file = backup_path.name
1012
+
1013
+ # Create ResultReporter for unified output
1014
+ reporter = ResultReporter("hatch mcp backup restore", dry_run=dry_run)
1015
+ reporter.add(ConsequenceType.RESTORE, f"Backup '{backup_file}' to host '{host}'")
1016
+
1017
+ if dry_run:
1018
+ reporter.report_result()
1019
+ return EXIT_SUCCESS
1020
+
1021
+ # Show prompt for confirmation
1022
+ prompt = reporter.report_prompt()
1023
+ if prompt:
1024
+ print(prompt)
1025
+
1026
+ # Confirm operation unless auto-approved
1027
+ if not request_confirmation("Proceed?", auto_approve):
1028
+ format_info("Operation cancelled")
1029
+ return EXIT_SUCCESS
1030
+
1031
+ # Perform restoration
1032
+ success = backup_manager.restore_backup(host, backup_file)
1033
+
1034
+ if success:
1035
+ reporter.report_result()
1036
+
1037
+ # Read restored configuration to get actual server list
1038
+ try:
1039
+ # Import strategies to trigger registration
1040
+ import hatch.mcp_host_config.strategies
1041
+
1042
+ host_type = MCPHostType(host)
1043
+ strategy = MCPHostRegistry.get_strategy(host_type)
1044
+ restored_config = strategy.read_configuration()
1045
+
1046
+ # Update environment tracking to match restored state
1047
+ updates_count = (
1048
+ env_manager.apply_restored_host_configuration_to_environments(
1049
+ host, restored_config.servers
1050
+ )
1051
+ )
1052
+ if updates_count > 0:
1053
+ print(
1054
+ f" Synchronized {updates_count} package entries with restored configuration"
1055
+ )
1056
+
1057
+ except Exception as e:
1058
+ from hatch.cli.cli_utils import Color, _colors_enabled
1059
+ if _colors_enabled():
1060
+ print(f" {Color.YELLOW.value}[WARNING]{Color.RESET.value} Could not synchronize environment tracking: {e}")
1061
+ else:
1062
+ print(f" [WARNING] Could not synchronize environment tracking: {e}")
1063
+
1064
+ return EXIT_SUCCESS
1065
+ else:
1066
+ print(f"[ERROR] Failed to restore backup '{backup_file}' for host '{host}'")
1067
+ return EXIT_ERROR
1068
+
1069
+ except Exception as e:
1070
+ reporter = ResultReporter("hatch mcp backup restore")
1071
+ reporter.report_error("Failed to restore backup", details=[f"Reason: {str(e)}"])
1072
+ return EXIT_ERROR
1073
+
1074
+
1075
+ def handle_mcp_backup_list(args: Namespace) -> int:
1076
+ """Handle 'hatch mcp backup list' command.
1077
+
1078
+ Args:
1079
+ args: Parsed command-line arguments containing:
1080
+ - host: Host platform to list backups for
1081
+ - detailed: Show detailed backup information
1082
+ - json: Optional flag for JSON output
1083
+
1084
+ Returns:
1085
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
1086
+ """
1087
+ try:
1088
+ import json as json_module
1089
+ from hatch.mcp_host_config.backup import MCPHostConfigBackupManager
1090
+
1091
+ host: str = args.host
1092
+ detailed: bool = getattr(args, 'detailed', False)
1093
+ json_output: bool = getattr(args, 'json', False)
1094
+
1095
+ # Validate host type
1096
+ try:
1097
+ host_type = MCPHostType(host)
1098
+ except ValueError:
1099
+ format_validation_error(ValidationError(
1100
+ f"Invalid host '{host}'",
1101
+ field="--host",
1102
+ suggestion=f"Supported hosts: {', '.join(h.value for h in MCPHostType)}"
1103
+ ))
1104
+ return EXIT_ERROR
1105
+
1106
+ backup_manager = MCPHostConfigBackupManager()
1107
+ backups = backup_manager.list_backups(host)
1108
+
1109
+ # JSON output
1110
+ if json_output:
1111
+ backups_data = []
1112
+ for backup in backups:
1113
+ backups_data.append({
1114
+ "file": backup.file_path.name,
1115
+ "created": backup.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
1116
+ "size_bytes": backup.file_size,
1117
+ "age_days": backup.age_days
1118
+ })
1119
+ print(json_module.dumps({
1120
+ "host": host,
1121
+ "backups": backups_data
1122
+ }, indent=2))
1123
+ return EXIT_SUCCESS
1124
+
1125
+ if not backups:
1126
+ print(f"No backups found for host '{host}'")
1127
+ return EXIT_SUCCESS
1128
+
1129
+ print(f"Backups for host '{host}' ({len(backups)} found):")
1130
+
1131
+ if detailed:
1132
+ # Define table columns per R02 §2.7
1133
+ columns = [
1134
+ ColumnDef(name="Backup File", width=40),
1135
+ ColumnDef(name="Created", width=20),
1136
+ ColumnDef(name="Size", width=12, align="right"),
1137
+ ColumnDef(name="Age (days)", width=10, align="right"),
1138
+ ]
1139
+ formatter = TableFormatter(columns)
1140
+
1141
+ for backup in backups:
1142
+ created = backup.timestamp.strftime("%Y-%m-%d %H:%M:%S")
1143
+ size = f"{backup.file_size:,} B"
1144
+ age = str(backup.age_days)
1145
+ formatter.add_row([backup.file_path.name, created, size, age])
1146
+
1147
+ print(formatter.render())
1148
+ else:
1149
+ for backup in backups:
1150
+ created = backup.timestamp.strftime("%Y-%m-%d %H:%M:%S")
1151
+ print(
1152
+ f" {backup.file_path.name} (created: {created}, {backup.age_days} days ago)"
1153
+ )
1154
+
1155
+ return EXIT_SUCCESS
1156
+ except Exception as e:
1157
+ reporter = ResultReporter("hatch mcp backup list")
1158
+ reporter.report_error("Failed to list backups", details=[f"Reason: {str(e)}"])
1159
+ return EXIT_ERROR
1160
+
1161
+
1162
+ def handle_mcp_backup_clean(args: Namespace) -> int:
1163
+ """Handle 'hatch mcp backup clean' command.
1164
+
1165
+ Args:
1166
+ args: Parsed command-line arguments containing:
1167
+ - host: Host platform to clean backups for
1168
+ - older_than_days: Remove backups older than specified days
1169
+ - keep_count: Keep only the specified number of newest backups
1170
+ - dry_run: Preview without execution
1171
+ - auto_approve: Skip confirmation prompts
1172
+
1173
+ Returns:
1174
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
1175
+ """
1176
+ from hatch.cli.cli_utils import (
1177
+ request_confirmation,
1178
+ ResultReporter,
1179
+ ConsequenceType,
1180
+ )
1181
+
1182
+ try:
1183
+ from hatch.mcp_host_config.backup import MCPHostConfigBackupManager
1184
+
1185
+ host: str = args.host
1186
+ older_than_days: Optional[int] = getattr(args, 'older_than_days', None)
1187
+ keep_count: Optional[int] = getattr(args, 'keep_count', None)
1188
+ dry_run: bool = getattr(args, 'dry_run', False)
1189
+ auto_approve: bool = getattr(args, 'auto_approve', False)
1190
+
1191
+ # Validate host type
1192
+ try:
1193
+ host_type = MCPHostType(host)
1194
+ except ValueError:
1195
+ format_validation_error(ValidationError(
1196
+ f"Invalid host '{host}'",
1197
+ field="--host",
1198
+ suggestion=f"Supported hosts: {', '.join(h.value for h in MCPHostType)}"
1199
+ ))
1200
+ return EXIT_ERROR
1201
+
1202
+ # Validate cleanup criteria
1203
+ if not older_than_days and not keep_count:
1204
+ format_validation_error(ValidationError(
1205
+ "Must specify either --older-than-days or --keep-count",
1206
+ suggestion="Use --older-than-days N to remove backups older than N days, or --keep-count N to keep only the N most recent"
1207
+ ))
1208
+ return EXIT_ERROR
1209
+
1210
+ backup_manager = MCPHostConfigBackupManager()
1211
+ backups = backup_manager.list_backups(host)
1212
+
1213
+ if not backups:
1214
+ print(f"No backups found for host '{host}'")
1215
+ return EXIT_SUCCESS
1216
+
1217
+ # Determine which backups would be cleaned
1218
+ to_clean = []
1219
+
1220
+ if older_than_days:
1221
+ for backup in backups:
1222
+ if backup.age_days > older_than_days:
1223
+ to_clean.append(backup)
1224
+
1225
+ if keep_count and len(backups) > keep_count:
1226
+ # Keep newest backups, remove oldest
1227
+ to_clean.extend(backups[keep_count:])
1228
+
1229
+ # Remove duplicates while preserving order
1230
+ seen = set()
1231
+ unique_to_clean = []
1232
+ for backup in to_clean:
1233
+ if backup.file_path not in seen:
1234
+ seen.add(backup.file_path)
1235
+ unique_to_clean.append(backup)
1236
+
1237
+ if not unique_to_clean:
1238
+ print(f"No backups match cleanup criteria for host '{host}'")
1239
+ return EXIT_SUCCESS
1240
+
1241
+ # Create ResultReporter for unified output
1242
+ reporter = ResultReporter("hatch mcp backup clean", dry_run=dry_run)
1243
+ for backup in unique_to_clean:
1244
+ reporter.add(ConsequenceType.CLEAN, f"{backup.file_path.name} (age: {backup.age_days} days)")
1245
+
1246
+ if dry_run:
1247
+ reporter.report_result()
1248
+ return EXIT_SUCCESS
1249
+
1250
+ # Show prompt for confirmation
1251
+ prompt = reporter.report_prompt()
1252
+ if prompt:
1253
+ print(prompt)
1254
+
1255
+ # Confirm operation unless auto-approved
1256
+ if not request_confirmation("Proceed?", auto_approve):
1257
+ format_info("Operation cancelled")
1258
+ return EXIT_SUCCESS
1259
+
1260
+ # Perform cleanup
1261
+ filters = {}
1262
+ if older_than_days:
1263
+ filters["older_than_days"] = older_than_days
1264
+ if keep_count:
1265
+ filters["keep_count"] = keep_count
1266
+
1267
+ cleaned_count = backup_manager.clean_backups(host, **filters)
1268
+
1269
+ if cleaned_count > 0:
1270
+ reporter.report_result()
1271
+ return EXIT_SUCCESS
1272
+ else:
1273
+ print(f"No backups were cleaned for host '{host}'")
1274
+ return EXIT_SUCCESS
1275
+
1276
+ except Exception as e:
1277
+ reporter = ResultReporter("hatch mcp backup clean")
1278
+ reporter.report_error("Failed to clean backups", details=[f"Reason: {str(e)}"])
1279
+ return EXIT_ERROR
1280
+
1281
+
1282
+ def handle_mcp_configure(args: Namespace) -> int:
1283
+ """Handle 'hatch mcp configure' command with ALL host-specific arguments.
1284
+
1285
+ Host-specific arguments are accepted for all hosts. The reporting system will
1286
+ show unsupported fields as "UNSUPPORTED" in the conversion report rather than
1287
+ rejecting them upfront.
1288
+
1289
+ The CLI creates a unified MCPServerConfig directly. Adapters handle host-specific
1290
+ validation and serialization when writing to host configuration files.
1291
+
1292
+ Args:
1293
+ args: Parsed command-line arguments containing all configuration options
1294
+
1295
+ Returns:
1296
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
1297
+ """
1298
+ import shlex
1299
+ from hatch.cli.cli_utils import (
1300
+ request_confirmation,
1301
+ parse_env_vars,
1302
+ parse_header,
1303
+ parse_input,
1304
+ ResultReporter,
1305
+ ConsequenceType,
1306
+ )
1307
+ from hatch.mcp_host_config.reporting import generate_conversion_report
1308
+
1309
+ try:
1310
+ # Extract arguments from Namespace
1311
+ host: str = args.host
1312
+ server_name: str = args.server_name
1313
+ command: Optional[str] = getattr(args, 'server_command', None)
1314
+ cmd_args: Optional[list] = getattr(args, 'args', None)
1315
+ env: Optional[list] = getattr(args, 'env_var', None)
1316
+ url: Optional[str] = getattr(args, 'url', None)
1317
+ header: Optional[list] = getattr(args, 'header', None)
1318
+ timeout: Optional[int] = getattr(args, 'timeout', None)
1319
+ trust: bool = getattr(args, 'trust', False)
1320
+ cwd: Optional[str] = getattr(args, 'cwd', None)
1321
+ env_file: Optional[str] = getattr(args, 'env_file', None)
1322
+ http_url: Optional[str] = getattr(args, 'http_url', None)
1323
+ include_tools: Optional[list] = getattr(args, 'include_tools', None)
1324
+ exclude_tools: Optional[list] = getattr(args, 'exclude_tools', None)
1325
+ input_vars: Optional[list] = getattr(args, 'input', None)
1326
+ disabled: Optional[bool] = getattr(args, 'disabled', None)
1327
+ auto_approve_tools: Optional[list] = getattr(args, 'auto_approve_tools', None)
1328
+ disable_tools: Optional[list] = getattr(args, 'disable_tools', None)
1329
+ env_vars: Optional[list] = getattr(args, 'env_vars', None)
1330
+ startup_timeout: Optional[int] = getattr(args, 'startup_timeout', None)
1331
+ tool_timeout: Optional[int] = getattr(args, 'tool_timeout', None)
1332
+ enabled: Optional[bool] = getattr(args, 'enabled', None)
1333
+ bearer_token_env_var: Optional[str] = getattr(args, 'bearer_token_env_var', None)
1334
+ env_header: Optional[list] = getattr(args, 'env_header', None)
1335
+ no_backup: bool = getattr(args, 'no_backup', False)
1336
+ dry_run: bool = getattr(args, 'dry_run', False)
1337
+ auto_approve: bool = getattr(args, 'auto_approve', False)
1338
+
1339
+ # Validate host type
1340
+ try:
1341
+ host_type = MCPHostType(host)
1342
+ except ValueError:
1343
+ format_validation_error(ValidationError(
1344
+ f"Invalid host '{host}'",
1345
+ field="--host",
1346
+ suggestion=f"Supported hosts: {', '.join(h.value for h in MCPHostType)}"
1347
+ ))
1348
+ return EXIT_ERROR
1349
+
1350
+ # Validate Claude Desktop/Code transport restrictions (Issue 2)
1351
+ if host_type in (MCPHostType.CLAUDE_DESKTOP, MCPHostType.CLAUDE_CODE):
1352
+ if url is not None:
1353
+ format_validation_error(ValidationError(
1354
+ f"{host} does not support remote servers (--url)",
1355
+ field="--url",
1356
+ suggestion="Only local servers with --command are supported for this host"
1357
+ ))
1358
+ return EXIT_ERROR
1359
+
1360
+ # Validate argument dependencies
1361
+ if command and header:
1362
+ format_validation_error(ValidationError(
1363
+ "--header can only be used with --url or --http-url (remote servers)",
1364
+ field="--header",
1365
+ suggestion="Remove --header when using --command (local servers)"
1366
+ ))
1367
+ return EXIT_ERROR
1368
+
1369
+ if (url or http_url) and cmd_args:
1370
+ format_validation_error(ValidationError(
1371
+ "--args can only be used with --command (local servers)",
1372
+ field="--args",
1373
+ suggestion="Remove --args when using --url or --http-url (remote servers)"
1374
+ ))
1375
+ return EXIT_ERROR
1376
+
1377
+ # Check if server exists (for partial update support)
1378
+ manager = MCPHostConfigurationManager()
1379
+ existing_config = manager.get_server_config(host, server_name)
1380
+ is_update = existing_config is not None
1381
+
1382
+ # Conditional validation: Create requires command OR url OR http_url, update does not
1383
+ if not is_update:
1384
+ if not command and not url and not http_url:
1385
+ format_validation_error(ValidationError(
1386
+ "When creating a new server, you must provide a transport type",
1387
+ suggestion="Use --command (local servers), --url (SSE remote servers), or --http-url (HTTP remote servers)"
1388
+ ))
1389
+ return EXIT_ERROR
1390
+
1391
+ # Parse environment variables, headers, and inputs
1392
+ env_dict = parse_env_vars(env)
1393
+ headers_dict = parse_header(header)
1394
+ inputs_list = parse_input(input_vars)
1395
+
1396
+ # Build unified configuration data
1397
+ config_data = {"name": server_name}
1398
+
1399
+ if command is not None:
1400
+ config_data["command"] = command
1401
+ if cmd_args is not None:
1402
+ # Process args with shlex.split() to handle quoted strings
1403
+ processed_args = []
1404
+ for arg in cmd_args:
1405
+ if arg:
1406
+ try:
1407
+ split_args = shlex.split(arg)
1408
+ processed_args.extend(split_args)
1409
+ except ValueError as e:
1410
+ from hatch.cli.cli_utils import Color, _colors_enabled
1411
+ if _colors_enabled():
1412
+ print(f"{Color.YELLOW.value}[WARNING]{Color.RESET.value} Invalid quote in argument '{arg}': {e}")
1413
+ else:
1414
+ print(f"[WARNING] Invalid quote in argument '{arg}': {e}")
1415
+ processed_args.append(arg)
1416
+ config_data["args"] = processed_args if processed_args else None
1417
+ if env_dict:
1418
+ config_data["env"] = env_dict
1419
+ if url is not None:
1420
+ config_data["url"] = url
1421
+ if headers_dict:
1422
+ config_data["headers"] = headers_dict
1423
+
1424
+ # Host-specific fields (Gemini)
1425
+ if timeout is not None:
1426
+ config_data["timeout"] = timeout
1427
+ if trust:
1428
+ config_data["trust"] = trust
1429
+ if cwd is not None:
1430
+ config_data["cwd"] = cwd
1431
+ if http_url is not None:
1432
+ config_data["httpUrl"] = http_url
1433
+ if include_tools is not None:
1434
+ config_data["includeTools"] = include_tools
1435
+ if exclude_tools is not None:
1436
+ config_data["excludeTools"] = exclude_tools
1437
+
1438
+ # Host-specific fields (Cursor/VS Code/LM Studio)
1439
+ if env_file is not None:
1440
+ config_data["envFile"] = env_file
1441
+
1442
+ # Host-specific fields (VS Code)
1443
+ if inputs_list is not None:
1444
+ config_data["inputs"] = inputs_list
1445
+
1446
+ # Host-specific fields (Kiro)
1447
+ if disabled is not None:
1448
+ config_data["disabled"] = disabled
1449
+ if auto_approve_tools is not None:
1450
+ config_data["autoApprove"] = auto_approve_tools
1451
+ if disable_tools is not None:
1452
+ config_data["disabledTools"] = disable_tools
1453
+
1454
+ # Host-specific fields (Codex)
1455
+ if env_vars is not None:
1456
+ config_data["env_vars"] = env_vars
1457
+ if startup_timeout is not None:
1458
+ config_data["startup_timeout_sec"] = startup_timeout
1459
+ if tool_timeout is not None:
1460
+ config_data["tool_timeout_sec"] = tool_timeout
1461
+ if enabled is not None:
1462
+ config_data["enabled"] = enabled
1463
+ if bearer_token_env_var is not None:
1464
+ config_data["bearer_token_env_var"] = bearer_token_env_var
1465
+ if env_header is not None:
1466
+ env_http_headers = {}
1467
+ for header_spec in env_header:
1468
+ if '=' in header_spec:
1469
+ key, env_var_name = header_spec.split('=', 1)
1470
+ env_http_headers[key] = env_var_name
1471
+ if env_http_headers:
1472
+ config_data["env_http_headers"] = env_http_headers
1473
+
1474
+ # Partial update merge logic
1475
+ if is_update:
1476
+ existing_data = existing_config.model_dump(
1477
+ exclude_unset=True, exclude={"name"}
1478
+ )
1479
+
1480
+ if (url is not None or http_url is not None) and existing_config.command is not None:
1481
+ existing_data.pop("command", None)
1482
+ existing_data.pop("args", None)
1483
+ existing_data.pop("type", None)
1484
+
1485
+ if command is not None and (
1486
+ existing_config.url is not None
1487
+ or getattr(existing_config, "httpUrl", None) is not None
1488
+ ):
1489
+ existing_data.pop("url", None)
1490
+ existing_data.pop("httpUrl", None)
1491
+ existing_data.pop("headers", None)
1492
+ existing_data.pop("type", None)
1493
+
1494
+ merged_data = {**existing_data, **config_data}
1495
+ config_data = merged_data
1496
+
1497
+ # Create unified MCPServerConfig directly
1498
+ # Adapters handle host-specific validation and serialization
1499
+ server_config = MCPServerConfig(**config_data)
1500
+
1501
+ # Generate conversion report
1502
+ report = generate_conversion_report(
1503
+ operation="update" if is_update else "create",
1504
+ server_name=server_name,
1505
+ target_host=host_type,
1506
+ config=server_config,
1507
+ old_config=existing_config if is_update else None,
1508
+ dry_run=dry_run,
1509
+ )
1510
+
1511
+ # Create ResultReporter for unified output
1512
+ reporter = ResultReporter("hatch mcp configure", dry_run=dry_run)
1513
+ reporter.add_from_conversion_report(report)
1514
+
1515
+ # Display prompt and handle dry-run
1516
+ if dry_run:
1517
+ reporter.report_result()
1518
+ return EXIT_SUCCESS
1519
+
1520
+ # Show prompt for confirmation
1521
+ prompt = reporter.report_prompt()
1522
+ if prompt:
1523
+ print(prompt)
1524
+
1525
+ if not request_confirmation(
1526
+ f"Proceed?", auto_approve
1527
+ ):
1528
+ format_info("Operation cancelled")
1529
+ return EXIT_SUCCESS
1530
+
1531
+ # Perform configuration
1532
+ mcp_manager = MCPHostConfigurationManager()
1533
+ result = mcp_manager.configure_server(
1534
+ server_config=server_config, hostname=host, no_backup=no_backup
1535
+ )
1536
+
1537
+ if result.success:
1538
+ if result.backup_path:
1539
+ reporter.add(ConsequenceType.CREATE, f"Backup: {result.backup_path}")
1540
+ reporter.report_result()
1541
+ return EXIT_SUCCESS
1542
+ else:
1543
+ print(
1544
+ f"[ERROR] Failed to configure MCP server '{server_name}' on host '{host}': {result.error_message}"
1545
+ )
1546
+ return EXIT_ERROR
1547
+
1548
+ except Exception as e:
1549
+ reporter = ResultReporter("hatch mcp configure")
1550
+ reporter.report_error("Failed to configure MCP server", details=[f"Reason: {str(e)}"])
1551
+ return EXIT_ERROR
1552
+
1553
+
1554
+ def handle_mcp_remove(args: Namespace) -> int:
1555
+ """Handle 'hatch mcp remove' command.
1556
+
1557
+ Removes an MCP server configuration from a specific host.
1558
+
1559
+ Args:
1560
+ args: Namespace with:
1561
+ - host: Target host identifier (e.g., 'claude-desktop', 'vscode')
1562
+ - server_name: Name of the server to remove
1563
+ - no_backup: If True, skip creating backup before removal
1564
+ - dry_run: If True, show what would be done without making changes
1565
+ - auto_approve: If True, skip confirmation prompt
1566
+
1567
+ Returns:
1568
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
1569
+ """
1570
+ from hatch.cli.cli_utils import (
1571
+ request_confirmation,
1572
+ ResultReporter,
1573
+ ConsequenceType,
1574
+ )
1575
+
1576
+ host = args.host
1577
+ server_name = args.server_name
1578
+ no_backup = getattr(args, "no_backup", False)
1579
+ dry_run = getattr(args, "dry_run", False)
1580
+ auto_approve = getattr(args, "auto_approve", False)
1581
+
1582
+ try:
1583
+ # Validate host type
1584
+ try:
1585
+ host_type = MCPHostType(host)
1586
+ except ValueError:
1587
+ format_validation_error(ValidationError(
1588
+ f"Invalid host '{host}'",
1589
+ field="--host",
1590
+ suggestion=f"Supported hosts: {', '.join(h.value for h in MCPHostType)}"
1591
+ ))
1592
+ return EXIT_ERROR
1593
+
1594
+ # Create ResultReporter for unified output
1595
+ reporter = ResultReporter("hatch mcp remove", dry_run=dry_run)
1596
+ reporter.add(ConsequenceType.REMOVE, f"Server '{server_name}' from '{host}'")
1597
+
1598
+ if dry_run:
1599
+ reporter.report_result()
1600
+ return EXIT_SUCCESS
1601
+
1602
+ # Show prompt for confirmation
1603
+ prompt = reporter.report_prompt()
1604
+ if prompt:
1605
+ print(prompt)
1606
+
1607
+ # Confirm operation unless auto-approved
1608
+ if not request_confirmation("Proceed?", auto_approve):
1609
+ format_info("Operation cancelled")
1610
+ return EXIT_SUCCESS
1611
+
1612
+ # Perform removal
1613
+ mcp_manager = MCPHostConfigurationManager()
1614
+ result = mcp_manager.remove_server(
1615
+ server_name=server_name, hostname=host, no_backup=no_backup
1616
+ )
1617
+
1618
+ if result.success:
1619
+ if result.backup_path:
1620
+ reporter.add(ConsequenceType.CREATE, f"Backup: {result.backup_path}")
1621
+ reporter.report_result()
1622
+ return EXIT_SUCCESS
1623
+ else:
1624
+ print(
1625
+ f"[ERROR] Failed to remove MCP server '{server_name}' from host '{host}': {result.error_message}"
1626
+ )
1627
+ return EXIT_ERROR
1628
+
1629
+ except Exception as e:
1630
+ reporter = ResultReporter("hatch mcp remove")
1631
+ reporter.report_error("Failed to remove MCP server", details=[f"Reason: {str(e)}"])
1632
+ return EXIT_ERROR
1633
+
1634
+
1635
+ def handle_mcp_remove_server(args: Namespace) -> int:
1636
+ """Handle 'hatch mcp remove server' command.
1637
+
1638
+ Removes an MCP server from multiple hosts.
1639
+
1640
+ Args:
1641
+ args: Namespace with:
1642
+ - env_manager: Environment manager instance for tracking
1643
+ - server_name: Name of the server to remove
1644
+ - host: Comma-separated list of target hosts
1645
+ - env: Environment name (for environment-based removal)
1646
+ - no_backup: If True, skip creating backups
1647
+ - dry_run: If True, show what would be done without making changes
1648
+ - auto_approve: If True, skip confirmation prompt
1649
+
1650
+ Returns:
1651
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
1652
+ """
1653
+ from hatch.cli.cli_utils import (
1654
+ request_confirmation,
1655
+ parse_host_list,
1656
+ ResultReporter,
1657
+ ConsequenceType,
1658
+ )
1659
+
1660
+ env_manager = args.env_manager
1661
+ server_name = args.server_name
1662
+ hosts = getattr(args, "host", None)
1663
+ env = getattr(args, "env", None)
1664
+ no_backup = getattr(args, "no_backup", False)
1665
+ dry_run = getattr(args, "dry_run", False)
1666
+ auto_approve = getattr(args, "auto_approve", False)
1667
+
1668
+ try:
1669
+ # Determine target hosts
1670
+ if hosts:
1671
+ target_hosts = parse_host_list(hosts)
1672
+ elif env:
1673
+ # TODO: Implement environment-based server removal
1674
+ format_validation_error(ValidationError(
1675
+ "Environment-based removal not yet implemented",
1676
+ field="--env",
1677
+ suggestion="Use --host to specify target hosts directly"
1678
+ ))
1679
+ return EXIT_ERROR
1680
+ else:
1681
+ format_validation_error(ValidationError(
1682
+ "Must specify either --host or --env",
1683
+ suggestion="Use --host HOST1,HOST2 or --env ENV_NAME"
1684
+ ))
1685
+ return EXIT_ERROR
1686
+
1687
+ if not target_hosts:
1688
+ format_validation_error(ValidationError(
1689
+ "No valid hosts specified",
1690
+ field="--host",
1691
+ suggestion=f"Supported hosts: {', '.join(h.value for h in MCPHostType)}"
1692
+ ))
1693
+ return EXIT_ERROR
1694
+
1695
+ # Create ResultReporter for unified output
1696
+ reporter = ResultReporter("hatch mcp remove-server", dry_run=dry_run)
1697
+ for host in target_hosts:
1698
+ reporter.add(ConsequenceType.REMOVE, f"Server '{server_name}' from '{host}'")
1699
+
1700
+ if dry_run:
1701
+ reporter.report_result()
1702
+ return EXIT_SUCCESS
1703
+
1704
+ # Show prompt for confirmation
1705
+ prompt = reporter.report_prompt()
1706
+ if prompt:
1707
+ print(prompt)
1708
+
1709
+ # Confirm operation unless auto-approved
1710
+ if not request_confirmation("Proceed?", auto_approve):
1711
+ format_info("Operation cancelled")
1712
+ return EXIT_SUCCESS
1713
+
1714
+ # Perform removal on each host
1715
+ mcp_manager = MCPHostConfigurationManager()
1716
+ success_count = 0
1717
+ total_count = len(target_hosts)
1718
+
1719
+ # Create result reporter for actual results
1720
+ result_reporter = ResultReporter("hatch mcp remove-server", dry_run=False)
1721
+
1722
+ for host in target_hosts:
1723
+ result = mcp_manager.remove_server(
1724
+ server_name=server_name, hostname=host, no_backup=no_backup
1725
+ )
1726
+
1727
+ if result.success:
1728
+ result_reporter.add(ConsequenceType.REMOVE, f"'{server_name}' from '{host}'")
1729
+ success_count += 1
1730
+
1731
+ # Update environment tracking for current environment only
1732
+ current_env = env_manager.get_current_environment()
1733
+ if current_env:
1734
+ env_manager.remove_package_host_configuration(
1735
+ current_env, server_name, host
1736
+ )
1737
+ else:
1738
+ result_reporter.add(ConsequenceType.SKIP, f"'{server_name}' from '{host}': {result.error_message}")
1739
+
1740
+ # Summary
1741
+ if success_count == total_count:
1742
+ result_reporter.report_result()
1743
+ return EXIT_SUCCESS
1744
+ elif success_count > 0:
1745
+ print(f"[WARNING] Partial success: {success_count}/{total_count} hosts")
1746
+ result_reporter.report_result()
1747
+ return EXIT_ERROR
1748
+ else:
1749
+ print(f"[ERROR] Failed to remove '{server_name}' from any hosts")
1750
+ return EXIT_ERROR
1751
+
1752
+ except Exception as e:
1753
+ reporter = ResultReporter("hatch mcp remove-server")
1754
+ reporter.report_error("Failed to remove MCP server", details=[f"Reason: {str(e)}"])
1755
+ return EXIT_ERROR
1756
+
1757
+
1758
+ def handle_mcp_remove_host(args: Namespace) -> int:
1759
+ """Handle 'hatch mcp remove host' command.
1760
+
1761
+ Removes entire host configuration (all MCP servers from a host).
1762
+
1763
+ Args:
1764
+ args: Namespace with:
1765
+ - env_manager: Environment manager instance for tracking
1766
+ - host_name: Name of the host to remove configuration from
1767
+ - no_backup: If True, skip creating backup
1768
+ - dry_run: If True, show what would be done without making changes
1769
+ - auto_approve: If True, skip confirmation prompt
1770
+
1771
+ Returns:
1772
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
1773
+ """
1774
+ from hatch.cli.cli_utils import (
1775
+ request_confirmation,
1776
+ ResultReporter,
1777
+ ConsequenceType,
1778
+ )
1779
+
1780
+ env_manager = args.env_manager
1781
+ host_name = args.host_name
1782
+ no_backup = getattr(args, "no_backup", False)
1783
+ dry_run = getattr(args, "dry_run", False)
1784
+ auto_approve = getattr(args, "auto_approve", False)
1785
+
1786
+ try:
1787
+ # Validate host type
1788
+ try:
1789
+ host_type = MCPHostType(host_name)
1790
+ except ValueError:
1791
+ format_validation_error(ValidationError(
1792
+ f"Invalid host '{host_name}'",
1793
+ field="host_name",
1794
+ suggestion=f"Supported hosts: {', '.join(h.value for h in MCPHostType)}"
1795
+ ))
1796
+ return EXIT_ERROR
1797
+
1798
+ # Create ResultReporter for unified output
1799
+ reporter = ResultReporter("hatch mcp remove-host", dry_run=dry_run)
1800
+ reporter.add(ConsequenceType.REMOVE, f"All servers from host '{host_name}'")
1801
+
1802
+ if dry_run:
1803
+ reporter.report_result()
1804
+ return EXIT_SUCCESS
1805
+
1806
+ # Show prompt for confirmation
1807
+ prompt = reporter.report_prompt()
1808
+ if prompt:
1809
+ print(prompt)
1810
+
1811
+ # Confirm operation unless auto-approved
1812
+ if not request_confirmation("Proceed?", auto_approve):
1813
+ format_info("Operation cancelled")
1814
+ return EXIT_SUCCESS
1815
+
1816
+ # Perform host configuration removal
1817
+ mcp_manager = MCPHostConfigurationManager()
1818
+ result = mcp_manager.remove_host_configuration(
1819
+ hostname=host_name, no_backup=no_backup
1820
+ )
1821
+
1822
+ if result.success:
1823
+ if result.backup_path:
1824
+ reporter.add(ConsequenceType.CREATE, f"Backup: {result.backup_path}")
1825
+
1826
+ # Update environment tracking across all environments
1827
+ updates_count = env_manager.clear_host_from_all_packages_all_envs(host_name)
1828
+ if updates_count > 0:
1829
+ reporter.add(ConsequenceType.UPDATE, f"Updated {updates_count} package entries across environments")
1830
+
1831
+ reporter.report_result()
1832
+ return EXIT_SUCCESS
1833
+ else:
1834
+ print(
1835
+ f"[ERROR] Failed to remove host configuration for '{host_name}': {result.error_message}"
1836
+ )
1837
+ return EXIT_ERROR
1838
+
1839
+ except Exception as e:
1840
+ reporter = ResultReporter("hatch mcp remove-host")
1841
+ reporter.report_error("Failed to remove host configuration", details=[f"Reason: {str(e)}"])
1842
+ return EXIT_ERROR
1843
+
1844
+
1845
+ def handle_mcp_sync(args: Namespace) -> int:
1846
+ """Handle 'hatch mcp sync' command.
1847
+
1848
+ Synchronizes MCP server configurations from a source to target hosts.
1849
+
1850
+ Args:
1851
+ args: Namespace with:
1852
+ - from_env: Source environment name
1853
+ - from_host: Source host name
1854
+ - to_host: Comma-separated list of target hosts
1855
+ - servers: Comma-separated list of server names to sync
1856
+ - pattern: Pattern to filter servers
1857
+ - dry_run: If True, show what would be done without making changes
1858
+ - auto_approve: If True, skip confirmation prompt
1859
+ - no_backup: If True, skip creating backups
1860
+
1861
+ Returns:
1862
+ int: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
1863
+ """
1864
+ from hatch.cli.cli_utils import (
1865
+ request_confirmation,
1866
+ parse_host_list,
1867
+ ResultReporter,
1868
+ ConsequenceType,
1869
+ )
1870
+
1871
+ from_env = getattr(args, "from_env", None)
1872
+ from_host = getattr(args, "from_host", None)
1873
+ to_hosts = getattr(args, "to_host", None)
1874
+ servers = getattr(args, "servers", None)
1875
+ pattern = getattr(args, "pattern", None)
1876
+ dry_run = getattr(args, "dry_run", False)
1877
+ auto_approve = getattr(args, "auto_approve", False)
1878
+ no_backup = getattr(args, "no_backup", False)
1879
+
1880
+ try:
1881
+ # Parse target hosts
1882
+ if not to_hosts:
1883
+ format_validation_error(ValidationError(
1884
+ "Must specify --to-host",
1885
+ field="--to-host",
1886
+ suggestion="Use --to-host HOST1,HOST2 or --to-host all"
1887
+ ))
1888
+ return EXIT_ERROR
1889
+
1890
+ target_hosts = parse_host_list(to_hosts)
1891
+
1892
+ # Parse server filters
1893
+ server_list = None
1894
+ if servers:
1895
+ server_list = [s.strip() for s in servers.split(",") if s.strip()]
1896
+
1897
+ # Create ResultReporter for unified output
1898
+ reporter = ResultReporter("hatch mcp sync", dry_run=dry_run)
1899
+
1900
+ # Build source description
1901
+ source_desc = f"environment '{from_env}'" if from_env else f"host '{from_host}'"
1902
+
1903
+ # Add sync consequences for preview
1904
+ for target_host in target_hosts:
1905
+ reporter.add(ConsequenceType.SYNC, f"{source_desc} → '{target_host}'")
1906
+
1907
+ if dry_run:
1908
+ reporter.report_result()
1909
+ if server_list:
1910
+ print(f" Server filter: {', '.join(server_list)}")
1911
+ elif pattern:
1912
+ print(f" Pattern filter: {pattern}")
1913
+ return EXIT_SUCCESS
1914
+
1915
+ # Show prompt for confirmation
1916
+ prompt = reporter.report_prompt()
1917
+ if prompt:
1918
+ print(prompt)
1919
+
1920
+ # Confirm operation unless auto-approved
1921
+ if not request_confirmation("Proceed?", auto_approve):
1922
+ format_info("Operation cancelled")
1923
+ return EXIT_SUCCESS
1924
+
1925
+ # Perform synchronization
1926
+ mcp_manager = MCPHostConfigurationManager()
1927
+ result = mcp_manager.sync_configurations(
1928
+ from_env=from_env,
1929
+ from_host=from_host,
1930
+ to_hosts=target_hosts,
1931
+ servers=server_list,
1932
+ pattern=pattern,
1933
+ no_backup=no_backup,
1934
+ )
1935
+
1936
+ if result.success:
1937
+ # Create new reporter for results with actual sync details
1938
+ result_reporter = ResultReporter("hatch mcp sync", dry_run=False)
1939
+ for res in result.results:
1940
+ if res.success:
1941
+ result_reporter.add(ConsequenceType.SYNC, f"→ {res.hostname}")
1942
+ else:
1943
+ result_reporter.add(ConsequenceType.SKIP, f"→ {res.hostname}: {res.error_message}")
1944
+
1945
+ # Add sync statistics as summary details
1946
+ result_reporter.add(ConsequenceType.UPDATE, f"Servers synced: {result.servers_synced}")
1947
+ result_reporter.add(ConsequenceType.UPDATE, f"Hosts updated: {result.hosts_updated}")
1948
+
1949
+ result_reporter.report_result()
1950
+
1951
+ return EXIT_SUCCESS
1952
+ else:
1953
+ print(f"[ERROR] Synchronization failed")
1954
+ for res in result.results:
1955
+ if not res.success:
1956
+ print(f" ✗ {res.hostname}: {res.error_message}")
1957
+ return EXIT_ERROR
1958
+
1959
+ except ValueError as e:
1960
+ format_validation_error(ValidationError(str(e)))
1961
+ return EXIT_ERROR
1962
+ except Exception as e:
1963
+ reporter = ResultReporter("hatch mcp sync")
1964
+ reporter.report_error("Failed to synchronize", details=[f"Reason: {str(e)}"])
1965
+ return EXIT_ERROR