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.
- hatch/__init__.py +1 -1
- hatch/cli/__init__.py +71 -0
- hatch/cli/__main__.py +1035 -0
- hatch/cli/cli_env.py +865 -0
- hatch/cli/cli_mcp.py +1965 -0
- hatch/cli/cli_package.py +566 -0
- hatch/cli/cli_system.py +136 -0
- hatch/cli/cli_utils.py +1289 -0
- hatch/cli_hatch.py +160 -2838
- hatch/mcp_host_config/__init__.py +10 -10
- hatch/mcp_host_config/adapters/__init__.py +34 -0
- hatch/mcp_host_config/adapters/base.py +170 -0
- hatch/mcp_host_config/adapters/claude.py +105 -0
- hatch/mcp_host_config/adapters/codex.py +104 -0
- hatch/mcp_host_config/adapters/cursor.py +83 -0
- hatch/mcp_host_config/adapters/gemini.py +75 -0
- hatch/mcp_host_config/adapters/kiro.py +78 -0
- hatch/mcp_host_config/adapters/lmstudio.py +79 -0
- hatch/mcp_host_config/adapters/registry.py +149 -0
- hatch/mcp_host_config/adapters/vscode.py +83 -0
- hatch/mcp_host_config/backup.py +5 -3
- hatch/mcp_host_config/fields.py +126 -0
- hatch/mcp_host_config/models.py +161 -456
- hatch/mcp_host_config/reporting.py +57 -16
- hatch/mcp_host_config/strategies.py +155 -87
- hatch/template_generator.py +1 -1
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/METADATA +3 -2
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/RECORD +52 -43
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/WHEEL +1 -1
- hatch_xclam-0.8.0.dev1.dist-info/entry_points.txt +2 -0
- tests/cli_test_utils.py +280 -0
- tests/integration/cli/__init__.py +14 -0
- tests/integration/cli/test_cli_reporter_integration.py +2439 -0
- tests/integration/mcp/__init__.py +0 -0
- tests/integration/mcp/test_adapter_serialization.py +173 -0
- tests/regression/cli/__init__.py +16 -0
- tests/regression/cli/test_color_logic.py +268 -0
- tests/regression/cli/test_consequence_type.py +298 -0
- tests/regression/cli/test_error_formatting.py +328 -0
- tests/regression/cli/test_result_reporter.py +586 -0
- tests/regression/cli/test_table_formatter.py +211 -0
- tests/regression/mcp/__init__.py +0 -0
- tests/regression/mcp/test_field_filtering.py +162 -0
- tests/test_cli_version.py +7 -5
- tests/test_data/fixtures/cli_reporter_fixtures.py +184 -0
- tests/unit/__init__.py +0 -0
- tests/unit/mcp/__init__.py +0 -0
- tests/unit/mcp/test_adapter_protocol.py +138 -0
- tests/unit/mcp/test_adapter_registry.py +158 -0
- tests/unit/mcp/test_config_model.py +146 -0
- hatch_xclam-0.7.1.dev3.dist-info/entry_points.txt +0 -2
- tests/integration/test_mcp_kiro_integration.py +0 -153
- tests/regression/test_mcp_codex_backup_integration.py +0 -162
- tests/regression/test_mcp_codex_host_strategy.py +0 -163
- tests/regression/test_mcp_codex_model_validation.py +0 -117
- tests/regression/test_mcp_kiro_backup_integration.py +0 -241
- tests/regression/test_mcp_kiro_cli_integration.py +0 -141
- tests/regression/test_mcp_kiro_decorator_registration.py +0 -71
- tests/regression/test_mcp_kiro_host_strategy.py +0 -214
- tests/regression/test_mcp_kiro_model_validation.py +0 -116
- tests/regression/test_mcp_kiro_omni_conversion.py +0 -104
- tests/test_mcp_atomic_operations.py +0 -276
- tests/test_mcp_backup_integration.py +0 -308
- tests/test_mcp_cli_all_host_specific_args.py +0 -496
- tests/test_mcp_cli_backup_management.py +0 -295
- tests/test_mcp_cli_direct_management.py +0 -456
- tests/test_mcp_cli_discovery_listing.py +0 -582
- tests/test_mcp_cli_host_config_integration.py +0 -823
- tests/test_mcp_cli_package_management.py +0 -360
- tests/test_mcp_cli_partial_updates.py +0 -859
- tests/test_mcp_environment_integration.py +0 -520
- tests/test_mcp_host_config_backup.py +0 -257
- tests/test_mcp_host_configuration_manager.py +0 -331
- tests/test_mcp_host_registry_decorator.py +0 -348
- tests/test_mcp_pydantic_architecture_v4.py +0 -603
- tests/test_mcp_server_config_models.py +0 -242
- tests/test_mcp_server_config_type_field.py +0 -221
- tests/test_mcp_sync_functionality.py +0 -316
- tests/test_mcp_user_feedback_reporting.py +0 -359
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/licenses/LICENSE +0 -0
- {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
|