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_hatch.py
CHANGED
|
@@ -1,24 +1,112 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
1
|
+
"""Backward compatibility shim for Hatch CLI.
|
|
2
|
+
|
|
3
|
+
.. deprecated:: 0.7.2
|
|
4
|
+
This module is deprecated. Import from ``hatch.cli`` instead.
|
|
5
|
+
This shim will be removed in version 0.9.0.
|
|
6
|
+
|
|
7
|
+
This module re-exports all public symbols from the new hatch.cli package
|
|
8
|
+
to maintain backward compatibility for external consumers who import from
|
|
9
|
+
hatch.cli_hatch directly.
|
|
10
|
+
|
|
11
|
+
Migration Note:
|
|
12
|
+
New code should import from hatch.cli instead:
|
|
13
|
+
|
|
14
|
+
# Old (deprecated):
|
|
15
|
+
from hatch.cli_hatch import main, handle_mcp_configure
|
|
16
|
+
|
|
17
|
+
# New (preferred):
|
|
18
|
+
from hatch.cli import main
|
|
19
|
+
from hatch.cli.cli_mcp import handle_mcp_configure
|
|
20
|
+
|
|
21
|
+
Implementation Modules:
|
|
22
|
+
- hatch.cli.__main__: Entry point and argument parsing
|
|
23
|
+
- hatch.cli.cli_utils: Shared utilities and constants
|
|
24
|
+
- hatch.cli.cli_mcp: MCP host configuration handlers
|
|
25
|
+
- hatch.cli.cli_env: Environment management handlers
|
|
26
|
+
- hatch.cli.cli_package: Package management handlers
|
|
27
|
+
- hatch.cli.cli_system: System commands (create, validate)
|
|
28
|
+
|
|
29
|
+
Exported Symbols:
|
|
30
|
+
- main: CLI entry point
|
|
31
|
+
- All MCP handlers (handle_mcp_*)
|
|
32
|
+
- All utility functions (parse_*, request_confirmation, etc.)
|
|
33
|
+
- Exit code constants (EXIT_SUCCESS, EXIT_ERROR)
|
|
34
|
+
- HatchEnvironmentManager (re-exported for convenience)
|
|
8
35
|
"""
|
|
9
36
|
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
37
|
+
import warnings
|
|
38
|
+
|
|
39
|
+
warnings.warn(
|
|
40
|
+
"hatch.cli_hatch is deprecated since version 0.7.2. "
|
|
41
|
+
"Import from hatch.cli instead. "
|
|
42
|
+
"This module will be removed in version 0.9.0.",
|
|
43
|
+
DeprecationWarning,
|
|
44
|
+
stacklevel=2
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Re-export main entry point
|
|
48
|
+
from hatch.cli import main
|
|
49
|
+
|
|
50
|
+
# Re-export utilities
|
|
51
|
+
from hatch.cli.cli_utils import (
|
|
52
|
+
EXIT_SUCCESS,
|
|
53
|
+
EXIT_ERROR,
|
|
54
|
+
get_hatch_version,
|
|
55
|
+
request_confirmation,
|
|
56
|
+
parse_env_vars,
|
|
57
|
+
parse_header,
|
|
58
|
+
parse_input,
|
|
59
|
+
parse_host_list,
|
|
60
|
+
get_package_mcp_server_config,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Re-export MCP handlers (for backward compatibility with tests)
|
|
64
|
+
from hatch.cli.cli_mcp import (
|
|
65
|
+
handle_mcp_discover_hosts,
|
|
66
|
+
handle_mcp_discover_servers,
|
|
67
|
+
handle_mcp_list_hosts,
|
|
68
|
+
handle_mcp_list_servers,
|
|
69
|
+
handle_mcp_show,
|
|
70
|
+
handle_mcp_backup_restore,
|
|
71
|
+
handle_mcp_backup_list,
|
|
72
|
+
handle_mcp_backup_clean,
|
|
73
|
+
handle_mcp_configure,
|
|
74
|
+
handle_mcp_remove,
|
|
75
|
+
handle_mcp_remove_server,
|
|
76
|
+
handle_mcp_remove_host,
|
|
77
|
+
handle_mcp_sync,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Re-export environment handlers
|
|
81
|
+
from hatch.cli.cli_env import (
|
|
82
|
+
handle_env_create,
|
|
83
|
+
handle_env_remove,
|
|
84
|
+
handle_env_list,
|
|
85
|
+
handle_env_use,
|
|
86
|
+
handle_env_current,
|
|
87
|
+
handle_env_show,
|
|
88
|
+
handle_env_python_init,
|
|
89
|
+
handle_env_python_info,
|
|
90
|
+
handle_env_python_remove,
|
|
91
|
+
handle_env_python_shell,
|
|
92
|
+
handle_env_python_add_hatch_mcp,
|
|
93
|
+
)
|
|
18
94
|
|
|
19
|
-
|
|
20
|
-
from
|
|
95
|
+
# Re-export package handlers
|
|
96
|
+
from hatch.cli.cli_package import (
|
|
97
|
+
handle_package_add,
|
|
98
|
+
handle_package_remove,
|
|
99
|
+
handle_package_list,
|
|
100
|
+
handle_package_sync,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Re-export system handlers
|
|
104
|
+
from hatch.cli.cli_system import (
|
|
105
|
+
handle_create,
|
|
106
|
+
handle_validate,
|
|
107
|
+
)
|
|
21
108
|
|
|
109
|
+
# Re-export commonly used types for backward compatibility
|
|
22
110
|
from hatch.environment_manager import HatchEnvironmentManager
|
|
23
111
|
from hatch.mcp_host_config import (
|
|
24
112
|
MCPHostConfigurationManager,
|
|
@@ -26,2825 +114,59 @@ from hatch.mcp_host_config import (
|
|
|
26
114
|
MCPHostType,
|
|
27
115
|
MCPServerConfig,
|
|
28
116
|
)
|
|
29
|
-
from hatch.mcp_host_config.models import HOST_MODEL_REGISTRY, MCPServerConfigOmni
|
|
30
|
-
from hatch.mcp_host_config.reporting import display_report, generate_conversion_report
|
|
31
|
-
from hatch.template_generator import create_package_template
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def get_hatch_version() -> str:
|
|
35
|
-
"""Get Hatch version from package metadata.
|
|
36
|
-
|
|
37
|
-
Returns:
|
|
38
|
-
str: Version string from package metadata, or 'unknown (development mode)'
|
|
39
|
-
if package is not installed.
|
|
40
|
-
"""
|
|
41
|
-
try:
|
|
42
|
-
return version("hatch")
|
|
43
|
-
except PackageNotFoundError:
|
|
44
|
-
return "unknown (development mode)"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def parse_host_list(host_arg: str):
|
|
48
|
-
"""Parse comma-separated host list or 'all'."""
|
|
49
|
-
if not host_arg:
|
|
50
|
-
return []
|
|
51
|
-
|
|
52
|
-
if host_arg.lower() == "all":
|
|
53
|
-
return MCPHostRegistry.detect_available_hosts()
|
|
54
|
-
|
|
55
|
-
hosts = []
|
|
56
|
-
for host_str in host_arg.split(","):
|
|
57
|
-
host_str = host_str.strip()
|
|
58
|
-
try:
|
|
59
|
-
host_type = MCPHostType(host_str)
|
|
60
|
-
hosts.append(host_type)
|
|
61
|
-
except ValueError:
|
|
62
|
-
available = [h.value for h in MCPHostType]
|
|
63
|
-
raise ValueError(f"Unknown host '{host_str}'. Available: {available}")
|
|
64
|
-
|
|
65
|
-
return hosts
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def request_confirmation(message: str, auto_approve: bool = False) -> bool:
|
|
69
|
-
"""Request user confirmation with non-TTY support following Hatch patterns."""
|
|
70
|
-
import os
|
|
71
|
-
import sys
|
|
72
|
-
|
|
73
|
-
# Check for auto-approve first
|
|
74
|
-
if auto_approve or os.getenv("HATCH_AUTO_APPROVE", "").lower() in (
|
|
75
|
-
"1",
|
|
76
|
-
"true",
|
|
77
|
-
"yes",
|
|
78
|
-
):
|
|
79
|
-
return True
|
|
80
|
-
|
|
81
|
-
# Interactive mode - request user input (works in both TTY and test environments)
|
|
82
|
-
try:
|
|
83
|
-
while True:
|
|
84
|
-
response = input(f"{message} [y/N]: ").strip().lower()
|
|
85
|
-
if response in ["y", "yes"]:
|
|
86
|
-
return True
|
|
87
|
-
elif response in ["n", "no", ""]:
|
|
88
|
-
return False
|
|
89
|
-
else:
|
|
90
|
-
print("Please enter 'y' for yes or 'n' for no.")
|
|
91
|
-
except (EOFError, KeyboardInterrupt):
|
|
92
|
-
# Only auto-approve on EOF/interrupt if not in TTY (non-interactive environment)
|
|
93
|
-
if not sys.stdin.isatty():
|
|
94
|
-
return True
|
|
95
|
-
return False
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def get_package_mcp_server_config(
|
|
99
|
-
env_manager: HatchEnvironmentManager, env_name: str, package_name: str
|
|
100
|
-
) -> MCPServerConfig:
|
|
101
|
-
"""Get MCP server configuration for a package using existing APIs."""
|
|
102
|
-
try:
|
|
103
|
-
# Get package info from environment
|
|
104
|
-
packages = env_manager.list_packages(env_name)
|
|
105
|
-
package_info = next(
|
|
106
|
-
(pkg for pkg in packages if pkg["name"] == package_name), None
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
if not package_info:
|
|
110
|
-
raise ValueError(
|
|
111
|
-
f"Package '{package_name}' not found in environment '{env_name}'"
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
# Load package metadata using existing pattern from environment_manager.py:716-727
|
|
115
|
-
package_path = Path(package_info["source"]["path"])
|
|
116
|
-
metadata_path = package_path / "hatch_metadata.json"
|
|
117
|
-
|
|
118
|
-
if not metadata_path.exists():
|
|
119
|
-
raise ValueError(
|
|
120
|
-
f"Package '{package_name}' is not a Hatch package (no hatch_metadata.json)"
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
with open(metadata_path, "r") as f:
|
|
124
|
-
metadata = json.load(f)
|
|
125
|
-
|
|
126
|
-
# Use PackageService for schema-aware access
|
|
127
|
-
from hatch_validator.package.package_service import PackageService
|
|
128
|
-
|
|
129
|
-
package_service = PackageService(metadata)
|
|
130
|
-
|
|
131
|
-
# Get the HatchMCP entry point (this handles both v1.2.0 and v1.2.1 schemas)
|
|
132
|
-
mcp_entry_point = package_service.get_mcp_entry_point()
|
|
133
|
-
if not mcp_entry_point:
|
|
134
|
-
raise ValueError(
|
|
135
|
-
f"Package '{package_name}' does not have a HatchMCP entry point"
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
# Get environment-specific Python executable
|
|
139
|
-
python_executable = env_manager.get_current_python_executable()
|
|
140
|
-
if not python_executable:
|
|
141
|
-
# Fallback to system Python if no environment-specific Python available
|
|
142
|
-
python_executable = "python"
|
|
143
|
-
|
|
144
|
-
# Create server configuration
|
|
145
|
-
server_path = str(package_path / mcp_entry_point)
|
|
146
|
-
server_config = MCPServerConfig(
|
|
147
|
-
name=package_name, command=python_executable, args=[server_path], env={}
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
return server_config
|
|
151
|
-
|
|
152
|
-
except Exception as e:
|
|
153
|
-
raise ValueError(
|
|
154
|
-
f"Failed to get MCP server config for package '{package_name}': {e}"
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def handle_mcp_discover_hosts():
|
|
159
|
-
"""Handle 'hatch mcp discover hosts' command."""
|
|
160
|
-
try:
|
|
161
|
-
# Import strategies to trigger registration
|
|
162
|
-
import hatch.mcp_host_config.strategies
|
|
163
|
-
|
|
164
|
-
available_hosts = MCPHostRegistry.detect_available_hosts()
|
|
165
|
-
print("Available MCP host platforms:")
|
|
166
|
-
|
|
167
|
-
for host_type in MCPHostType:
|
|
168
|
-
try:
|
|
169
|
-
strategy = MCPHostRegistry.get_strategy(host_type)
|
|
170
|
-
config_path = strategy.get_config_path()
|
|
171
|
-
is_available = host_type in available_hosts
|
|
172
|
-
|
|
173
|
-
status = "✓ Available" if is_available else "✗ Not detected"
|
|
174
|
-
print(f" {host_type.value}: {status}")
|
|
175
|
-
if config_path:
|
|
176
|
-
print(f" Config path: {config_path}")
|
|
177
|
-
except Exception as e:
|
|
178
|
-
print(f" {host_type.value}: Error - {e}")
|
|
179
|
-
|
|
180
|
-
return 0
|
|
181
|
-
except Exception as e:
|
|
182
|
-
print(f"Error discovering hosts: {e}")
|
|
183
|
-
return 1
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def handle_mcp_discover_servers(
|
|
187
|
-
env_manager: HatchEnvironmentManager, env_name: Optional[str] = None
|
|
188
|
-
):
|
|
189
|
-
"""Handle 'hatch mcp discover servers' command."""
|
|
190
|
-
try:
|
|
191
|
-
env_name = env_name or env_manager.get_current_environment()
|
|
192
|
-
|
|
193
|
-
if not env_manager.environment_exists(env_name):
|
|
194
|
-
print(f"Error: Environment '{env_name}' does not exist")
|
|
195
|
-
return 1
|
|
196
|
-
|
|
197
|
-
packages = env_manager.list_packages(env_name)
|
|
198
|
-
mcp_packages = []
|
|
199
|
-
|
|
200
|
-
for package in packages:
|
|
201
|
-
try:
|
|
202
|
-
# Check if package has MCP server entry point
|
|
203
|
-
server_config = get_package_mcp_server_config(
|
|
204
|
-
env_manager, env_name, package["name"]
|
|
205
|
-
)
|
|
206
|
-
mcp_packages.append(
|
|
207
|
-
{"package": package, "server_config": server_config}
|
|
208
|
-
)
|
|
209
|
-
except ValueError:
|
|
210
|
-
# Package doesn't have MCP server
|
|
211
|
-
continue
|
|
212
|
-
|
|
213
|
-
if not mcp_packages:
|
|
214
|
-
print(f"No MCP servers found in environment '{env_name}'")
|
|
215
|
-
return 0
|
|
216
|
-
|
|
217
|
-
print(f"MCP servers in environment '{env_name}':")
|
|
218
|
-
for item in mcp_packages:
|
|
219
|
-
package = item["package"]
|
|
220
|
-
server_config = item["server_config"]
|
|
221
|
-
print(f" {server_config.name}:")
|
|
222
|
-
print(
|
|
223
|
-
f" Package: {package['name']} v{package.get('version', 'unknown')}"
|
|
224
|
-
)
|
|
225
|
-
print(f" Command: {server_config.command}")
|
|
226
|
-
print(f" Args: {server_config.args}")
|
|
227
|
-
if server_config.env:
|
|
228
|
-
print(f" Environment: {server_config.env}")
|
|
229
|
-
|
|
230
|
-
return 0
|
|
231
|
-
except Exception as e:
|
|
232
|
-
print(f"Error discovering servers: {e}")
|
|
233
|
-
return 1
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def handle_mcp_list_hosts(
|
|
237
|
-
env_manager: HatchEnvironmentManager,
|
|
238
|
-
env_name: Optional[str] = None,
|
|
239
|
-
detailed: bool = False,
|
|
240
|
-
):
|
|
241
|
-
"""Handle 'hatch mcp list hosts' command - shows configured hosts in environment."""
|
|
242
|
-
try:
|
|
243
|
-
from collections import defaultdict
|
|
244
|
-
|
|
245
|
-
# Resolve environment name
|
|
246
|
-
target_env = env_name or env_manager.get_current_environment()
|
|
247
|
-
|
|
248
|
-
# Validate environment exists
|
|
249
|
-
if not env_manager.environment_exists(target_env):
|
|
250
|
-
available_envs = env_manager.list_environments()
|
|
251
|
-
print(f"Error: Environment '{target_env}' does not exist.")
|
|
252
|
-
if available_envs:
|
|
253
|
-
print(f"Available environments: {', '.join(available_envs)}")
|
|
254
|
-
return 1
|
|
255
|
-
|
|
256
|
-
# Collect hosts from configured_hosts across all packages in environment
|
|
257
|
-
hosts = defaultdict(int)
|
|
258
|
-
host_details = defaultdict(list)
|
|
259
|
-
|
|
260
|
-
try:
|
|
261
|
-
env_data = env_manager.get_environment_data(target_env)
|
|
262
|
-
packages = env_data.get("packages", [])
|
|
263
|
-
|
|
264
|
-
for package in packages:
|
|
265
|
-
package_name = package.get("name", "unknown")
|
|
266
|
-
configured_hosts = package.get("configured_hosts", {})
|
|
267
|
-
|
|
268
|
-
for host_name, host_config in configured_hosts.items():
|
|
269
|
-
hosts[host_name] += 1
|
|
270
|
-
if detailed:
|
|
271
|
-
config_path = host_config.get("config_path", "N/A")
|
|
272
|
-
configured_at = host_config.get("configured_at", "N/A")
|
|
273
|
-
host_details[host_name].append(
|
|
274
|
-
{
|
|
275
|
-
"package": package_name,
|
|
276
|
-
"config_path": config_path,
|
|
277
|
-
"configured_at": configured_at,
|
|
278
|
-
}
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
except Exception as e:
|
|
282
|
-
print(f"Error reading environment data: {e}")
|
|
283
|
-
return 1
|
|
284
|
-
|
|
285
|
-
# Display results
|
|
286
|
-
if not hosts:
|
|
287
|
-
print(f"No configured hosts for environment '{target_env}'")
|
|
288
|
-
return 0
|
|
289
|
-
|
|
290
|
-
print(f"Configured hosts for environment '{target_env}':")
|
|
291
|
-
|
|
292
|
-
for host_name, package_count in sorted(hosts.items()):
|
|
293
|
-
if detailed:
|
|
294
|
-
print(f"\n{host_name} ({package_count} packages):")
|
|
295
|
-
for detail in host_details[host_name]:
|
|
296
|
-
print(f" - Package: {detail['package']}")
|
|
297
|
-
print(f" Config path: {detail['config_path']}")
|
|
298
|
-
print(f" Configured at: {detail['configured_at']}")
|
|
299
|
-
else:
|
|
300
|
-
print(f" - {host_name} ({package_count} packages)")
|
|
301
|
-
|
|
302
|
-
return 0
|
|
303
|
-
except Exception as e:
|
|
304
|
-
print(f"Error listing hosts: {e}")
|
|
305
|
-
return 1
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def handle_mcp_list_servers(
|
|
309
|
-
env_manager: HatchEnvironmentManager, env_name: Optional[str] = None
|
|
310
|
-
):
|
|
311
|
-
"""Handle 'hatch mcp list servers' command."""
|
|
312
|
-
try:
|
|
313
|
-
env_name = env_name or env_manager.get_current_environment()
|
|
314
|
-
|
|
315
|
-
if not env_manager.environment_exists(env_name):
|
|
316
|
-
print(f"Error: Environment '{env_name}' does not exist")
|
|
317
|
-
return 1
|
|
318
|
-
|
|
319
|
-
packages = env_manager.list_packages(env_name)
|
|
320
|
-
mcp_packages = []
|
|
321
|
-
|
|
322
|
-
for package in packages:
|
|
323
|
-
# Check if package has host configuration tracking (indicating MCP server)
|
|
324
|
-
configured_hosts = package.get("configured_hosts", {})
|
|
325
|
-
if configured_hosts:
|
|
326
|
-
# Use the tracked server configuration from any host
|
|
327
|
-
first_host = next(iter(configured_hosts.values()))
|
|
328
|
-
server_config_data = first_host.get("server_config", {})
|
|
329
|
-
|
|
330
|
-
# Create a simple server config object
|
|
331
|
-
class SimpleServerConfig:
|
|
332
|
-
def __init__(self, data):
|
|
333
|
-
self.name = data.get("name", package["name"])
|
|
334
|
-
self.command = data.get("command", "unknown")
|
|
335
|
-
self.args = data.get("args", [])
|
|
336
|
-
|
|
337
|
-
server_config = SimpleServerConfig(server_config_data)
|
|
338
|
-
mcp_packages.append(
|
|
339
|
-
{"package": package, "server_config": server_config}
|
|
340
|
-
)
|
|
341
|
-
else:
|
|
342
|
-
# Try the original method as fallback
|
|
343
|
-
try:
|
|
344
|
-
server_config = get_package_mcp_server_config(
|
|
345
|
-
env_manager, env_name, package["name"]
|
|
346
|
-
)
|
|
347
|
-
mcp_packages.append(
|
|
348
|
-
{"package": package, "server_config": server_config}
|
|
349
|
-
)
|
|
350
|
-
except:
|
|
351
|
-
# Package doesn't have MCP server or method failed
|
|
352
|
-
continue
|
|
353
|
-
|
|
354
|
-
if not mcp_packages:
|
|
355
|
-
print(f"No MCP servers configured in environment '{env_name}'")
|
|
356
|
-
return 0
|
|
357
|
-
|
|
358
|
-
print(f"MCP servers in environment '{env_name}':")
|
|
359
|
-
print(f"{'Server Name':<20} {'Package':<20} {'Version':<10} {'Command'}")
|
|
360
|
-
print("-" * 80)
|
|
361
|
-
|
|
362
|
-
for item in mcp_packages:
|
|
363
|
-
package = item["package"]
|
|
364
|
-
server_config = item["server_config"]
|
|
365
|
-
|
|
366
|
-
server_name = server_config.name
|
|
367
|
-
package_name = package["name"]
|
|
368
|
-
version = package.get("version", "unknown")
|
|
369
|
-
command = f"{server_config.command} {' '.join(server_config.args)}"
|
|
370
|
-
|
|
371
|
-
print(f"{server_name:<20} {package_name:<20} {version:<10} {command}")
|
|
372
|
-
|
|
373
|
-
# Display host configuration tracking information
|
|
374
|
-
configured_hosts = package.get("configured_hosts", {})
|
|
375
|
-
if configured_hosts:
|
|
376
|
-
print(f"{'':>20} Configured on hosts:")
|
|
377
|
-
for hostname, host_config in configured_hosts.items():
|
|
378
|
-
config_path = host_config.get("config_path", "unknown")
|
|
379
|
-
last_synced = host_config.get("last_synced", "unknown")
|
|
380
|
-
# Format the timestamp for better readability
|
|
381
|
-
if last_synced != "unknown":
|
|
382
|
-
try:
|
|
383
|
-
from datetime import datetime
|
|
384
|
-
|
|
385
|
-
dt = datetime.fromisoformat(
|
|
386
|
-
last_synced.replace("Z", "+00:00")
|
|
387
|
-
)
|
|
388
|
-
last_synced = dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
389
|
-
except:
|
|
390
|
-
pass # Keep original format if parsing fails
|
|
391
|
-
print(
|
|
392
|
-
f"{'':>22} - {hostname}: {config_path} (synced: {last_synced})"
|
|
393
|
-
)
|
|
394
|
-
else:
|
|
395
|
-
print(f"{'':>20} No host configurations tracked")
|
|
396
|
-
print() # Add blank line between servers
|
|
397
|
-
|
|
398
|
-
return 0
|
|
399
|
-
except Exception as e:
|
|
400
|
-
print(f"Error listing servers: {e}")
|
|
401
|
-
return 1
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
def handle_mcp_backup_restore(
|
|
405
|
-
env_manager: HatchEnvironmentManager,
|
|
406
|
-
host: str,
|
|
407
|
-
backup_file: Optional[str] = None,
|
|
408
|
-
dry_run: bool = False,
|
|
409
|
-
auto_approve: bool = False,
|
|
410
|
-
):
|
|
411
|
-
"""Handle 'hatch mcp backup restore' command."""
|
|
412
|
-
try:
|
|
413
|
-
from hatch.mcp_host_config.backup import MCPHostConfigBackupManager
|
|
414
|
-
|
|
415
|
-
# Validate host type
|
|
416
|
-
try:
|
|
417
|
-
host_type = MCPHostType(host)
|
|
418
|
-
except ValueError:
|
|
419
|
-
print(
|
|
420
|
-
f"Error: Invalid host '{host}'. Supported hosts: {[h.value for h in MCPHostType]}"
|
|
421
|
-
)
|
|
422
|
-
return 1
|
|
423
|
-
|
|
424
|
-
backup_manager = MCPHostConfigBackupManager()
|
|
425
|
-
|
|
426
|
-
# Get backup file path
|
|
427
|
-
if backup_file:
|
|
428
|
-
backup_path = backup_manager.backup_root / host / backup_file
|
|
429
|
-
if not backup_path.exists():
|
|
430
|
-
print(f"Error: Backup file '{backup_file}' not found for host '{host}'")
|
|
431
|
-
return 1
|
|
432
|
-
else:
|
|
433
|
-
backup_path = backup_manager._get_latest_backup(host)
|
|
434
|
-
if not backup_path:
|
|
435
|
-
print(f"Error: No backups found for host '{host}'")
|
|
436
|
-
return 1
|
|
437
|
-
backup_file = backup_path.name
|
|
438
|
-
|
|
439
|
-
if dry_run:
|
|
440
|
-
print(f"[DRY RUN] Would restore backup for host '{host}':")
|
|
441
|
-
print(f"[DRY RUN] Backup file: {backup_file}")
|
|
442
|
-
print(f"[DRY RUN] Backup path: {backup_path}")
|
|
443
|
-
return 0
|
|
444
|
-
|
|
445
|
-
# Confirm operation unless auto-approved
|
|
446
|
-
if not request_confirmation(
|
|
447
|
-
f"Restore backup '{backup_file}' for host '{host}'? This will overwrite current configuration.",
|
|
448
|
-
auto_approve,
|
|
449
|
-
):
|
|
450
|
-
print("Operation cancelled.")
|
|
451
|
-
return 0
|
|
452
|
-
|
|
453
|
-
# Perform restoration
|
|
454
|
-
success = backup_manager.restore_backup(host, backup_file)
|
|
455
|
-
|
|
456
|
-
if success:
|
|
457
|
-
print(
|
|
458
|
-
f"[SUCCESS] Successfully restored backup '{backup_file}' for host '{host}'"
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
# Read restored configuration to get actual server list
|
|
462
|
-
try:
|
|
463
|
-
# Import strategies to trigger registration
|
|
464
|
-
import hatch.mcp_host_config.strategies
|
|
465
|
-
|
|
466
|
-
host_type = MCPHostType(host)
|
|
467
|
-
strategy = MCPHostRegistry.get_strategy(host_type)
|
|
468
|
-
restored_config = strategy.read_configuration()
|
|
469
|
-
|
|
470
|
-
# Update environment tracking to match restored state
|
|
471
|
-
updates_count = (
|
|
472
|
-
env_manager.apply_restored_host_configuration_to_environments(
|
|
473
|
-
host, restored_config.servers
|
|
474
|
-
)
|
|
475
|
-
)
|
|
476
|
-
if updates_count > 0:
|
|
477
|
-
print(
|
|
478
|
-
f"Synchronized {updates_count} package entries with restored configuration"
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
except Exception as e:
|
|
482
|
-
print(f"Warning: Could not synchronize environment tracking: {e}")
|
|
483
|
-
|
|
484
|
-
return 0
|
|
485
|
-
else:
|
|
486
|
-
print(f"[ERROR] Failed to restore backup '{backup_file}' for host '{host}'")
|
|
487
|
-
return 1
|
|
488
|
-
|
|
489
|
-
except Exception as e:
|
|
490
|
-
print(f"Error restoring backup: {e}")
|
|
491
|
-
return 1
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
def handle_mcp_backup_list(host: str, detailed: bool = False):
|
|
495
|
-
"""Handle 'hatch mcp backup list' command."""
|
|
496
|
-
try:
|
|
497
|
-
from hatch.mcp_host_config.backup import MCPHostConfigBackupManager
|
|
498
|
-
|
|
499
|
-
# Validate host type
|
|
500
|
-
try:
|
|
501
|
-
host_type = MCPHostType(host)
|
|
502
|
-
except ValueError:
|
|
503
|
-
print(
|
|
504
|
-
f"Error: Invalid host '{host}'. Supported hosts: {[h.value for h in MCPHostType]}"
|
|
505
|
-
)
|
|
506
|
-
return 1
|
|
507
|
-
|
|
508
|
-
backup_manager = MCPHostConfigBackupManager()
|
|
509
|
-
backups = backup_manager.list_backups(host)
|
|
510
|
-
|
|
511
|
-
if not backups:
|
|
512
|
-
print(f"No backups found for host '{host}'")
|
|
513
|
-
return 0
|
|
514
|
-
|
|
515
|
-
print(f"Backups for host '{host}' ({len(backups)} found):")
|
|
516
|
-
|
|
517
|
-
if detailed:
|
|
518
|
-
print(f"{'Backup File':<40} {'Created':<20} {'Size':<10} {'Age (days)'}")
|
|
519
|
-
print("-" * 80)
|
|
520
|
-
|
|
521
|
-
for backup in backups:
|
|
522
|
-
created = backup.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
523
|
-
size = f"{backup.file_size:,} B"
|
|
524
|
-
age = backup.age_days
|
|
525
|
-
|
|
526
|
-
print(f"{backup.file_path.name:<40} {created:<20} {size:<10} {age}")
|
|
527
|
-
else:
|
|
528
|
-
for backup in backups:
|
|
529
|
-
created = backup.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
530
|
-
print(
|
|
531
|
-
f" {backup.file_path.name} (created: {created}, {backup.age_days} days ago)"
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
return 0
|
|
535
|
-
except Exception as e:
|
|
536
|
-
print(f"Error listing backups: {e}")
|
|
537
|
-
return 1
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
def handle_mcp_backup_clean(
|
|
541
|
-
host: str,
|
|
542
|
-
older_than_days: Optional[int] = None,
|
|
543
|
-
keep_count: Optional[int] = None,
|
|
544
|
-
dry_run: bool = False,
|
|
545
|
-
auto_approve: bool = False,
|
|
546
|
-
):
|
|
547
|
-
"""Handle 'hatch mcp backup clean' command."""
|
|
548
|
-
try:
|
|
549
|
-
from hatch.mcp_host_config.backup import MCPHostConfigBackupManager
|
|
550
|
-
|
|
551
|
-
# Validate host type
|
|
552
|
-
try:
|
|
553
|
-
host_type = MCPHostType(host)
|
|
554
|
-
except ValueError:
|
|
555
|
-
print(
|
|
556
|
-
f"Error: Invalid host '{host}'. Supported hosts: {[h.value for h in MCPHostType]}"
|
|
557
|
-
)
|
|
558
|
-
return 1
|
|
559
|
-
|
|
560
|
-
# Validate cleanup criteria
|
|
561
|
-
if not older_than_days and not keep_count:
|
|
562
|
-
print("Error: Must specify either --older-than-days or --keep-count")
|
|
563
|
-
return 1
|
|
564
|
-
|
|
565
|
-
backup_manager = MCPHostConfigBackupManager()
|
|
566
|
-
backups = backup_manager.list_backups(host)
|
|
567
|
-
|
|
568
|
-
if not backups:
|
|
569
|
-
print(f"No backups found for host '{host}'")
|
|
570
|
-
return 0
|
|
571
|
-
|
|
572
|
-
# Determine which backups would be cleaned
|
|
573
|
-
to_clean = []
|
|
574
|
-
|
|
575
|
-
if older_than_days:
|
|
576
|
-
for backup in backups:
|
|
577
|
-
if backup.age_days > older_than_days:
|
|
578
|
-
to_clean.append(backup)
|
|
579
|
-
|
|
580
|
-
if keep_count and len(backups) > keep_count:
|
|
581
|
-
# Keep newest backups, remove oldest
|
|
582
|
-
to_clean.extend(backups[keep_count:])
|
|
583
|
-
|
|
584
|
-
# Remove duplicates while preserving order
|
|
585
|
-
seen = set()
|
|
586
|
-
unique_to_clean = []
|
|
587
|
-
for backup in to_clean:
|
|
588
|
-
if backup.file_path not in seen:
|
|
589
|
-
seen.add(backup.file_path)
|
|
590
|
-
unique_to_clean.append(backup)
|
|
591
|
-
|
|
592
|
-
if not unique_to_clean:
|
|
593
|
-
print(f"No backups match cleanup criteria for host '{host}'")
|
|
594
|
-
return 0
|
|
595
|
-
|
|
596
|
-
if dry_run:
|
|
597
|
-
print(
|
|
598
|
-
f"[DRY RUN] Would clean {len(unique_to_clean)} backup(s) for host '{host}':"
|
|
599
|
-
)
|
|
600
|
-
for backup in unique_to_clean:
|
|
601
|
-
print(
|
|
602
|
-
f"[DRY RUN] {backup.file_path.name} (age: {backup.age_days} days)"
|
|
603
|
-
)
|
|
604
|
-
return 0
|
|
605
|
-
|
|
606
|
-
# Confirm operation unless auto-approved
|
|
607
|
-
if not request_confirmation(
|
|
608
|
-
f"Clean {len(unique_to_clean)} backup(s) for host '{host}'?", auto_approve
|
|
609
|
-
):
|
|
610
|
-
print("Operation cancelled.")
|
|
611
|
-
return 0
|
|
612
|
-
|
|
613
|
-
# Perform cleanup
|
|
614
|
-
filters = {}
|
|
615
|
-
if older_than_days:
|
|
616
|
-
filters["older_than_days"] = older_than_days
|
|
617
|
-
if keep_count:
|
|
618
|
-
filters["keep_count"] = keep_count
|
|
619
|
-
|
|
620
|
-
cleaned_count = backup_manager.clean_backups(host, **filters)
|
|
621
|
-
|
|
622
|
-
if cleaned_count > 0:
|
|
623
|
-
print(f"✓ Successfully cleaned {cleaned_count} backup(s) for host '{host}'")
|
|
624
|
-
return 0
|
|
625
|
-
else:
|
|
626
|
-
print(f"No backups were cleaned for host '{host}'")
|
|
627
|
-
return 0
|
|
628
|
-
|
|
629
|
-
except Exception as e:
|
|
630
|
-
print(f"Error cleaning backups: {e}")
|
|
631
|
-
return 1
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
def parse_env_vars(env_list: Optional[list]) -> dict:
|
|
635
|
-
"""Parse environment variables from command line format."""
|
|
636
|
-
if not env_list:
|
|
637
|
-
return {}
|
|
638
|
-
|
|
639
|
-
env_dict = {}
|
|
640
|
-
for env_var in env_list:
|
|
641
|
-
if "=" not in env_var:
|
|
642
|
-
print(
|
|
643
|
-
f"Warning: Invalid environment variable format '{env_var}'. Expected KEY=VALUE"
|
|
644
|
-
)
|
|
645
|
-
continue
|
|
646
|
-
key, value = env_var.split("=", 1)
|
|
647
|
-
env_dict[key.strip()] = value.strip()
|
|
648
|
-
|
|
649
|
-
return env_dict
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
def parse_header(header_list: Optional[list]) -> dict:
|
|
653
|
-
"""Parse HTTP headers from command line format."""
|
|
654
|
-
if not header_list:
|
|
655
|
-
return {}
|
|
656
|
-
|
|
657
|
-
headers_dict = {}
|
|
658
|
-
for header in header_list:
|
|
659
|
-
if "=" not in header:
|
|
660
|
-
print(f"Warning: Invalid header format '{header}'. Expected KEY=VALUE")
|
|
661
|
-
continue
|
|
662
|
-
key, value = header.split("=", 1)
|
|
663
|
-
headers_dict[key.strip()] = value.strip()
|
|
664
|
-
|
|
665
|
-
return headers_dict
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
def parse_input(input_list: Optional[list]) -> Optional[list]:
|
|
669
|
-
"""Parse VS Code input variable definitions from command line format.
|
|
670
|
-
|
|
671
|
-
Format: type,id,description[,password=true]
|
|
672
|
-
Example: promptString,api-key,GitHub Personal Access Token,password=true
|
|
673
|
-
|
|
674
|
-
Returns:
|
|
675
|
-
List of input variable definition dictionaries, or None if no inputs provided.
|
|
676
|
-
"""
|
|
677
|
-
if not input_list:
|
|
678
|
-
return None
|
|
679
|
-
|
|
680
|
-
parsed_inputs = []
|
|
681
|
-
for input_str in input_list:
|
|
682
|
-
parts = [p.strip() for p in input_str.split(",")]
|
|
683
|
-
if len(parts) < 3:
|
|
684
|
-
print(
|
|
685
|
-
f"Warning: Invalid input format '{input_str}'. Expected: type,id,description[,password=true]"
|
|
686
|
-
)
|
|
687
|
-
continue
|
|
688
|
-
|
|
689
|
-
input_def = {"type": parts[0], "id": parts[1], "description": parts[2]}
|
|
690
|
-
|
|
691
|
-
# Check for optional password flag
|
|
692
|
-
if len(parts) > 3 and parts[3].lower() == "password=true":
|
|
693
|
-
input_def["password"] = True
|
|
694
|
-
|
|
695
|
-
parsed_inputs.append(input_def)
|
|
696
|
-
|
|
697
|
-
return parsed_inputs if parsed_inputs else None
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
def handle_mcp_configure(
|
|
701
|
-
host: str,
|
|
702
|
-
server_name: str,
|
|
703
|
-
command: str,
|
|
704
|
-
args: list,
|
|
705
|
-
env: Optional[list] = None,
|
|
706
|
-
url: Optional[str] = None,
|
|
707
|
-
header: Optional[list] = None,
|
|
708
|
-
timeout: Optional[int] = None,
|
|
709
|
-
trust: bool = False,
|
|
710
|
-
cwd: Optional[str] = None,
|
|
711
|
-
env_file: Optional[str] = None,
|
|
712
|
-
http_url: Optional[str] = None,
|
|
713
|
-
include_tools: Optional[list] = None,
|
|
714
|
-
exclude_tools: Optional[list] = None,
|
|
715
|
-
input: Optional[list] = None,
|
|
716
|
-
disabled: Optional[bool] = None,
|
|
717
|
-
auto_approve_tools: Optional[list] = None,
|
|
718
|
-
disable_tools: Optional[list] = None,
|
|
719
|
-
env_vars: Optional[list] = None,
|
|
720
|
-
startup_timeout: Optional[int] = None,
|
|
721
|
-
tool_timeout: Optional[int] = None,
|
|
722
|
-
enabled: Optional[bool] = None,
|
|
723
|
-
bearer_token_env_var: Optional[str] = None,
|
|
724
|
-
env_header: Optional[list] = None,
|
|
725
|
-
no_backup: bool = False,
|
|
726
|
-
dry_run: bool = False,
|
|
727
|
-
auto_approve: bool = False,
|
|
728
|
-
):
|
|
729
|
-
"""Handle 'hatch mcp configure' command with ALL host-specific arguments.
|
|
730
|
-
|
|
731
|
-
Host-specific arguments are accepted for all hosts. The reporting system will
|
|
732
|
-
show unsupported fields as "UNSUPPORTED" in the conversion report rather than
|
|
733
|
-
rejecting them upfront.
|
|
734
|
-
"""
|
|
735
|
-
try:
|
|
736
|
-
# Validate host type
|
|
737
|
-
try:
|
|
738
|
-
host_type = MCPHostType(host)
|
|
739
|
-
except ValueError:
|
|
740
|
-
print(
|
|
741
|
-
f"Error: Invalid host '{host}'. Supported hosts: {[h.value for h in MCPHostType]}"
|
|
742
|
-
)
|
|
743
|
-
return 1
|
|
744
|
-
|
|
745
|
-
# Validate Claude Desktop/Code transport restrictions (Issue 2)
|
|
746
|
-
if host_type in (MCPHostType.CLAUDE_DESKTOP, MCPHostType.CLAUDE_CODE):
|
|
747
|
-
if url is not None:
|
|
748
|
-
print(
|
|
749
|
-
f"Error: {host} does not support remote servers (--url). Only local servers with --command are supported."
|
|
750
|
-
)
|
|
751
|
-
return 1
|
|
752
|
-
|
|
753
|
-
# Validate argument dependencies
|
|
754
|
-
if command and header:
|
|
755
|
-
print(
|
|
756
|
-
"Error: --header can only be used with --url or --http-url (remote servers), not with --command (local servers)"
|
|
757
|
-
)
|
|
758
|
-
return 1
|
|
759
|
-
|
|
760
|
-
if (url or http_url) and args:
|
|
761
|
-
print(
|
|
762
|
-
"Error: --args can only be used with --command (local servers), not with --url or --http-url (remote servers)"
|
|
763
|
-
)
|
|
764
|
-
return 1
|
|
765
|
-
|
|
766
|
-
# NOTE: We do NOT validate host-specific arguments here.
|
|
767
|
-
# The reporting system will show unsupported fields as "UNSUPPORTED" in the conversion report.
|
|
768
|
-
# This allows users to see which fields are not supported by their target host without blocking the operation.
|
|
769
|
-
|
|
770
|
-
# Check if server exists (for partial update support)
|
|
771
|
-
manager = MCPHostConfigurationManager()
|
|
772
|
-
existing_config = manager.get_server_config(host, server_name)
|
|
773
|
-
is_update = existing_config is not None
|
|
774
|
-
|
|
775
|
-
# Conditional validation: Create requires command OR url OR http_url, update does not
|
|
776
|
-
if not is_update:
|
|
777
|
-
# Create operation: require command, url, or http_url
|
|
778
|
-
if not command and not url and not http_url:
|
|
779
|
-
print(
|
|
780
|
-
f"Error: When creating a new server, you must provide either --command (for local servers), --url (for SSE remote servers), or --http-url (for HTTP remote servers, Gemini only)"
|
|
781
|
-
)
|
|
782
|
-
return 1
|
|
783
|
-
|
|
784
|
-
# Parse environment variables, headers, and inputs
|
|
785
|
-
env_dict = parse_env_vars(env)
|
|
786
|
-
headers_dict = parse_header(header)
|
|
787
|
-
inputs_list = parse_input(input)
|
|
788
|
-
|
|
789
|
-
# Create Omni configuration (universal model)
|
|
790
|
-
# Only include fields that have actual values to ensure model_dump(exclude_unset=True) works correctly
|
|
791
|
-
omni_config_data = {"name": server_name}
|
|
792
|
-
|
|
793
|
-
if command is not None:
|
|
794
|
-
omni_config_data["command"] = command
|
|
795
|
-
if args is not None:
|
|
796
|
-
# Process args with shlex.split() to handle quoted strings (Issue 4)
|
|
797
|
-
processed_args = []
|
|
798
|
-
for arg in args:
|
|
799
|
-
if arg: # Skip empty strings
|
|
800
|
-
try:
|
|
801
|
-
# Split quoted strings into individual arguments
|
|
802
|
-
split_args = shlex.split(arg)
|
|
803
|
-
processed_args.extend(split_args)
|
|
804
|
-
except ValueError as e:
|
|
805
|
-
# Handle invalid quotes gracefully
|
|
806
|
-
print(f"Warning: Invalid quote in argument '{arg}': {e}")
|
|
807
|
-
processed_args.append(arg)
|
|
808
|
-
omni_config_data["args"] = processed_args if processed_args else None
|
|
809
|
-
if env_dict:
|
|
810
|
-
omni_config_data["env"] = env_dict
|
|
811
|
-
if url is not None:
|
|
812
|
-
omni_config_data["url"] = url
|
|
813
|
-
if headers_dict:
|
|
814
|
-
omni_config_data["headers"] = headers_dict
|
|
815
|
-
|
|
816
|
-
# Host-specific fields (Gemini)
|
|
817
|
-
if timeout is not None:
|
|
818
|
-
omni_config_data["timeout"] = timeout
|
|
819
|
-
if trust:
|
|
820
|
-
omni_config_data["trust"] = trust
|
|
821
|
-
if cwd is not None:
|
|
822
|
-
omni_config_data["cwd"] = cwd
|
|
823
|
-
if http_url is not None:
|
|
824
|
-
omni_config_data["httpUrl"] = http_url
|
|
825
|
-
if include_tools is not None:
|
|
826
|
-
omni_config_data["includeTools"] = include_tools
|
|
827
|
-
if exclude_tools is not None:
|
|
828
|
-
omni_config_data["excludeTools"] = exclude_tools
|
|
829
|
-
|
|
830
|
-
# Host-specific fields (Cursor/VS Code/LM Studio)
|
|
831
|
-
if env_file is not None:
|
|
832
|
-
omni_config_data["envFile"] = env_file
|
|
833
|
-
|
|
834
|
-
# Host-specific fields (VS Code)
|
|
835
|
-
if inputs_list is not None:
|
|
836
|
-
omni_config_data["inputs"] = inputs_list
|
|
837
|
-
|
|
838
|
-
# Host-specific fields (Kiro)
|
|
839
|
-
if disabled is not None:
|
|
840
|
-
omni_config_data["disabled"] = disabled
|
|
841
|
-
if auto_approve_tools is not None:
|
|
842
|
-
omni_config_data["autoApprove"] = auto_approve_tools
|
|
843
|
-
if disable_tools is not None:
|
|
844
|
-
omni_config_data["disabledTools"] = disable_tools
|
|
845
|
-
|
|
846
|
-
# Host-specific fields (Codex)
|
|
847
|
-
if env_vars is not None:
|
|
848
|
-
omni_config_data["env_vars"] = env_vars
|
|
849
|
-
if startup_timeout is not None:
|
|
850
|
-
omni_config_data["startup_timeout_sec"] = startup_timeout
|
|
851
|
-
if tool_timeout is not None:
|
|
852
|
-
omni_config_data["tool_timeout_sec"] = tool_timeout
|
|
853
|
-
if enabled is not None:
|
|
854
|
-
omni_config_data["enabled"] = enabled
|
|
855
|
-
if bearer_token_env_var is not None:
|
|
856
|
-
omni_config_data["bearer_token_env_var"] = bearer_token_env_var
|
|
857
|
-
if env_header is not None:
|
|
858
|
-
# Parse KEY=ENV_VAR_NAME format into dict
|
|
859
|
-
env_http_headers = {}
|
|
860
|
-
for header_spec in env_header:
|
|
861
|
-
if '=' in header_spec:
|
|
862
|
-
key, env_var_name = header_spec.split('=', 1)
|
|
863
|
-
env_http_headers[key] = env_var_name
|
|
864
|
-
if env_http_headers:
|
|
865
|
-
omni_config_data["env_http_headers"] = env_http_headers
|
|
866
|
-
|
|
867
|
-
# Partial update merge logic
|
|
868
|
-
if is_update:
|
|
869
|
-
# Merge with existing configuration
|
|
870
|
-
existing_data = existing_config.model_dump(
|
|
871
|
-
exclude_unset=True, exclude={"name"}
|
|
872
|
-
)
|
|
873
|
-
|
|
874
|
-
# Handle command/URL/httpUrl switching behavior
|
|
875
|
-
# If switching from command to URL or httpUrl: clear command-based fields
|
|
876
|
-
if (
|
|
877
|
-
url is not None or http_url is not None
|
|
878
|
-
) and existing_config.command is not None:
|
|
879
|
-
existing_data.pop("command", None)
|
|
880
|
-
existing_data.pop("args", None)
|
|
881
|
-
existing_data.pop(
|
|
882
|
-
"type", None
|
|
883
|
-
) # Clear type field when switching transports (Issue 1)
|
|
884
|
-
|
|
885
|
-
# If switching from URL/httpUrl to command: clear URL-based fields
|
|
886
|
-
if command is not None and (
|
|
887
|
-
existing_config.url is not None
|
|
888
|
-
or getattr(existing_config, "httpUrl", None) is not None
|
|
889
|
-
):
|
|
890
|
-
existing_data.pop("url", None)
|
|
891
|
-
existing_data.pop("httpUrl", None)
|
|
892
|
-
existing_data.pop("headers", None)
|
|
893
|
-
existing_data.pop(
|
|
894
|
-
"type", None
|
|
895
|
-
) # Clear type field when switching transports (Issue 1)
|
|
896
|
-
|
|
897
|
-
# Merge: new values override existing values
|
|
898
|
-
merged_data = {**existing_data, **omni_config_data}
|
|
899
|
-
omni_config_data = merged_data
|
|
900
|
-
|
|
901
|
-
# Create Omni model
|
|
902
|
-
omni_config = MCPServerConfigOmni(**omni_config_data)
|
|
903
|
-
|
|
904
|
-
# Convert to host-specific model using HOST_MODEL_REGISTRY
|
|
905
|
-
host_model_class = HOST_MODEL_REGISTRY.get(host_type)
|
|
906
|
-
if not host_model_class:
|
|
907
|
-
print(f"Error: No model registered for host '{host}'")
|
|
908
|
-
return 1
|
|
909
|
-
|
|
910
|
-
# Convert Omni to host-specific model
|
|
911
|
-
server_config = host_model_class.from_omni(omni_config)
|
|
912
|
-
|
|
913
|
-
# Generate conversion report
|
|
914
|
-
report = generate_conversion_report(
|
|
915
|
-
operation="update" if is_update else "create",
|
|
916
|
-
server_name=server_name,
|
|
917
|
-
target_host=host_type,
|
|
918
|
-
omni=omni_config,
|
|
919
|
-
old_config=existing_config if is_update else None,
|
|
920
|
-
dry_run=dry_run,
|
|
921
|
-
)
|
|
922
|
-
|
|
923
|
-
# Display conversion report
|
|
924
|
-
if dry_run:
|
|
925
|
-
print(
|
|
926
|
-
f"[DRY RUN] Would configure MCP server '{server_name}' on host '{host}':"
|
|
927
|
-
)
|
|
928
|
-
print(f"[DRY RUN] Command: {command}")
|
|
929
|
-
if args:
|
|
930
|
-
print(f"[DRY RUN] Args: {args}")
|
|
931
|
-
if env_dict:
|
|
932
|
-
print(f"[DRY RUN] Environment: {env_dict}")
|
|
933
|
-
if url:
|
|
934
|
-
print(f"[DRY RUN] URL: {url}")
|
|
935
|
-
if headers_dict:
|
|
936
|
-
print(f"[DRY RUN] Headers: {headers_dict}")
|
|
937
|
-
print(f"[DRY RUN] Backup: {'Disabled' if no_backup else 'Enabled'}")
|
|
938
|
-
# Display report in dry-run mode
|
|
939
|
-
display_report(report)
|
|
940
|
-
return 0
|
|
941
|
-
|
|
942
|
-
# Display report before confirmation
|
|
943
|
-
display_report(report)
|
|
944
|
-
|
|
945
|
-
# Confirm operation unless auto-approved
|
|
946
|
-
if not request_confirmation(
|
|
947
|
-
f"Configure MCP server '{server_name}' on host '{host}'?", auto_approve
|
|
948
|
-
):
|
|
949
|
-
print("Operation cancelled.")
|
|
950
|
-
return 0
|
|
951
|
-
|
|
952
|
-
# Perform configuration
|
|
953
|
-
mcp_manager = MCPHostConfigurationManager()
|
|
954
|
-
result = mcp_manager.configure_server(
|
|
955
|
-
server_config=server_config, hostname=host, no_backup=no_backup
|
|
956
|
-
)
|
|
957
|
-
|
|
958
|
-
if result.success:
|
|
959
|
-
print(
|
|
960
|
-
f"[SUCCESS] Successfully configured MCP server '{server_name}' on host '{host}'"
|
|
961
|
-
)
|
|
962
|
-
if result.backup_path:
|
|
963
|
-
print(f" Backup created: {result.backup_path}")
|
|
964
|
-
return 0
|
|
965
|
-
else:
|
|
966
|
-
print(
|
|
967
|
-
f"[ERROR] Failed to configure MCP server '{server_name}' on host '{host}': {result.error_message}"
|
|
968
|
-
)
|
|
969
|
-
return 1
|
|
970
|
-
|
|
971
|
-
except Exception as e:
|
|
972
|
-
print(f"Error configuring MCP server: {e}")
|
|
973
|
-
return 1
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
def handle_mcp_remove(
|
|
977
|
-
host: str,
|
|
978
|
-
server_name: str,
|
|
979
|
-
no_backup: bool = False,
|
|
980
|
-
dry_run: bool = False,
|
|
981
|
-
auto_approve: bool = False,
|
|
982
|
-
):
|
|
983
|
-
"""Handle 'hatch mcp remove' command."""
|
|
984
|
-
try:
|
|
985
|
-
# Validate host type
|
|
986
|
-
try:
|
|
987
|
-
host_type = MCPHostType(host)
|
|
988
|
-
except ValueError:
|
|
989
|
-
print(
|
|
990
|
-
f"Error: Invalid host '{host}'. Supported hosts: {[h.value for h in MCPHostType]}"
|
|
991
|
-
)
|
|
992
|
-
return 1
|
|
993
|
-
|
|
994
|
-
if dry_run:
|
|
995
|
-
print(
|
|
996
|
-
f"[DRY RUN] Would remove MCP server '{server_name}' from host '{host}'"
|
|
997
|
-
)
|
|
998
|
-
print(f"[DRY RUN] Backup: {'Disabled' if no_backup else 'Enabled'}")
|
|
999
|
-
return 0
|
|
1000
|
-
|
|
1001
|
-
# Confirm operation unless auto-approved
|
|
1002
|
-
if not request_confirmation(
|
|
1003
|
-
f"Remove MCP server '{server_name}' from host '{host}'?", auto_approve
|
|
1004
|
-
):
|
|
1005
|
-
print("Operation cancelled.")
|
|
1006
|
-
return 0
|
|
1007
|
-
|
|
1008
|
-
# Perform removal
|
|
1009
|
-
mcp_manager = MCPHostConfigurationManager()
|
|
1010
|
-
result = mcp_manager.remove_server(
|
|
1011
|
-
server_name=server_name, hostname=host, no_backup=no_backup
|
|
1012
|
-
)
|
|
1013
|
-
|
|
1014
|
-
if result.success:
|
|
1015
|
-
print(
|
|
1016
|
-
f"[SUCCESS] Successfully removed MCP server '{server_name}' from host '{host}'"
|
|
1017
|
-
)
|
|
1018
|
-
if result.backup_path:
|
|
1019
|
-
print(f" Backup created: {result.backup_path}")
|
|
1020
|
-
return 0
|
|
1021
|
-
else:
|
|
1022
|
-
print(
|
|
1023
|
-
f"[ERROR] Failed to remove MCP server '{server_name}' from host '{host}': {result.error_message}"
|
|
1024
|
-
)
|
|
1025
|
-
return 1
|
|
1026
|
-
|
|
1027
|
-
except Exception as e:
|
|
1028
|
-
print(f"Error removing MCP server: {e}")
|
|
1029
|
-
return 1
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
def parse_host_list(host_arg: str) -> List[str]:
|
|
1033
|
-
"""Parse comma-separated host list or 'all'."""
|
|
1034
|
-
if not host_arg:
|
|
1035
|
-
return []
|
|
1036
|
-
|
|
1037
|
-
if host_arg.lower() == "all":
|
|
1038
|
-
from hatch.mcp_host_config.host_management import MCPHostRegistry
|
|
1039
|
-
|
|
1040
|
-
available_hosts = MCPHostRegistry.detect_available_hosts()
|
|
1041
|
-
return [host.value for host in available_hosts]
|
|
1042
|
-
|
|
1043
|
-
hosts = []
|
|
1044
|
-
for host_str in host_arg.split(","):
|
|
1045
|
-
host_str = host_str.strip()
|
|
1046
|
-
try:
|
|
1047
|
-
host_type = MCPHostType(host_str)
|
|
1048
|
-
hosts.append(host_type.value)
|
|
1049
|
-
except ValueError:
|
|
1050
|
-
available = [h.value for h in MCPHostType]
|
|
1051
|
-
raise ValueError(f"Unknown host '{host_str}'. Available: {available}")
|
|
1052
|
-
|
|
1053
|
-
return hosts
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
def handle_mcp_remove_server(
|
|
1057
|
-
env_manager: HatchEnvironmentManager,
|
|
1058
|
-
server_name: str,
|
|
1059
|
-
hosts: Optional[str] = None,
|
|
1060
|
-
env: Optional[str] = None,
|
|
1061
|
-
no_backup: bool = False,
|
|
1062
|
-
dry_run: bool = False,
|
|
1063
|
-
auto_approve: bool = False,
|
|
1064
|
-
):
|
|
1065
|
-
"""Handle 'hatch mcp remove server' command."""
|
|
1066
|
-
try:
|
|
1067
|
-
# Determine target hosts
|
|
1068
|
-
if hosts:
|
|
1069
|
-
target_hosts = parse_host_list(hosts)
|
|
1070
|
-
elif env:
|
|
1071
|
-
# TODO: Implement environment-based server removal
|
|
1072
|
-
print("Error: Environment-based removal not yet implemented")
|
|
1073
|
-
return 1
|
|
1074
|
-
else:
|
|
1075
|
-
print("Error: Must specify either --host or --env")
|
|
1076
|
-
return 1
|
|
1077
|
-
|
|
1078
|
-
if not target_hosts:
|
|
1079
|
-
print("Error: No valid hosts specified")
|
|
1080
|
-
return 1
|
|
1081
|
-
|
|
1082
|
-
if dry_run:
|
|
1083
|
-
print(
|
|
1084
|
-
f"[DRY RUN] Would remove MCP server '{server_name}' from hosts: {', '.join(target_hosts)}"
|
|
1085
|
-
)
|
|
1086
|
-
print(f"[DRY RUN] Backup: {'Disabled' if no_backup else 'Enabled'}")
|
|
1087
|
-
return 0
|
|
1088
|
-
|
|
1089
|
-
# Confirm operation unless auto-approved
|
|
1090
|
-
hosts_str = ", ".join(target_hosts)
|
|
1091
|
-
if not request_confirmation(
|
|
1092
|
-
f"Remove MCP server '{server_name}' from hosts: {hosts_str}?", auto_approve
|
|
1093
|
-
):
|
|
1094
|
-
print("Operation cancelled.")
|
|
1095
|
-
return 0
|
|
1096
|
-
|
|
1097
|
-
# Perform removal on each host
|
|
1098
|
-
mcp_manager = MCPHostConfigurationManager()
|
|
1099
|
-
success_count = 0
|
|
1100
|
-
total_count = len(target_hosts)
|
|
1101
|
-
|
|
1102
|
-
for host in target_hosts:
|
|
1103
|
-
result = mcp_manager.remove_server(
|
|
1104
|
-
server_name=server_name, hostname=host, no_backup=no_backup
|
|
1105
|
-
)
|
|
1106
|
-
|
|
1107
|
-
if result.success:
|
|
1108
|
-
print(f"[SUCCESS] Successfully removed '{server_name}' from '{host}'")
|
|
1109
|
-
if result.backup_path:
|
|
1110
|
-
print(f" Backup created: {result.backup_path}")
|
|
1111
|
-
success_count += 1
|
|
1112
|
-
|
|
1113
|
-
# Update environment tracking for current environment only
|
|
1114
|
-
current_env = env_manager.get_current_environment()
|
|
1115
|
-
if current_env:
|
|
1116
|
-
env_manager.remove_package_host_configuration(
|
|
1117
|
-
current_env, server_name, host
|
|
1118
|
-
)
|
|
1119
|
-
else:
|
|
1120
|
-
print(
|
|
1121
|
-
f"[ERROR] Failed to remove '{server_name}' from '{host}': {result.error_message}"
|
|
1122
|
-
)
|
|
1123
|
-
|
|
1124
|
-
# Summary
|
|
1125
|
-
if success_count == total_count:
|
|
1126
|
-
print(f"[SUCCESS] Removed '{server_name}' from all {total_count} hosts")
|
|
1127
|
-
return 0
|
|
1128
|
-
elif success_count > 0:
|
|
1129
|
-
print(
|
|
1130
|
-
f"[PARTIAL SUCCESS] Removed '{server_name}' from {success_count}/{total_count} hosts"
|
|
1131
|
-
)
|
|
1132
|
-
return 1
|
|
1133
|
-
else:
|
|
1134
|
-
print(f"[ERROR] Failed to remove '{server_name}' from any hosts")
|
|
1135
|
-
return 1
|
|
1136
|
-
|
|
1137
|
-
except Exception as e:
|
|
1138
|
-
print(f"Error removing MCP server: {e}")
|
|
1139
|
-
return 1
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
def handle_mcp_remove_host(
|
|
1143
|
-
env_manager: HatchEnvironmentManager,
|
|
1144
|
-
host_name: str,
|
|
1145
|
-
no_backup: bool = False,
|
|
1146
|
-
dry_run: bool = False,
|
|
1147
|
-
auto_approve: bool = False,
|
|
1148
|
-
):
|
|
1149
|
-
"""Handle 'hatch mcp remove host' command."""
|
|
1150
|
-
try:
|
|
1151
|
-
# Validate host type
|
|
1152
|
-
try:
|
|
1153
|
-
host_type = MCPHostType(host_name)
|
|
1154
|
-
except ValueError:
|
|
1155
|
-
print(
|
|
1156
|
-
f"Error: Invalid host '{host_name}'. Supported hosts: {[h.value for h in MCPHostType]}"
|
|
1157
|
-
)
|
|
1158
|
-
return 1
|
|
1159
|
-
|
|
1160
|
-
if dry_run:
|
|
1161
|
-
print(f"[DRY RUN] Would remove entire host configuration for '{host_name}'")
|
|
1162
|
-
print(f"[DRY RUN] Backup: {'Disabled' if no_backup else 'Enabled'}")
|
|
1163
|
-
return 0
|
|
1164
|
-
|
|
1165
|
-
# Confirm operation unless auto-approved
|
|
1166
|
-
if not request_confirmation(
|
|
1167
|
-
f"Remove entire host configuration for '{host_name}'? This will remove ALL MCP servers from this host.",
|
|
1168
|
-
auto_approve,
|
|
1169
|
-
):
|
|
1170
|
-
print("Operation cancelled.")
|
|
1171
|
-
return 0
|
|
1172
|
-
|
|
1173
|
-
# Perform host configuration removal
|
|
1174
|
-
mcp_manager = MCPHostConfigurationManager()
|
|
1175
|
-
result = mcp_manager.remove_host_configuration(
|
|
1176
|
-
hostname=host_name, no_backup=no_backup
|
|
1177
|
-
)
|
|
1178
|
-
|
|
1179
|
-
if result.success:
|
|
1180
|
-
print(
|
|
1181
|
-
f"[SUCCESS] Successfully removed host configuration for '{host_name}'"
|
|
1182
|
-
)
|
|
1183
|
-
if result.backup_path:
|
|
1184
|
-
print(f" Backup created: {result.backup_path}")
|
|
1185
|
-
|
|
1186
|
-
# Update environment tracking across all environments
|
|
1187
|
-
updates_count = env_manager.clear_host_from_all_packages_all_envs(host_name)
|
|
1188
|
-
if updates_count > 0:
|
|
1189
|
-
print(f"Updated {updates_count} package entries across environments")
|
|
1190
|
-
|
|
1191
|
-
return 0
|
|
1192
|
-
else:
|
|
1193
|
-
print(
|
|
1194
|
-
f"[ERROR] Failed to remove host configuration for '{host_name}': {result.error_message}"
|
|
1195
|
-
)
|
|
1196
|
-
return 1
|
|
1197
|
-
|
|
1198
|
-
except Exception as e:
|
|
1199
|
-
print(f"Error removing host configuration: {e}")
|
|
1200
|
-
return 1
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
def handle_mcp_sync(
|
|
1204
|
-
from_env: Optional[str] = None,
|
|
1205
|
-
from_host: Optional[str] = None,
|
|
1206
|
-
to_hosts: Optional[str] = None,
|
|
1207
|
-
servers: Optional[str] = None,
|
|
1208
|
-
pattern: Optional[str] = None,
|
|
1209
|
-
dry_run: bool = False,
|
|
1210
|
-
auto_approve: bool = False,
|
|
1211
|
-
no_backup: bool = False,
|
|
1212
|
-
) -> int:
|
|
1213
|
-
"""Handle 'hatch mcp sync' command."""
|
|
1214
|
-
try:
|
|
1215
|
-
# Parse target hosts
|
|
1216
|
-
if not to_hosts:
|
|
1217
|
-
print("Error: Must specify --to-host")
|
|
1218
|
-
return 1
|
|
1219
|
-
|
|
1220
|
-
target_hosts = parse_host_list(to_hosts)
|
|
1221
|
-
|
|
1222
|
-
# Parse server filters
|
|
1223
|
-
server_list = None
|
|
1224
|
-
if servers:
|
|
1225
|
-
server_list = [s.strip() for s in servers.split(",") if s.strip()]
|
|
1226
|
-
|
|
1227
|
-
if dry_run:
|
|
1228
|
-
source_desc = (
|
|
1229
|
-
f"environment '{from_env}'" if from_env else f"host '{from_host}'"
|
|
1230
|
-
)
|
|
1231
|
-
target_desc = f"hosts: {', '.join(target_hosts)}"
|
|
1232
|
-
print(f"[DRY RUN] Would synchronize from {source_desc} to {target_desc}")
|
|
1233
|
-
|
|
1234
|
-
if server_list:
|
|
1235
|
-
print(f"[DRY RUN] Server filter: {', '.join(server_list)}")
|
|
1236
|
-
elif pattern:
|
|
1237
|
-
print(f"[DRY RUN] Pattern filter: {pattern}")
|
|
1238
|
-
|
|
1239
|
-
print(f"[DRY RUN] Backup: {'Disabled' if no_backup else 'Enabled'}")
|
|
1240
|
-
return 0
|
|
1241
|
-
|
|
1242
|
-
# Confirm operation unless auto-approved
|
|
1243
|
-
source_desc = f"environment '{from_env}'" if from_env else f"host '{from_host}'"
|
|
1244
|
-
target_desc = f"{len(target_hosts)} host(s)"
|
|
1245
|
-
if not request_confirmation(
|
|
1246
|
-
f"Synchronize MCP configurations from {source_desc} to {target_desc}?",
|
|
1247
|
-
auto_approve,
|
|
1248
|
-
):
|
|
1249
|
-
print("Operation cancelled.")
|
|
1250
|
-
return 0
|
|
1251
|
-
|
|
1252
|
-
# Perform synchronization
|
|
1253
|
-
mcp_manager = MCPHostConfigurationManager()
|
|
1254
|
-
result = mcp_manager.sync_configurations(
|
|
1255
|
-
from_env=from_env,
|
|
1256
|
-
from_host=from_host,
|
|
1257
|
-
to_hosts=target_hosts,
|
|
1258
|
-
servers=server_list,
|
|
1259
|
-
pattern=pattern,
|
|
1260
|
-
no_backup=no_backup,
|
|
1261
|
-
)
|
|
1262
|
-
|
|
1263
|
-
if result.success:
|
|
1264
|
-
print(f"[SUCCESS] Synchronization completed")
|
|
1265
|
-
print(f" Servers synced: {result.servers_synced}")
|
|
1266
|
-
print(f" Hosts updated: {result.hosts_updated}")
|
|
1267
|
-
|
|
1268
|
-
# Show detailed results
|
|
1269
|
-
for res in result.results:
|
|
1270
|
-
if res.success:
|
|
1271
|
-
backup_info = (
|
|
1272
|
-
f" (backup: {res.backup_path})" if res.backup_path else ""
|
|
1273
|
-
)
|
|
1274
|
-
print(f" ✓ {res.hostname}{backup_info}")
|
|
1275
|
-
else:
|
|
1276
|
-
print(f" ✗ {res.hostname}: {res.error_message}")
|
|
1277
|
-
|
|
1278
|
-
return 0
|
|
1279
|
-
else:
|
|
1280
|
-
print(f"[ERROR] Synchronization failed")
|
|
1281
|
-
for res in result.results:
|
|
1282
|
-
if not res.success:
|
|
1283
|
-
print(f" ✗ {res.hostname}: {res.error_message}")
|
|
1284
|
-
return 1
|
|
1285
|
-
|
|
1286
|
-
except ValueError as e:
|
|
1287
|
-
print(f"Error: {e}")
|
|
1288
|
-
return 1
|
|
1289
|
-
except Exception as e:
|
|
1290
|
-
print(f"Error during synchronization: {e}")
|
|
1291
|
-
return 1
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
def main():
|
|
1295
|
-
"""Main entry point for Hatch CLI.
|
|
1296
|
-
|
|
1297
|
-
Parses command-line arguments and executes the requested commands for:
|
|
1298
|
-
- Package template creation
|
|
1299
|
-
- Package validation
|
|
1300
|
-
- Environment management (create, remove, list, use, current)
|
|
1301
|
-
- Package management (add, remove, list)
|
|
1302
|
-
|
|
1303
|
-
Returns:
|
|
1304
|
-
int: Exit code (0 for success, 1 for errors)
|
|
1305
|
-
"""
|
|
1306
|
-
# Configure logging
|
|
1307
|
-
logging.basicConfig(
|
|
1308
|
-
level=logging.INFO,
|
|
1309
|
-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
1310
|
-
)
|
|
1311
|
-
|
|
1312
|
-
# Create argument parser
|
|
1313
|
-
parser = argparse.ArgumentParser(description="Hatch package manager CLI")
|
|
1314
|
-
|
|
1315
|
-
# Add version argument
|
|
1316
|
-
parser.add_argument(
|
|
1317
|
-
"--version", action="version", version=f"%(prog)s {get_hatch_version()}"
|
|
1318
|
-
)
|
|
1319
|
-
|
|
1320
|
-
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
|
|
1321
|
-
|
|
1322
|
-
# Create template command
|
|
1323
|
-
create_parser = subparsers.add_parser(
|
|
1324
|
-
"create", help="Create a new package template"
|
|
1325
|
-
)
|
|
1326
|
-
create_parser.add_argument("name", help="Package name")
|
|
1327
|
-
create_parser.add_argument(
|
|
1328
|
-
"--dir", "-d", default=".", help="Target directory (default: current directory)"
|
|
1329
|
-
)
|
|
1330
|
-
create_parser.add_argument(
|
|
1331
|
-
"--description", "-D", default="", help="Package description"
|
|
1332
|
-
)
|
|
1333
|
-
|
|
1334
|
-
# Validate package command
|
|
1335
|
-
validate_parser = subparsers.add_parser("validate", help="Validate a package")
|
|
1336
|
-
validate_parser.add_argument("package_dir", help="Path to package directory")
|
|
1337
|
-
|
|
1338
|
-
# Environment management commands
|
|
1339
|
-
env_subparsers = subparsers.add_parser(
|
|
1340
|
-
"env", help="Environment management commands"
|
|
1341
|
-
).add_subparsers(dest="env_command", help="Environment command to execute")
|
|
1342
|
-
|
|
1343
|
-
# Create environment command
|
|
1344
|
-
env_create_parser = env_subparsers.add_parser(
|
|
1345
|
-
"create", help="Create a new environment"
|
|
1346
|
-
)
|
|
1347
|
-
env_create_parser.add_argument("name", help="Environment name")
|
|
1348
|
-
env_create_parser.add_argument(
|
|
1349
|
-
"--description", "-D", default="", help="Environment description"
|
|
1350
|
-
)
|
|
1351
|
-
env_create_parser.add_argument(
|
|
1352
|
-
"--python-version", help="Python version for the environment (e.g., 3.11, 3.12)"
|
|
1353
|
-
)
|
|
1354
|
-
env_create_parser.add_argument(
|
|
1355
|
-
"--no-python",
|
|
1356
|
-
action="store_true",
|
|
1357
|
-
help="Don't create a Python environment using conda/mamba",
|
|
1358
|
-
)
|
|
1359
|
-
env_create_parser.add_argument(
|
|
1360
|
-
"--no-hatch-mcp-server",
|
|
1361
|
-
action="store_true",
|
|
1362
|
-
help="Don't install hatch_mcp_server wrapper in the new environment",
|
|
1363
|
-
)
|
|
1364
|
-
env_create_parser.add_argument(
|
|
1365
|
-
"--hatch_mcp_server_tag",
|
|
1366
|
-
help="Git tag/branch reference for hatch_mcp_server wrapper installation (e.g., 'dev', 'v0.1.0')",
|
|
1367
|
-
)
|
|
1368
|
-
|
|
1369
|
-
# Remove environment command
|
|
1370
|
-
env_remove_parser = env_subparsers.add_parser(
|
|
1371
|
-
"remove", help="Remove an environment"
|
|
1372
|
-
)
|
|
1373
|
-
env_remove_parser.add_argument("name", help="Environment name")
|
|
1374
|
-
|
|
1375
|
-
# List environments command
|
|
1376
|
-
env_subparsers.add_parser("list", help="List all available environments")
|
|
1377
|
-
|
|
1378
|
-
# Set current environment command
|
|
1379
|
-
env_use_parser = env_subparsers.add_parser(
|
|
1380
|
-
"use", help="Set the current environment"
|
|
1381
|
-
)
|
|
1382
|
-
env_use_parser.add_argument("name", help="Environment name")
|
|
1383
|
-
|
|
1384
|
-
# Show current environment command
|
|
1385
|
-
env_subparsers.add_parser("current", help="Show the current environment")
|
|
1386
|
-
|
|
1387
|
-
# Python environment management commands - advanced subcommands
|
|
1388
|
-
env_python_subparsers = env_subparsers.add_parser(
|
|
1389
|
-
"python", help="Manage Python environments"
|
|
1390
|
-
).add_subparsers(
|
|
1391
|
-
dest="python_command", help="Python environment command to execute"
|
|
1392
|
-
)
|
|
1393
|
-
|
|
1394
|
-
# Initialize Python environment
|
|
1395
|
-
python_init_parser = env_python_subparsers.add_parser(
|
|
1396
|
-
"init", help="Initialize Python environment"
|
|
1397
|
-
)
|
|
1398
|
-
python_init_parser.add_argument(
|
|
1399
|
-
"--hatch_env",
|
|
1400
|
-
default=None,
|
|
1401
|
-
help="Hatch environment name in which the Python environment is located (default: current environment)",
|
|
1402
|
-
)
|
|
1403
|
-
python_init_parser.add_argument(
|
|
1404
|
-
"--python-version", help="Python version (e.g., 3.11, 3.12)"
|
|
1405
|
-
)
|
|
1406
|
-
python_init_parser.add_argument(
|
|
1407
|
-
"--force", action="store_true", help="Force recreation if exists"
|
|
1408
|
-
)
|
|
1409
|
-
python_init_parser.add_argument(
|
|
1410
|
-
"--no-hatch-mcp-server",
|
|
1411
|
-
action="store_true",
|
|
1412
|
-
help="Don't install hatch_mcp_server wrapper in the Python environment",
|
|
1413
|
-
)
|
|
1414
|
-
python_init_parser.add_argument(
|
|
1415
|
-
"--hatch_mcp_server_tag",
|
|
1416
|
-
help="Git tag/branch reference for hatch_mcp_server wrapper installation (e.g., 'dev', 'v0.1.0')",
|
|
1417
|
-
)
|
|
1418
|
-
|
|
1419
|
-
# Show Python environment info
|
|
1420
|
-
python_info_parser = env_python_subparsers.add_parser(
|
|
1421
|
-
"info", help="Show Python environment information"
|
|
1422
|
-
)
|
|
1423
|
-
python_info_parser.add_argument(
|
|
1424
|
-
"--hatch_env",
|
|
1425
|
-
default=None,
|
|
1426
|
-
help="Hatch environment name in which the Python environment is located (default: current environment)",
|
|
1427
|
-
)
|
|
1428
|
-
python_info_parser.add_argument(
|
|
1429
|
-
"--detailed", action="store_true", help="Show detailed diagnostics"
|
|
1430
|
-
)
|
|
1431
|
-
|
|
1432
|
-
# Hatch MCP server wrapper management commands
|
|
1433
|
-
hatch_mcp_parser = env_python_subparsers.add_parser(
|
|
1434
|
-
"add-hatch-mcp", help="Add hatch_mcp_server wrapper to the environment"
|
|
1435
|
-
)
|
|
1436
|
-
## Install MCP server command
|
|
1437
|
-
hatch_mcp_parser.add_argument(
|
|
1438
|
-
"--hatch_env",
|
|
1439
|
-
default=None,
|
|
1440
|
-
help="Hatch environment name. It must possess a valid Python environment. (default: current environment)",
|
|
1441
|
-
)
|
|
1442
|
-
hatch_mcp_parser.add_argument(
|
|
1443
|
-
"--tag",
|
|
1444
|
-
default=None,
|
|
1445
|
-
help="Git tag/branch reference for wrapper installation (e.g., 'dev', 'v0.1.0')",
|
|
1446
|
-
)
|
|
1447
|
-
|
|
1448
|
-
# Remove Python environment
|
|
1449
|
-
python_remove_parser = env_python_subparsers.add_parser(
|
|
1450
|
-
"remove", help="Remove Python environment"
|
|
1451
|
-
)
|
|
1452
|
-
python_remove_parser.add_argument(
|
|
1453
|
-
"--hatch_env",
|
|
1454
|
-
default=None,
|
|
1455
|
-
help="Hatch environment name in which the Python environment is located (default: current environment)",
|
|
1456
|
-
)
|
|
1457
|
-
python_remove_parser.add_argument(
|
|
1458
|
-
"--force", action="store_true", help="Force removal without confirmation"
|
|
1459
|
-
)
|
|
1460
|
-
|
|
1461
|
-
# Launch Python shell
|
|
1462
|
-
python_shell_parser = env_python_subparsers.add_parser(
|
|
1463
|
-
"shell", help="Launch Python shell in environment"
|
|
1464
|
-
)
|
|
1465
|
-
python_shell_parser.add_argument(
|
|
1466
|
-
"--hatch_env",
|
|
1467
|
-
default=None,
|
|
1468
|
-
help="Hatch environment name in which the Python environment is located (default: current environment)",
|
|
1469
|
-
)
|
|
1470
|
-
python_shell_parser.add_argument(
|
|
1471
|
-
"--cmd", help="Command to run in the shell (optional)"
|
|
1472
|
-
)
|
|
1473
|
-
|
|
1474
|
-
# MCP host configuration commands
|
|
1475
|
-
mcp_subparsers = subparsers.add_parser(
|
|
1476
|
-
"mcp", help="MCP host configuration commands"
|
|
1477
|
-
).add_subparsers(dest="mcp_command", help="MCP command to execute")
|
|
1478
|
-
|
|
1479
|
-
# MCP discovery commands
|
|
1480
|
-
mcp_discover_subparsers = mcp_subparsers.add_parser(
|
|
1481
|
-
"discover", help="Discover MCP hosts and servers"
|
|
1482
|
-
).add_subparsers(dest="discover_command", help="Discovery command to execute")
|
|
1483
|
-
|
|
1484
|
-
# Discover hosts command
|
|
1485
|
-
mcp_discover_hosts_parser = mcp_discover_subparsers.add_parser(
|
|
1486
|
-
"hosts", help="Discover available MCP host platforms"
|
|
1487
|
-
)
|
|
1488
|
-
|
|
1489
|
-
# Discover servers command
|
|
1490
|
-
mcp_discover_servers_parser = mcp_discover_subparsers.add_parser(
|
|
1491
|
-
"servers", help="Discover configured MCP servers"
|
|
1492
|
-
)
|
|
1493
|
-
mcp_discover_servers_parser.add_argument(
|
|
1494
|
-
"--env",
|
|
1495
|
-
"-e",
|
|
1496
|
-
default=None,
|
|
1497
|
-
help="Environment name (default: current environment)",
|
|
1498
|
-
)
|
|
1499
|
-
|
|
1500
|
-
# MCP list commands
|
|
1501
|
-
mcp_list_subparsers = mcp_subparsers.add_parser(
|
|
1502
|
-
"list", help="List MCP hosts and servers"
|
|
1503
|
-
).add_subparsers(dest="list_command", help="List command to execute")
|
|
1504
|
-
|
|
1505
|
-
# List hosts command
|
|
1506
|
-
mcp_list_hosts_parser = mcp_list_subparsers.add_parser(
|
|
1507
|
-
"hosts", help="List configured MCP hosts from environment"
|
|
1508
|
-
)
|
|
1509
|
-
mcp_list_hosts_parser.add_argument(
|
|
1510
|
-
"--env",
|
|
1511
|
-
"-e",
|
|
1512
|
-
default=None,
|
|
1513
|
-
help="Environment name (default: current environment)",
|
|
1514
|
-
)
|
|
1515
|
-
mcp_list_hosts_parser.add_argument(
|
|
1516
|
-
"--detailed",
|
|
1517
|
-
action="store_true",
|
|
1518
|
-
help="Show detailed host configuration information",
|
|
1519
|
-
)
|
|
1520
|
-
|
|
1521
|
-
# List servers command
|
|
1522
|
-
mcp_list_servers_parser = mcp_list_subparsers.add_parser(
|
|
1523
|
-
"servers", help="List configured MCP servers from environment"
|
|
1524
|
-
)
|
|
1525
|
-
mcp_list_servers_parser.add_argument(
|
|
1526
|
-
"--env",
|
|
1527
|
-
"-e",
|
|
1528
|
-
default=None,
|
|
1529
|
-
help="Environment name (default: current environment)",
|
|
1530
|
-
)
|
|
1531
|
-
|
|
1532
|
-
# MCP backup commands
|
|
1533
|
-
mcp_backup_subparsers = mcp_subparsers.add_parser(
|
|
1534
|
-
"backup", help="Backup management commands"
|
|
1535
|
-
).add_subparsers(dest="backup_command", help="Backup command to execute")
|
|
1536
|
-
|
|
1537
|
-
# Restore backup command
|
|
1538
|
-
mcp_backup_restore_parser = mcp_backup_subparsers.add_parser(
|
|
1539
|
-
"restore", help="Restore MCP host configuration from backup"
|
|
1540
|
-
)
|
|
1541
|
-
mcp_backup_restore_parser.add_argument(
|
|
1542
|
-
"host", help="Host platform to restore (e.g., claude-desktop, cursor)"
|
|
1543
|
-
)
|
|
1544
|
-
mcp_backup_restore_parser.add_argument(
|
|
1545
|
-
"--backup-file",
|
|
1546
|
-
"-f",
|
|
1547
|
-
default=None,
|
|
1548
|
-
help="Specific backup file to restore (default: latest)",
|
|
1549
|
-
)
|
|
1550
|
-
mcp_backup_restore_parser.add_argument(
|
|
1551
|
-
"--dry-run",
|
|
1552
|
-
action="store_true",
|
|
1553
|
-
help="Preview restore operation without execution",
|
|
1554
|
-
)
|
|
1555
|
-
mcp_backup_restore_parser.add_argument(
|
|
1556
|
-
"--auto-approve", action="store_true", help="Skip confirmation prompts"
|
|
1557
|
-
)
|
|
1558
|
-
|
|
1559
|
-
# List backups command
|
|
1560
|
-
mcp_backup_list_parser = mcp_backup_subparsers.add_parser(
|
|
1561
|
-
"list", help="List available backups for MCP host"
|
|
1562
|
-
)
|
|
1563
|
-
mcp_backup_list_parser.add_argument(
|
|
1564
|
-
"host", help="Host platform to list backups for (e.g., claude-desktop, cursor)"
|
|
1565
|
-
)
|
|
1566
|
-
mcp_backup_list_parser.add_argument(
|
|
1567
|
-
"--detailed", "-d", action="store_true", help="Show detailed backup information"
|
|
1568
|
-
)
|
|
1569
|
-
|
|
1570
|
-
# Clean backups command
|
|
1571
|
-
mcp_backup_clean_parser = mcp_backup_subparsers.add_parser(
|
|
1572
|
-
"clean", help="Clean old backups based on criteria"
|
|
1573
|
-
)
|
|
1574
|
-
mcp_backup_clean_parser.add_argument(
|
|
1575
|
-
"host", help="Host platform to clean backups for (e.g., claude-desktop, cursor)"
|
|
1576
|
-
)
|
|
1577
|
-
mcp_backup_clean_parser.add_argument(
|
|
1578
|
-
"--older-than-days", type=int, help="Remove backups older than specified days"
|
|
1579
|
-
)
|
|
1580
|
-
mcp_backup_clean_parser.add_argument(
|
|
1581
|
-
"--keep-count",
|
|
1582
|
-
type=int,
|
|
1583
|
-
help="Keep only the specified number of newest backups",
|
|
1584
|
-
)
|
|
1585
|
-
mcp_backup_clean_parser.add_argument(
|
|
1586
|
-
"--dry-run",
|
|
1587
|
-
action="store_true",
|
|
1588
|
-
help="Preview cleanup operation without execution",
|
|
1589
|
-
)
|
|
1590
|
-
mcp_backup_clean_parser.add_argument(
|
|
1591
|
-
"--auto-approve", action="store_true", help="Skip confirmation prompts"
|
|
1592
|
-
)
|
|
1593
|
-
|
|
1594
|
-
# MCP direct management commands
|
|
1595
|
-
mcp_configure_parser = mcp_subparsers.add_parser(
|
|
1596
|
-
"configure", help="Configure MCP server directly on host"
|
|
1597
|
-
)
|
|
1598
|
-
mcp_configure_parser.add_argument(
|
|
1599
|
-
"server_name", help="Name for the MCP server [hosts: all]"
|
|
1600
|
-
)
|
|
1601
|
-
mcp_configure_parser.add_argument(
|
|
1602
|
-
"--host",
|
|
1603
|
-
required=True,
|
|
1604
|
-
help="Host platform to configure (e.g., claude-desktop, cursor) [hosts: all]",
|
|
1605
|
-
)
|
|
1606
|
-
|
|
1607
|
-
# Create mutually exclusive group for server type
|
|
1608
|
-
server_type_group = mcp_configure_parser.add_mutually_exclusive_group()
|
|
1609
|
-
server_type_group.add_argument(
|
|
1610
|
-
"--command",
|
|
1611
|
-
dest="server_command",
|
|
1612
|
-
help="Command to execute the MCP server (for local servers) [hosts: all]",
|
|
1613
|
-
)
|
|
1614
|
-
server_type_group.add_argument(
|
|
1615
|
-
"--url", help="Server URL for remote MCP servers (SSE transport) [hosts: all except claude-desktop, claude-code]"
|
|
1616
|
-
)
|
|
1617
|
-
server_type_group.add_argument(
|
|
1618
|
-
"--http-url", help="HTTP streaming endpoint URL [hosts: gemini]"
|
|
1619
|
-
)
|
|
1620
|
-
|
|
1621
|
-
mcp_configure_parser.add_argument(
|
|
1622
|
-
"--args",
|
|
1623
|
-
nargs="*",
|
|
1624
|
-
help="Arguments for the MCP server command (only with --command) [hosts: all]",
|
|
1625
|
-
)
|
|
1626
|
-
mcp_configure_parser.add_argument(
|
|
1627
|
-
"--env-var",
|
|
1628
|
-
action="append",
|
|
1629
|
-
help="Environment variables (format: KEY=VALUE) [hosts: all]",
|
|
1630
|
-
)
|
|
1631
|
-
mcp_configure_parser.add_argument(
|
|
1632
|
-
"--header",
|
|
1633
|
-
action="append",
|
|
1634
|
-
help="HTTP headers for remote servers (format: KEY=VALUE, only with --url) [hosts: all except claude-desktop, claude-code]",
|
|
1635
|
-
)
|
|
1636
|
-
|
|
1637
|
-
# Host-specific arguments (Gemini)
|
|
1638
|
-
mcp_configure_parser.add_argument(
|
|
1639
|
-
"--timeout", type=int, help="Request timeout in milliseconds [hosts: gemini]"
|
|
1640
|
-
)
|
|
1641
|
-
mcp_configure_parser.add_argument(
|
|
1642
|
-
"--trust", action="store_true", help="Bypass tool call confirmations [hosts: gemini]"
|
|
1643
|
-
)
|
|
1644
|
-
mcp_configure_parser.add_argument(
|
|
1645
|
-
"--cwd", help="Working directory for stdio transport [hosts: gemini, codex]"
|
|
1646
|
-
)
|
|
1647
|
-
mcp_configure_parser.add_argument(
|
|
1648
|
-
"--include-tools",
|
|
1649
|
-
nargs="*",
|
|
1650
|
-
help="Tool allowlist / enabled tools [hosts: gemini, codex]",
|
|
1651
|
-
)
|
|
1652
|
-
mcp_configure_parser.add_argument(
|
|
1653
|
-
"--exclude-tools",
|
|
1654
|
-
nargs="*",
|
|
1655
|
-
help="Tool blocklist / disabled tools [hosts: gemini, codex]",
|
|
1656
|
-
)
|
|
1657
|
-
|
|
1658
|
-
# Host-specific arguments (Cursor/VS Code/LM Studio)
|
|
1659
|
-
mcp_configure_parser.add_argument(
|
|
1660
|
-
"--env-file", help="Path to environment file [hosts: cursor, vscode, lmstudio]"
|
|
1661
|
-
)
|
|
1662
|
-
|
|
1663
|
-
# Host-specific arguments (VS Code)
|
|
1664
|
-
mcp_configure_parser.add_argument(
|
|
1665
|
-
"--input",
|
|
1666
|
-
action="append",
|
|
1667
|
-
help="Input variable definitions in format: type,id,description[,password=true] [hosts: vscode]",
|
|
1668
|
-
)
|
|
1669
|
-
|
|
1670
|
-
# Host-specific arguments (Kiro)
|
|
1671
|
-
mcp_configure_parser.add_argument(
|
|
1672
|
-
"--disabled",
|
|
1673
|
-
action="store_true",
|
|
1674
|
-
default=None,
|
|
1675
|
-
help="Disable the MCP server [hosts: kiro]"
|
|
1676
|
-
)
|
|
1677
|
-
mcp_configure_parser.add_argument(
|
|
1678
|
-
"--auto-approve-tools",
|
|
1679
|
-
action="append",
|
|
1680
|
-
help="Tool names to auto-approve without prompting [hosts: kiro]"
|
|
1681
|
-
)
|
|
1682
|
-
mcp_configure_parser.add_argument(
|
|
1683
|
-
"--disable-tools",
|
|
1684
|
-
action="append",
|
|
1685
|
-
help="Tool names to disable [hosts: kiro]"
|
|
1686
|
-
)
|
|
1687
|
-
|
|
1688
|
-
# Codex-specific arguments
|
|
1689
|
-
mcp_configure_parser.add_argument(
|
|
1690
|
-
"--env-vars",
|
|
1691
|
-
action="append",
|
|
1692
|
-
help="Environment variable names to whitelist/forward [hosts: codex]"
|
|
1693
|
-
)
|
|
1694
|
-
mcp_configure_parser.add_argument(
|
|
1695
|
-
"--startup-timeout",
|
|
1696
|
-
type=int,
|
|
1697
|
-
help="Server startup timeout in seconds (default: 10) [hosts: codex]"
|
|
1698
|
-
)
|
|
1699
|
-
mcp_configure_parser.add_argument(
|
|
1700
|
-
"--tool-timeout",
|
|
1701
|
-
type=int,
|
|
1702
|
-
help="Tool execution timeout in seconds (default: 60) [hosts: codex]"
|
|
1703
|
-
)
|
|
1704
|
-
mcp_configure_parser.add_argument(
|
|
1705
|
-
"--enabled",
|
|
1706
|
-
action="store_true",
|
|
1707
|
-
default=None,
|
|
1708
|
-
help="Enable the MCP server [hosts: codex]"
|
|
1709
|
-
)
|
|
1710
|
-
mcp_configure_parser.add_argument(
|
|
1711
|
-
"--bearer-token-env-var",
|
|
1712
|
-
type=str,
|
|
1713
|
-
help="Name of environment variable containing bearer token for Authorization header [hosts: codex]"
|
|
1714
|
-
)
|
|
1715
|
-
mcp_configure_parser.add_argument(
|
|
1716
|
-
"--env-header",
|
|
1717
|
-
action="append",
|
|
1718
|
-
help="HTTP header from environment variable in KEY=ENV_VAR_NAME format [hosts: codex]"
|
|
1719
|
-
)
|
|
1720
|
-
|
|
1721
|
-
mcp_configure_parser.add_argument(
|
|
1722
|
-
"--no-backup",
|
|
1723
|
-
action="store_true",
|
|
1724
|
-
help="Skip backup creation before configuration [hosts: all]",
|
|
1725
|
-
)
|
|
1726
|
-
mcp_configure_parser.add_argument(
|
|
1727
|
-
"--dry-run", action="store_true", help="Preview configuration without execution [hosts: all]"
|
|
1728
|
-
)
|
|
1729
|
-
mcp_configure_parser.add_argument(
|
|
1730
|
-
"--auto-approve", action="store_true", help="Skip confirmation prompts [hosts: all]"
|
|
1731
|
-
)
|
|
1732
|
-
|
|
1733
|
-
# Remove MCP commands (object-action pattern)
|
|
1734
|
-
mcp_remove_subparsers = mcp_subparsers.add_parser(
|
|
1735
|
-
"remove", help="Remove MCP servers or host configurations"
|
|
1736
|
-
).add_subparsers(dest="remove_command", help="Remove command to execute")
|
|
1737
|
-
|
|
1738
|
-
# Remove server command
|
|
1739
|
-
mcp_remove_server_parser = mcp_remove_subparsers.add_parser(
|
|
1740
|
-
"server", help="Remove MCP server from hosts"
|
|
1741
|
-
)
|
|
1742
|
-
mcp_remove_server_parser.add_argument(
|
|
1743
|
-
"server_name", help="Name of the MCP server to remove"
|
|
1744
|
-
)
|
|
1745
|
-
mcp_remove_server_parser.add_argument(
|
|
1746
|
-
"--host", help="Target hosts (comma-separated or 'all')"
|
|
1747
|
-
)
|
|
1748
|
-
mcp_remove_server_parser.add_argument(
|
|
1749
|
-
"--env", "-e", help="Environment name (for environment-based removal)"
|
|
1750
|
-
)
|
|
1751
|
-
mcp_remove_server_parser.add_argument(
|
|
1752
|
-
"--no-backup", action="store_true", help="Skip backup creation before removal"
|
|
1753
|
-
)
|
|
1754
|
-
mcp_remove_server_parser.add_argument(
|
|
1755
|
-
"--dry-run", action="store_true", help="Preview removal without execution"
|
|
1756
|
-
)
|
|
1757
|
-
mcp_remove_server_parser.add_argument(
|
|
1758
|
-
"--auto-approve", action="store_true", help="Skip confirmation prompts"
|
|
1759
|
-
)
|
|
1760
|
-
|
|
1761
|
-
# Remove host command
|
|
1762
|
-
mcp_remove_host_parser = mcp_remove_subparsers.add_parser(
|
|
1763
|
-
"host", help="Remove entire host configuration"
|
|
1764
|
-
)
|
|
1765
|
-
mcp_remove_host_parser.add_argument(
|
|
1766
|
-
"host_name", help="Host platform to remove (e.g., claude-desktop, cursor)"
|
|
1767
|
-
)
|
|
1768
|
-
mcp_remove_host_parser.add_argument(
|
|
1769
|
-
"--no-backup", action="store_true", help="Skip backup creation before removal"
|
|
1770
|
-
)
|
|
1771
|
-
mcp_remove_host_parser.add_argument(
|
|
1772
|
-
"--dry-run", action="store_true", help="Preview removal without execution"
|
|
1773
|
-
)
|
|
1774
|
-
mcp_remove_host_parser.add_argument(
|
|
1775
|
-
"--auto-approve", action="store_true", help="Skip confirmation prompts"
|
|
1776
|
-
)
|
|
1777
|
-
|
|
1778
|
-
# MCP synchronization command
|
|
1779
|
-
mcp_sync_parser = mcp_subparsers.add_parser(
|
|
1780
|
-
"sync", help="Synchronize MCP configurations between environments and hosts"
|
|
1781
|
-
)
|
|
1782
|
-
|
|
1783
|
-
# Source options (mutually exclusive)
|
|
1784
|
-
sync_source_group = mcp_sync_parser.add_mutually_exclusive_group(required=True)
|
|
1785
|
-
sync_source_group.add_argument("--from-env", help="Source environment name")
|
|
1786
|
-
sync_source_group.add_argument("--from-host", help="Source host platform")
|
|
1787
|
-
|
|
1788
|
-
# Target options
|
|
1789
|
-
mcp_sync_parser.add_argument(
|
|
1790
|
-
"--to-host", required=True, help="Target hosts (comma-separated or 'all')"
|
|
1791
|
-
)
|
|
1792
|
-
|
|
1793
|
-
# Filter options (mutually exclusive)
|
|
1794
|
-
sync_filter_group = mcp_sync_parser.add_mutually_exclusive_group()
|
|
1795
|
-
sync_filter_group.add_argument(
|
|
1796
|
-
"--servers", help="Specific server names to sync (comma-separated)"
|
|
1797
|
-
)
|
|
1798
|
-
sync_filter_group.add_argument(
|
|
1799
|
-
"--pattern", help="Regex pattern for server selection"
|
|
1800
|
-
)
|
|
1801
|
-
|
|
1802
|
-
# Standard options
|
|
1803
|
-
mcp_sync_parser.add_argument(
|
|
1804
|
-
"--dry-run",
|
|
1805
|
-
action="store_true",
|
|
1806
|
-
help="Preview synchronization without execution",
|
|
1807
|
-
)
|
|
1808
|
-
mcp_sync_parser.add_argument(
|
|
1809
|
-
"--auto-approve", action="store_true", help="Skip confirmation prompts"
|
|
1810
|
-
)
|
|
1811
|
-
mcp_sync_parser.add_argument(
|
|
1812
|
-
"--no-backup",
|
|
1813
|
-
action="store_true",
|
|
1814
|
-
help="Skip backup creation before synchronization",
|
|
1815
|
-
)
|
|
1816
|
-
|
|
1817
|
-
# Package management commands
|
|
1818
|
-
pkg_subparsers = subparsers.add_parser(
|
|
1819
|
-
"package", help="Package management commands"
|
|
1820
|
-
).add_subparsers(dest="pkg_command", help="Package command to execute")
|
|
1821
|
-
|
|
1822
|
-
# Add package command
|
|
1823
|
-
pkg_add_parser = pkg_subparsers.add_parser(
|
|
1824
|
-
"add", help="Add a package to the current environment"
|
|
1825
|
-
)
|
|
1826
|
-
pkg_add_parser.add_argument(
|
|
1827
|
-
"package_path_or_name", help="Path to package directory or name of the package"
|
|
1828
|
-
)
|
|
1829
|
-
pkg_add_parser.add_argument(
|
|
1830
|
-
"--env",
|
|
1831
|
-
"-e",
|
|
1832
|
-
default=None,
|
|
1833
|
-
help="Environment name (default: current environment)",
|
|
1834
|
-
)
|
|
1835
|
-
pkg_add_parser.add_argument(
|
|
1836
|
-
"--version", "-v", default=None, help="Version of the package (optional)"
|
|
1837
|
-
)
|
|
1838
|
-
pkg_add_parser.add_argument(
|
|
1839
|
-
"--force-download",
|
|
1840
|
-
"-f",
|
|
1841
|
-
action="store_true",
|
|
1842
|
-
help="Force download even if package is in cache",
|
|
1843
|
-
)
|
|
1844
|
-
pkg_add_parser.add_argument(
|
|
1845
|
-
"--refresh-registry",
|
|
1846
|
-
"-r",
|
|
1847
|
-
action="store_true",
|
|
1848
|
-
help="Force refresh of registry data",
|
|
1849
|
-
)
|
|
1850
|
-
pkg_add_parser.add_argument(
|
|
1851
|
-
"--auto-approve",
|
|
1852
|
-
action="store_true",
|
|
1853
|
-
help="Automatically approve changes installation of deps for automation scenario",
|
|
1854
|
-
)
|
|
1855
|
-
# MCP host configuration integration
|
|
1856
|
-
pkg_add_parser.add_argument(
|
|
1857
|
-
"--host",
|
|
1858
|
-
help="Comma-separated list of MCP host platforms to configure (e.g., claude-desktop,cursor)",
|
|
1859
|
-
)
|
|
1860
|
-
|
|
1861
|
-
# Remove package command
|
|
1862
|
-
pkg_remove_parser = pkg_subparsers.add_parser(
|
|
1863
|
-
"remove", help="Remove a package from the current environment"
|
|
1864
|
-
)
|
|
1865
|
-
pkg_remove_parser.add_argument("package_name", help="Name of the package to remove")
|
|
1866
|
-
pkg_remove_parser.add_argument(
|
|
1867
|
-
"--env",
|
|
1868
|
-
"-e",
|
|
1869
|
-
default=None,
|
|
1870
|
-
help="Environment name (default: current environment)",
|
|
1871
|
-
)
|
|
1872
|
-
|
|
1873
|
-
# List packages command
|
|
1874
|
-
pkg_list_parser = pkg_subparsers.add_parser(
|
|
1875
|
-
"list", help="List packages in an environment"
|
|
1876
|
-
)
|
|
1877
|
-
pkg_list_parser.add_argument(
|
|
1878
|
-
"--env", "-e", help="Environment name (default: current environment)"
|
|
1879
|
-
)
|
|
1880
|
-
|
|
1881
|
-
# Sync package MCP servers command
|
|
1882
|
-
pkg_sync_parser = pkg_subparsers.add_parser(
|
|
1883
|
-
"sync", help="Synchronize package MCP servers to host platforms"
|
|
1884
|
-
)
|
|
1885
|
-
pkg_sync_parser.add_argument(
|
|
1886
|
-
"package_name", help="Name of the package whose MCP servers to sync"
|
|
1887
|
-
)
|
|
1888
|
-
pkg_sync_parser.add_argument(
|
|
1889
|
-
"--host",
|
|
1890
|
-
required=True,
|
|
1891
|
-
help="Comma-separated list of host platforms to sync to (or 'all')",
|
|
1892
|
-
)
|
|
1893
|
-
pkg_sync_parser.add_argument(
|
|
1894
|
-
"--env",
|
|
1895
|
-
"-e",
|
|
1896
|
-
default=None,
|
|
1897
|
-
help="Environment name (default: current environment)",
|
|
1898
|
-
)
|
|
1899
|
-
pkg_sync_parser.add_argument(
|
|
1900
|
-
"--dry-run", action="store_true", help="Preview changes without execution"
|
|
1901
|
-
)
|
|
1902
|
-
pkg_sync_parser.add_argument(
|
|
1903
|
-
"--auto-approve", action="store_true", help="Skip confirmation prompts"
|
|
1904
|
-
)
|
|
1905
|
-
pkg_sync_parser.add_argument(
|
|
1906
|
-
"--no-backup", action="store_true", help="Disable default backup behavior"
|
|
1907
|
-
)
|
|
1908
|
-
|
|
1909
|
-
# General arguments for the environment manager
|
|
1910
|
-
parser.add_argument(
|
|
1911
|
-
"--envs-dir",
|
|
1912
|
-
default=Path.home() / ".hatch" / "envs",
|
|
1913
|
-
help="Directory to store environments",
|
|
1914
|
-
)
|
|
1915
|
-
parser.add_argument(
|
|
1916
|
-
"--cache-ttl",
|
|
1917
|
-
type=int,
|
|
1918
|
-
default=86400,
|
|
1919
|
-
help="Cache TTL in seconds (default: 86400 seconds --> 1 day)",
|
|
1920
|
-
)
|
|
1921
|
-
parser.add_argument(
|
|
1922
|
-
"--cache-dir",
|
|
1923
|
-
default=Path.home() / ".hatch" / "cache",
|
|
1924
|
-
help="Directory to store cached packages",
|
|
1925
|
-
)
|
|
1926
|
-
|
|
1927
|
-
args = parser.parse_args()
|
|
1928
|
-
|
|
1929
|
-
# Initialize environment manager
|
|
1930
|
-
env_manager = HatchEnvironmentManager(
|
|
1931
|
-
environments_dir=args.envs_dir,
|
|
1932
|
-
cache_ttl=args.cache_ttl,
|
|
1933
|
-
cache_dir=args.cache_dir,
|
|
1934
|
-
)
|
|
1935
|
-
|
|
1936
|
-
# Initialize MCP configuration manager
|
|
1937
|
-
mcp_manager = MCPHostConfigurationManager()
|
|
1938
|
-
|
|
1939
|
-
# Execute commands
|
|
1940
|
-
if args.command == "create":
|
|
1941
|
-
target_dir = Path(args.dir).resolve()
|
|
1942
|
-
package_dir = create_package_template(
|
|
1943
|
-
target_dir=target_dir, package_name=args.name, description=args.description
|
|
1944
|
-
)
|
|
1945
|
-
print(f"Package template created at: {package_dir}")
|
|
1946
|
-
|
|
1947
|
-
elif args.command == "validate":
|
|
1948
|
-
package_path = Path(args.package_dir).resolve()
|
|
1949
|
-
|
|
1950
|
-
# Create validator with registry data from environment manager
|
|
1951
|
-
validator = HatchPackageValidator(
|
|
1952
|
-
version="latest",
|
|
1953
|
-
allow_local_dependencies=True,
|
|
1954
|
-
registry_data=env_manager.registry_data,
|
|
1955
|
-
)
|
|
1956
|
-
|
|
1957
|
-
# Validate the package
|
|
1958
|
-
is_valid, validation_results = validator.validate_package(package_path)
|
|
1959
|
-
|
|
1960
|
-
if is_valid:
|
|
1961
|
-
print(f"Package validation SUCCESSFUL: {package_path}")
|
|
1962
|
-
return 0
|
|
1963
|
-
else:
|
|
1964
|
-
print(f"Package validation FAILED: {package_path}")
|
|
1965
|
-
|
|
1966
|
-
# Print detailed validation results if available
|
|
1967
|
-
if validation_results and isinstance(validation_results, dict):
|
|
1968
|
-
for category, result in validation_results.items():
|
|
1969
|
-
if (
|
|
1970
|
-
category != "valid"
|
|
1971
|
-
and category != "metadata"
|
|
1972
|
-
and isinstance(result, dict)
|
|
1973
|
-
):
|
|
1974
|
-
if not result.get("valid", True) and result.get("errors"):
|
|
1975
|
-
print(f"\n{category.replace('_', ' ').title()} errors:")
|
|
1976
|
-
for error in result["errors"]:
|
|
1977
|
-
print(f" - {error}")
|
|
1978
|
-
|
|
1979
|
-
return 1
|
|
1980
|
-
|
|
1981
|
-
elif args.command == "env":
|
|
1982
|
-
if args.env_command == "create":
|
|
1983
|
-
# Determine whether to create Python environment
|
|
1984
|
-
create_python_env = not args.no_python
|
|
1985
|
-
python_version = getattr(args, "python_version", None)
|
|
1986
|
-
|
|
1987
|
-
if env_manager.create_environment(
|
|
1988
|
-
args.name,
|
|
1989
|
-
args.description,
|
|
1990
|
-
python_version=python_version,
|
|
1991
|
-
create_python_env=create_python_env,
|
|
1992
|
-
no_hatch_mcp_server=args.no_hatch_mcp_server,
|
|
1993
|
-
hatch_mcp_server_tag=args.hatch_mcp_server_tag,
|
|
1994
|
-
):
|
|
1995
|
-
print(f"Environment created: {args.name}")
|
|
1996
|
-
|
|
1997
|
-
# Show Python environment status
|
|
1998
|
-
if create_python_env and env_manager.is_python_environment_available():
|
|
1999
|
-
python_exec = env_manager.python_env_manager.get_python_executable(
|
|
2000
|
-
args.name
|
|
2001
|
-
)
|
|
2002
|
-
if python_exec:
|
|
2003
|
-
python_version_info = (
|
|
2004
|
-
env_manager.python_env_manager.get_python_version(args.name)
|
|
2005
|
-
)
|
|
2006
|
-
print(f"Python environment: {python_exec}")
|
|
2007
|
-
if python_version_info:
|
|
2008
|
-
print(f"Python version: {python_version_info}")
|
|
2009
|
-
else:
|
|
2010
|
-
print("Python environment creation failed")
|
|
2011
|
-
elif create_python_env:
|
|
2012
|
-
print("Python environment requested but conda/mamba not available")
|
|
2013
|
-
|
|
2014
|
-
return 0
|
|
2015
|
-
else:
|
|
2016
|
-
print(f"Failed to create environment: {args.name}")
|
|
2017
|
-
return 1
|
|
2018
|
-
|
|
2019
|
-
elif args.env_command == "remove":
|
|
2020
|
-
if env_manager.remove_environment(args.name):
|
|
2021
|
-
print(f"Environment removed: {args.name}")
|
|
2022
|
-
return 0
|
|
2023
|
-
else:
|
|
2024
|
-
print(f"Failed to remove environment: {args.name}")
|
|
2025
|
-
return 1
|
|
2026
|
-
|
|
2027
|
-
elif args.env_command == "list":
|
|
2028
|
-
environments = env_manager.list_environments()
|
|
2029
|
-
print("Available environments:")
|
|
2030
|
-
|
|
2031
|
-
# Check if conda/mamba is available for status info
|
|
2032
|
-
conda_available = env_manager.is_python_environment_available()
|
|
2033
|
-
|
|
2034
|
-
for env in environments:
|
|
2035
|
-
current_marker = "* " if env.get("is_current") else " "
|
|
2036
|
-
description = (
|
|
2037
|
-
f" - {env.get('description')}" if env.get("description") else ""
|
|
2038
|
-
)
|
|
2039
|
-
|
|
2040
|
-
# Show basic environment info
|
|
2041
|
-
print(f"{current_marker}{env.get('name')}{description}")
|
|
2042
|
-
|
|
2043
|
-
# Show Python environment info if available
|
|
2044
|
-
python_env = env.get("python_environment", False)
|
|
2045
|
-
if python_env:
|
|
2046
|
-
python_info = env_manager.get_python_environment_info(
|
|
2047
|
-
env.get("name")
|
|
2048
|
-
)
|
|
2049
|
-
if python_info:
|
|
2050
|
-
python_version = python_info.get("python_version", "Unknown")
|
|
2051
|
-
conda_env = python_info.get("conda_env_name", "N/A")
|
|
2052
|
-
print(f" Python: {python_version} (conda: {conda_env})")
|
|
2053
|
-
else:
|
|
2054
|
-
print(f" Python: Configured but unavailable")
|
|
2055
|
-
elif conda_available:
|
|
2056
|
-
print(f" Python: Not configured")
|
|
2057
|
-
else:
|
|
2058
|
-
print(f" Python: Conda/mamba not available")
|
|
2059
|
-
|
|
2060
|
-
# Show conda/mamba status
|
|
2061
|
-
if conda_available:
|
|
2062
|
-
manager_info = env_manager.python_env_manager.get_manager_info()
|
|
2063
|
-
print(f"\nPython Environment Manager:")
|
|
2064
|
-
print(
|
|
2065
|
-
f" Conda executable: {manager_info.get('conda_executable', 'Not found')}"
|
|
2066
|
-
)
|
|
2067
|
-
print(
|
|
2068
|
-
f" Mamba executable: {manager_info.get('mamba_executable', 'Not found')}"
|
|
2069
|
-
)
|
|
2070
|
-
print(
|
|
2071
|
-
f" Preferred manager: {manager_info.get('preferred_manager', 'N/A')}"
|
|
2072
|
-
)
|
|
2073
|
-
else:
|
|
2074
|
-
print(f"\nPython Environment Manager: Conda/mamba not available")
|
|
2075
|
-
|
|
2076
|
-
return 0
|
|
2077
|
-
|
|
2078
|
-
elif args.env_command == "use":
|
|
2079
|
-
if env_manager.set_current_environment(args.name):
|
|
2080
|
-
print(f"Current environment set to: {args.name}")
|
|
2081
|
-
return 0
|
|
2082
|
-
else:
|
|
2083
|
-
print(f"Failed to set environment: {args.name}")
|
|
2084
|
-
return 1
|
|
2085
|
-
|
|
2086
|
-
elif args.env_command == "current":
|
|
2087
|
-
current_env = env_manager.get_current_environment()
|
|
2088
|
-
print(f"Current environment: {current_env}")
|
|
2089
|
-
return 0
|
|
2090
|
-
|
|
2091
|
-
elif args.env_command == "python":
|
|
2092
|
-
# Advanced Python environment management
|
|
2093
|
-
if args.python_command == "init":
|
|
2094
|
-
python_version = getattr(args, "python_version", None)
|
|
2095
|
-
force = getattr(args, "force", False)
|
|
2096
|
-
no_hatch_mcp_server = getattr(args, "no_hatch_mcp_server", False)
|
|
2097
|
-
hatch_mcp_server_tag = getattr(args, "hatch_mcp_server_tag", None)
|
|
2098
|
-
|
|
2099
|
-
if env_manager.create_python_environment_only(
|
|
2100
|
-
args.hatch_env,
|
|
2101
|
-
python_version,
|
|
2102
|
-
force,
|
|
2103
|
-
no_hatch_mcp_server=no_hatch_mcp_server,
|
|
2104
|
-
hatch_mcp_server_tag=hatch_mcp_server_tag,
|
|
2105
|
-
):
|
|
2106
|
-
print(f"Python environment initialized for: {args.hatch_env}")
|
|
2107
|
-
|
|
2108
|
-
# Show Python environment info
|
|
2109
|
-
python_info = env_manager.get_python_environment_info(
|
|
2110
|
-
args.hatch_env
|
|
2111
|
-
)
|
|
2112
|
-
if python_info:
|
|
2113
|
-
print(
|
|
2114
|
-
f" Python executable: {python_info['python_executable']}"
|
|
2115
|
-
)
|
|
2116
|
-
print(
|
|
2117
|
-
f" Python version: {python_info.get('python_version', 'Unknown')}"
|
|
2118
|
-
)
|
|
2119
|
-
print(
|
|
2120
|
-
f" Conda environment: {python_info.get('conda_env_name', 'N/A')}"
|
|
2121
|
-
)
|
|
2122
|
-
|
|
2123
|
-
return 0
|
|
2124
|
-
else:
|
|
2125
|
-
env_name = args.hatch_env or env_manager.get_current_environment()
|
|
2126
|
-
print(f"Failed to initialize Python environment for: {env_name}")
|
|
2127
|
-
return 1
|
|
2128
|
-
|
|
2129
|
-
elif args.python_command == "info":
|
|
2130
|
-
detailed = getattr(args, "detailed", False)
|
|
2131
|
-
|
|
2132
|
-
python_info = env_manager.get_python_environment_info(args.hatch_env)
|
|
2133
|
-
|
|
2134
|
-
if python_info:
|
|
2135
|
-
env_name = args.hatch_env or env_manager.get_current_environment()
|
|
2136
|
-
print(f"Python environment info for '{env_name}':")
|
|
2137
|
-
print(
|
|
2138
|
-
f" Status: {'Active' if python_info.get('enabled', False) else 'Inactive'}"
|
|
2139
|
-
)
|
|
2140
|
-
print(f" Python executable: {python_info['python_executable']}")
|
|
2141
|
-
print(
|
|
2142
|
-
f" Python version: {python_info.get('python_version', 'Unknown')}"
|
|
2143
|
-
)
|
|
2144
|
-
print(
|
|
2145
|
-
f" Conda environment: {python_info.get('conda_env_name', 'N/A')}"
|
|
2146
|
-
)
|
|
2147
|
-
print(f" Environment path: {python_info['environment_path']}")
|
|
2148
|
-
print(f" Created: {python_info.get('created_at', 'Unknown')}")
|
|
2149
|
-
print(f" Package count: {python_info.get('package_count', 0)}")
|
|
2150
|
-
print(f" Packages:")
|
|
2151
|
-
for pkg in python_info.get("packages", []):
|
|
2152
|
-
print(f" - {pkg['name']} ({pkg['version']})")
|
|
2153
|
-
|
|
2154
|
-
if detailed:
|
|
2155
|
-
print(f"\nDiagnostics:")
|
|
2156
|
-
diagnostics = env_manager.get_python_environment_diagnostics(
|
|
2157
|
-
args.hatch_env
|
|
2158
|
-
)
|
|
2159
|
-
if diagnostics:
|
|
2160
|
-
for key, value in diagnostics.items():
|
|
2161
|
-
print(f" {key}: {value}")
|
|
2162
|
-
else:
|
|
2163
|
-
print(" No diagnostics available")
|
|
2164
|
-
|
|
2165
|
-
return 0
|
|
2166
|
-
else:
|
|
2167
|
-
env_name = args.hatch_env or env_manager.get_current_environment()
|
|
2168
|
-
print(f"No Python environment found for: {env_name}")
|
|
2169
|
-
|
|
2170
|
-
# Show diagnostics for missing environment
|
|
2171
|
-
if detailed:
|
|
2172
|
-
print("\nDiagnostics:")
|
|
2173
|
-
general_diagnostics = (
|
|
2174
|
-
env_manager.get_python_manager_diagnostics()
|
|
2175
|
-
)
|
|
2176
|
-
for key, value in general_diagnostics.items():
|
|
2177
|
-
print(f" {key}: {value}")
|
|
2178
|
-
|
|
2179
|
-
return 1
|
|
2180
|
-
|
|
2181
|
-
elif args.python_command == "remove":
|
|
2182
|
-
force = getattr(args, "force", False)
|
|
2183
|
-
|
|
2184
|
-
if not force:
|
|
2185
|
-
# Ask for confirmation using TTY-aware function
|
|
2186
|
-
env_name = args.hatch_env or env_manager.get_current_environment()
|
|
2187
|
-
if not request_confirmation(
|
|
2188
|
-
f"Remove Python environment for '{env_name}'?"
|
|
2189
|
-
):
|
|
2190
|
-
print("Operation cancelled")
|
|
2191
|
-
return 0
|
|
2192
|
-
|
|
2193
|
-
if env_manager.remove_python_environment_only(args.hatch_env):
|
|
2194
|
-
env_name = args.hatch_env or env_manager.get_current_environment()
|
|
2195
|
-
print(f"Python environment removed from: {env_name}")
|
|
2196
|
-
return 0
|
|
2197
|
-
else:
|
|
2198
|
-
env_name = args.hatch_env or env_manager.get_current_environment()
|
|
2199
|
-
print(f"Failed to remove Python environment from: {env_name}")
|
|
2200
|
-
return 1
|
|
2201
|
-
|
|
2202
|
-
elif args.python_command == "shell":
|
|
2203
|
-
cmd = getattr(args, "cmd", None)
|
|
2204
|
-
|
|
2205
|
-
if env_manager.launch_python_shell(args.hatch_env, cmd):
|
|
2206
|
-
return 0
|
|
2207
|
-
else:
|
|
2208
|
-
env_name = args.hatch_env or env_manager.get_current_environment()
|
|
2209
|
-
print(f"Failed to launch Python shell for: {env_name}")
|
|
2210
|
-
return 1
|
|
2211
|
-
|
|
2212
|
-
elif args.python_command == "add-hatch-mcp":
|
|
2213
|
-
env_name = args.hatch_env or env_manager.get_current_environment()
|
|
2214
|
-
tag = args.tag
|
|
2215
|
-
|
|
2216
|
-
if env_manager.install_mcp_server(env_name, tag):
|
|
2217
|
-
print(
|
|
2218
|
-
f"hatch_mcp_server wrapper installed successfully in environment: {env_name}"
|
|
2219
|
-
)
|
|
2220
|
-
return 0
|
|
2221
|
-
else:
|
|
2222
|
-
print(
|
|
2223
|
-
f"Failed to install hatch_mcp_server wrapper in environment: {env_name}"
|
|
2224
|
-
)
|
|
2225
|
-
return 1
|
|
2226
|
-
|
|
2227
|
-
else:
|
|
2228
|
-
print("Unknown Python environment command")
|
|
2229
|
-
return 1
|
|
2230
|
-
|
|
2231
|
-
elif args.command == "package":
|
|
2232
|
-
if args.pkg_command == "add":
|
|
2233
|
-
# Add package to environment
|
|
2234
|
-
if env_manager.add_package_to_environment(
|
|
2235
|
-
args.package_path_or_name,
|
|
2236
|
-
args.env,
|
|
2237
|
-
args.version,
|
|
2238
|
-
args.force_download,
|
|
2239
|
-
args.refresh_registry,
|
|
2240
|
-
args.auto_approve,
|
|
2241
|
-
):
|
|
2242
|
-
print(f"Successfully added package: {args.package_path_or_name}")
|
|
2243
|
-
|
|
2244
|
-
# Handle MCP host configuration if requested
|
|
2245
|
-
if hasattr(args, "host") and args.host:
|
|
2246
|
-
try:
|
|
2247
|
-
hosts = parse_host_list(args.host)
|
|
2248
|
-
env_name = args.env or env_manager.get_current_environment()
|
|
2249
|
-
|
|
2250
|
-
package_name = args.package_path_or_name
|
|
2251
|
-
package_service = None
|
|
2252
|
-
|
|
2253
|
-
# Check if it's a local package path
|
|
2254
|
-
pkg_path = Path(args.package_path_or_name)
|
|
2255
|
-
if pkg_path.exists() and pkg_path.is_dir():
|
|
2256
|
-
# Local package - load metadata from directory
|
|
2257
|
-
with open(pkg_path / "hatch_metadata.json", "r") as f:
|
|
2258
|
-
metadata = json.load(f)
|
|
2259
|
-
package_service = PackageService(metadata)
|
|
2260
|
-
package_name = package_service.get_field("name")
|
|
2261
|
-
else:
|
|
2262
|
-
# Registry package - get metadata from environment manager
|
|
2263
|
-
try:
|
|
2264
|
-
env_data = env_manager.get_environment_data(env_name)
|
|
2265
|
-
if env_data:
|
|
2266
|
-
# Find the package in the environment
|
|
2267
|
-
for pkg in env_data.packages:
|
|
2268
|
-
if pkg.name == package_name:
|
|
2269
|
-
# Create a minimal metadata structure for PackageService
|
|
2270
|
-
metadata = {
|
|
2271
|
-
"name": pkg.name,
|
|
2272
|
-
"version": pkg.version,
|
|
2273
|
-
"dependencies": {}, # Will be populated if needed
|
|
2274
|
-
}
|
|
2275
|
-
package_service = PackageService(metadata)
|
|
2276
|
-
break
|
|
2277
|
-
|
|
2278
|
-
if package_service is None:
|
|
2279
|
-
print(
|
|
2280
|
-
f"Warning: Could not find package '{package_name}' in environment '{env_name}'. Skipping dependency analysis."
|
|
2281
|
-
)
|
|
2282
|
-
package_service = None
|
|
2283
|
-
except Exception as e:
|
|
2284
|
-
print(
|
|
2285
|
-
f"Warning: Could not load package metadata for '{package_name}': {e}. Skipping dependency analysis."
|
|
2286
|
-
)
|
|
2287
|
-
package_service = None
|
|
2288
|
-
|
|
2289
|
-
# Get dependency names if we have package service
|
|
2290
|
-
package_names = []
|
|
2291
|
-
if package_service:
|
|
2292
|
-
# Get Hatch dependencies
|
|
2293
|
-
dependencies = package_service.get_dependencies()
|
|
2294
|
-
hatch_deps = dependencies.get("hatch", [])
|
|
2295
|
-
package_names = [
|
|
2296
|
-
dep.get("name") for dep in hatch_deps if dep.get("name")
|
|
2297
|
-
]
|
|
2298
|
-
|
|
2299
|
-
# Resolve local dependency paths to actual names
|
|
2300
|
-
for i in range(len(package_names)):
|
|
2301
|
-
dep_path = Path(package_names[i])
|
|
2302
|
-
if dep_path.exists() and dep_path.is_dir():
|
|
2303
|
-
try:
|
|
2304
|
-
with open(
|
|
2305
|
-
dep_path / "hatch_metadata.json", "r"
|
|
2306
|
-
) as f:
|
|
2307
|
-
dep_metadata = json.load(f)
|
|
2308
|
-
dep_service = PackageService(dep_metadata)
|
|
2309
|
-
package_names[i] = dep_service.get_field("name")
|
|
2310
|
-
except Exception as e:
|
|
2311
|
-
print(
|
|
2312
|
-
f"Warning: Could not resolve dependency path '{package_names[i]}': {e}"
|
|
2313
|
-
)
|
|
2314
|
-
|
|
2315
|
-
# Add the main package to the list
|
|
2316
|
-
package_names.append(package_name)
|
|
2317
|
-
|
|
2318
|
-
# Get MCP server configuration for all packages
|
|
2319
|
-
server_configs = [
|
|
2320
|
-
get_package_mcp_server_config(
|
|
2321
|
-
env_manager, env_name, pkg_name
|
|
2322
|
-
)
|
|
2323
|
-
for pkg_name in package_names
|
|
2324
|
-
]
|
|
2325
|
-
|
|
2326
|
-
print(
|
|
2327
|
-
f"Configuring MCP server for package '{package_name}' on {len(hosts)} host(s)..."
|
|
2328
|
-
)
|
|
2329
|
-
|
|
2330
|
-
# Configure on each host
|
|
2331
|
-
success_count = 0
|
|
2332
|
-
for host in hosts: # 'host', here, is a string
|
|
2333
|
-
try:
|
|
2334
|
-
# Convert string to MCPHostType enum
|
|
2335
|
-
host_type = MCPHostType(host)
|
|
2336
|
-
host_model_class = HOST_MODEL_REGISTRY.get(host_type)
|
|
2337
|
-
if not host_model_class:
|
|
2338
|
-
print(
|
|
2339
|
-
f"✗ Error: No model registered for host '{host}'"
|
|
2340
|
-
)
|
|
2341
|
-
continue
|
|
2342
|
-
|
|
2343
|
-
host_success_count = 0
|
|
2344
|
-
for i, server_config in enumerate(server_configs):
|
|
2345
|
-
pkg_name = package_names[i]
|
|
2346
|
-
try:
|
|
2347
|
-
# Convert MCPServerConfig to Omni model
|
|
2348
|
-
# Only include fields that have actual values
|
|
2349
|
-
omni_config_data = {"name": server_config.name}
|
|
2350
|
-
if server_config.command is not None:
|
|
2351
|
-
omni_config_data["command"] = (
|
|
2352
|
-
server_config.command
|
|
2353
|
-
)
|
|
2354
|
-
if server_config.args is not None:
|
|
2355
|
-
omni_config_data["args"] = (
|
|
2356
|
-
server_config.args
|
|
2357
|
-
)
|
|
2358
|
-
if server_config.env:
|
|
2359
|
-
omni_config_data["env"] = server_config.env
|
|
2360
|
-
if server_config.url is not None:
|
|
2361
|
-
omni_config_data["url"] = server_config.url
|
|
2362
|
-
headers = getattr(
|
|
2363
|
-
server_config, "headers", None
|
|
2364
|
-
)
|
|
2365
|
-
if headers is not None:
|
|
2366
|
-
omni_config_data["headers"] = headers
|
|
2367
|
-
|
|
2368
|
-
omni_config = MCPServerConfigOmni(
|
|
2369
|
-
**omni_config_data
|
|
2370
|
-
)
|
|
2371
|
-
|
|
2372
|
-
# Convert to host-specific model
|
|
2373
|
-
host_config = host_model_class.from_omni(
|
|
2374
|
-
omni_config
|
|
2375
|
-
)
|
|
2376
|
-
|
|
2377
|
-
# Generate and display conversion report
|
|
2378
|
-
report = generate_conversion_report(
|
|
2379
|
-
operation="create",
|
|
2380
|
-
server_name=server_config.name,
|
|
2381
|
-
target_host=host_type,
|
|
2382
|
-
omni=omni_config,
|
|
2383
|
-
dry_run=False,
|
|
2384
|
-
)
|
|
2385
|
-
display_report(report)
|
|
2386
|
-
|
|
2387
|
-
result = mcp_manager.configure_server(
|
|
2388
|
-
hostname=host,
|
|
2389
|
-
server_config=host_config,
|
|
2390
|
-
no_backup=False, # Always backup when adding packages
|
|
2391
|
-
)
|
|
2392
|
-
|
|
2393
|
-
if result.success:
|
|
2394
|
-
print(
|
|
2395
|
-
f"✓ Configured {server_config.name} ({pkg_name}) on {host}"
|
|
2396
|
-
)
|
|
2397
|
-
host_success_count += 1
|
|
2398
|
-
|
|
2399
|
-
# Update package metadata with host configuration tracking
|
|
2400
|
-
try:
|
|
2401
|
-
server_config_dict = {
|
|
2402
|
-
"name": server_config.name,
|
|
2403
|
-
"command": server_config.command,
|
|
2404
|
-
"args": server_config.args,
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
env_manager.update_package_host_configuration(
|
|
2408
|
-
env_name=env_name,
|
|
2409
|
-
package_name=pkg_name,
|
|
2410
|
-
hostname=host,
|
|
2411
|
-
server_config=server_config_dict,
|
|
2412
|
-
)
|
|
2413
|
-
except Exception as e:
|
|
2414
|
-
# Log but don't fail the configuration operation
|
|
2415
|
-
print(
|
|
2416
|
-
f"[WARNING] Failed to update package metadata for {pkg_name}: {e}"
|
|
2417
|
-
)
|
|
2418
|
-
else:
|
|
2419
|
-
print(
|
|
2420
|
-
f"✗ Failed to configure {server_config.name} ({pkg_name}) on {host}: {result.error_message}"
|
|
2421
|
-
)
|
|
2422
|
-
|
|
2423
|
-
except Exception as e:
|
|
2424
|
-
print(
|
|
2425
|
-
f"✗ Error configuring {server_config.name} ({pkg_name}) on {host}: {e}"
|
|
2426
|
-
)
|
|
2427
|
-
|
|
2428
|
-
if host_success_count == len(server_configs):
|
|
2429
|
-
success_count += 1
|
|
2430
|
-
|
|
2431
|
-
except ValueError as e:
|
|
2432
|
-
print(f"✗ Invalid host '{host}': {e}")
|
|
2433
|
-
continue
|
|
2434
|
-
|
|
2435
|
-
if success_count > 0:
|
|
2436
|
-
print(
|
|
2437
|
-
f"MCP configuration completed: {success_count}/{len(hosts)} hosts configured"
|
|
2438
|
-
)
|
|
2439
|
-
else:
|
|
2440
|
-
print("Warning: MCP configuration failed on all hosts")
|
|
2441
|
-
|
|
2442
|
-
except ValueError as e:
|
|
2443
|
-
print(f"Warning: MCP host configuration failed: {e}")
|
|
2444
|
-
# Don't fail the entire operation for MCP configuration issues
|
|
2445
|
-
|
|
2446
|
-
return 0
|
|
2447
|
-
else:
|
|
2448
|
-
print(f"Failed to add package: {args.package_path_or_name}")
|
|
2449
|
-
return 1
|
|
2450
|
-
|
|
2451
|
-
elif args.pkg_command == "remove":
|
|
2452
|
-
if env_manager.remove_package(args.package_name, args.env):
|
|
2453
|
-
print(f"Successfully removed package: {args.package_name}")
|
|
2454
|
-
return 0
|
|
2455
|
-
else:
|
|
2456
|
-
print(f"Failed to remove package: {args.package_name}")
|
|
2457
|
-
return 1
|
|
2458
|
-
|
|
2459
|
-
elif args.pkg_command == "list":
|
|
2460
|
-
packages = env_manager.list_packages(args.env)
|
|
2461
|
-
|
|
2462
|
-
if not packages:
|
|
2463
|
-
print(f"No packages found in environment: {args.env}")
|
|
2464
|
-
return 0
|
|
2465
|
-
|
|
2466
|
-
print(f"Packages in environment '{args.env}':")
|
|
2467
|
-
for pkg in packages:
|
|
2468
|
-
print(
|
|
2469
|
-
f"{pkg['name']} ({pkg['version']})\tHatch compliant: {pkg['hatch_compliant']}\tsource: {pkg['source']['uri']}\tlocation: {pkg['source']['path']}"
|
|
2470
|
-
)
|
|
2471
|
-
return 0
|
|
2472
|
-
|
|
2473
|
-
elif args.pkg_command == "sync":
|
|
2474
|
-
try:
|
|
2475
|
-
# Parse host list
|
|
2476
|
-
hosts = parse_host_list(args.host)
|
|
2477
|
-
env_name = args.env or env_manager.get_current_environment()
|
|
2478
|
-
|
|
2479
|
-
# Get all packages to sync (main package + dependencies)
|
|
2480
|
-
package_names = [args.package_name]
|
|
2481
|
-
|
|
2482
|
-
# Try to get dependencies for the main package
|
|
2483
|
-
try:
|
|
2484
|
-
env_data = env_manager.get_environment_data(env_name)
|
|
2485
|
-
if env_data:
|
|
2486
|
-
# Find the main package in the environment
|
|
2487
|
-
main_package = None
|
|
2488
|
-
for pkg in env_data.packages:
|
|
2489
|
-
if pkg.name == args.package_name:
|
|
2490
|
-
main_package = pkg
|
|
2491
|
-
break
|
|
2492
|
-
|
|
2493
|
-
if main_package:
|
|
2494
|
-
# Create a minimal metadata structure for PackageService
|
|
2495
|
-
metadata = {
|
|
2496
|
-
"name": main_package.name,
|
|
2497
|
-
"version": main_package.version,
|
|
2498
|
-
"dependencies": {}, # Will be populated if needed
|
|
2499
|
-
}
|
|
2500
|
-
package_service = PackageService(metadata)
|
|
2501
|
-
|
|
2502
|
-
# Get Hatch dependencies
|
|
2503
|
-
dependencies = package_service.get_dependencies()
|
|
2504
|
-
hatch_deps = dependencies.get("hatch", [])
|
|
2505
|
-
dep_names = [
|
|
2506
|
-
dep.get("name") for dep in hatch_deps if dep.get("name")
|
|
2507
|
-
]
|
|
2508
|
-
|
|
2509
|
-
# Add dependencies to the sync list (before main package)
|
|
2510
|
-
package_names = dep_names + [args.package_name]
|
|
2511
|
-
else:
|
|
2512
|
-
print(
|
|
2513
|
-
f"Warning: Package '{args.package_name}' not found in environment '{env_name}'. Syncing only the specified package."
|
|
2514
|
-
)
|
|
2515
|
-
else:
|
|
2516
|
-
print(
|
|
2517
|
-
f"Warning: Could not access environment '{env_name}'. Syncing only the specified package."
|
|
2518
|
-
)
|
|
2519
|
-
except Exception as e:
|
|
2520
|
-
print(
|
|
2521
|
-
f"Warning: Could not analyze dependencies for '{args.package_name}': {e}. Syncing only the specified package."
|
|
2522
|
-
)
|
|
2523
|
-
|
|
2524
|
-
# Get MCP server configurations for all packages
|
|
2525
|
-
server_configs = []
|
|
2526
|
-
for pkg_name in package_names:
|
|
2527
|
-
try:
|
|
2528
|
-
config = get_package_mcp_server_config(
|
|
2529
|
-
env_manager, env_name, pkg_name
|
|
2530
|
-
)
|
|
2531
|
-
server_configs.append((pkg_name, config))
|
|
2532
|
-
except Exception as e:
|
|
2533
|
-
print(
|
|
2534
|
-
f"Warning: Could not get MCP configuration for package '{pkg_name}': {e}"
|
|
2535
|
-
)
|
|
2536
|
-
|
|
2537
|
-
if not server_configs:
|
|
2538
|
-
print(
|
|
2539
|
-
f"Error: No MCP server configurations found for package '{args.package_name}' or its dependencies"
|
|
2540
|
-
)
|
|
2541
|
-
return 1
|
|
2542
|
-
|
|
2543
|
-
if args.dry_run:
|
|
2544
|
-
print(
|
|
2545
|
-
f"[DRY RUN] Would synchronize MCP servers for {len(server_configs)} package(s) to hosts: {[h for h in hosts]}"
|
|
2546
|
-
)
|
|
2547
|
-
for pkg_name, config in server_configs:
|
|
2548
|
-
print(
|
|
2549
|
-
f"[DRY RUN] - {pkg_name}: {config.name} -> {' '.join(config.args)}"
|
|
2550
|
-
)
|
|
2551
|
-
|
|
2552
|
-
# Generate and display conversion reports for dry-run mode
|
|
2553
|
-
for host in hosts:
|
|
2554
|
-
try:
|
|
2555
|
-
host_type = MCPHostType(host)
|
|
2556
|
-
host_model_class = HOST_MODEL_REGISTRY.get(host_type)
|
|
2557
|
-
if not host_model_class:
|
|
2558
|
-
print(
|
|
2559
|
-
f"[DRY RUN] ✗ Error: No model registered for host '{host}'"
|
|
2560
|
-
)
|
|
2561
|
-
continue
|
|
2562
|
-
|
|
2563
|
-
# Convert to Omni model
|
|
2564
|
-
# Only include fields that have actual values
|
|
2565
|
-
omni_config_data = {"name": config.name}
|
|
2566
|
-
if config.command is not None:
|
|
2567
|
-
omni_config_data["command"] = config.command
|
|
2568
|
-
if config.args is not None:
|
|
2569
|
-
omni_config_data["args"] = config.args
|
|
2570
|
-
if config.env:
|
|
2571
|
-
omni_config_data["env"] = config.env
|
|
2572
|
-
if config.url is not None:
|
|
2573
|
-
omni_config_data["url"] = config.url
|
|
2574
|
-
headers = getattr(config, "headers", None)
|
|
2575
|
-
if headers is not None:
|
|
2576
|
-
omni_config_data["headers"] = headers
|
|
2577
|
-
|
|
2578
|
-
omni_config = MCPServerConfigOmni(**omni_config_data)
|
|
2579
|
-
|
|
2580
|
-
# Generate report
|
|
2581
|
-
report = generate_conversion_report(
|
|
2582
|
-
operation="create",
|
|
2583
|
-
server_name=config.name,
|
|
2584
|
-
target_host=host_type,
|
|
2585
|
-
omni=omni_config,
|
|
2586
|
-
dry_run=True,
|
|
2587
|
-
)
|
|
2588
|
-
print(f"[DRY RUN] Preview for {pkg_name} on {host}:")
|
|
2589
|
-
display_report(report)
|
|
2590
|
-
except ValueError as e:
|
|
2591
|
-
print(f"[DRY RUN] ✗ Invalid host '{host}': {e}")
|
|
2592
|
-
return 0
|
|
2593
|
-
|
|
2594
|
-
# Confirm operation unless auto-approved
|
|
2595
|
-
package_desc = (
|
|
2596
|
-
f"package '{args.package_name}'"
|
|
2597
|
-
if len(server_configs) == 1
|
|
2598
|
-
else f"{len(server_configs)} packages ('{args.package_name}' + dependencies)"
|
|
2599
|
-
)
|
|
2600
|
-
if not request_confirmation(
|
|
2601
|
-
f"Synchronize MCP servers for {package_desc} to {len(hosts)} host(s)?",
|
|
2602
|
-
args.auto_approve,
|
|
2603
|
-
):
|
|
2604
|
-
print("Operation cancelled.")
|
|
2605
|
-
return 0
|
|
2606
|
-
|
|
2607
|
-
# Perform synchronization to each host for all packages
|
|
2608
|
-
total_operations = len(server_configs) * len(hosts)
|
|
2609
|
-
success_count = 0
|
|
2610
|
-
|
|
2611
|
-
for host in hosts:
|
|
2612
|
-
try:
|
|
2613
|
-
# Convert string to MCPHostType enum
|
|
2614
|
-
host_type = MCPHostType(host)
|
|
2615
|
-
host_model_class = HOST_MODEL_REGISTRY.get(host_type)
|
|
2616
|
-
if not host_model_class:
|
|
2617
|
-
print(f"✗ Error: No model registered for host '{host}'")
|
|
2618
|
-
continue
|
|
2619
|
-
|
|
2620
|
-
for pkg_name, server_config in server_configs:
|
|
2621
|
-
try:
|
|
2622
|
-
# Convert MCPServerConfig to Omni model
|
|
2623
|
-
# Only include fields that have actual values
|
|
2624
|
-
omni_config_data = {"name": server_config.name}
|
|
2625
|
-
if server_config.command is not None:
|
|
2626
|
-
omni_config_data["command"] = server_config.command
|
|
2627
|
-
if server_config.args is not None:
|
|
2628
|
-
omni_config_data["args"] = server_config.args
|
|
2629
|
-
if server_config.env:
|
|
2630
|
-
omni_config_data["env"] = server_config.env
|
|
2631
|
-
if server_config.url is not None:
|
|
2632
|
-
omni_config_data["url"] = server_config.url
|
|
2633
|
-
headers = getattr(server_config, "headers", None)
|
|
2634
|
-
if headers is not None:
|
|
2635
|
-
omni_config_data["headers"] = headers
|
|
2636
|
-
|
|
2637
|
-
omni_config = MCPServerConfigOmni(**omni_config_data)
|
|
2638
|
-
|
|
2639
|
-
# Convert to host-specific model
|
|
2640
|
-
host_config = host_model_class.from_omni(omni_config)
|
|
2641
|
-
|
|
2642
|
-
# Generate and display conversion report
|
|
2643
|
-
report = generate_conversion_report(
|
|
2644
|
-
operation="create",
|
|
2645
|
-
server_name=server_config.name,
|
|
2646
|
-
target_host=host_type,
|
|
2647
|
-
omni=omni_config,
|
|
2648
|
-
dry_run=False,
|
|
2649
|
-
)
|
|
2650
|
-
display_report(report)
|
|
2651
|
-
|
|
2652
|
-
result = mcp_manager.configure_server(
|
|
2653
|
-
hostname=host,
|
|
2654
|
-
server_config=host_config,
|
|
2655
|
-
no_backup=args.no_backup,
|
|
2656
|
-
)
|
|
2657
|
-
|
|
2658
|
-
if result.success:
|
|
2659
|
-
print(
|
|
2660
|
-
f"[SUCCESS] Successfully configured {server_config.name} ({pkg_name}) on {host}"
|
|
2661
|
-
)
|
|
2662
|
-
success_count += 1
|
|
2663
|
-
|
|
2664
|
-
# Update package metadata with host configuration tracking
|
|
2665
|
-
try:
|
|
2666
|
-
server_config_dict = {
|
|
2667
|
-
"name": server_config.name,
|
|
2668
|
-
"command": server_config.command,
|
|
2669
|
-
"args": server_config.args,
|
|
2670
|
-
}
|
|
2671
|
-
|
|
2672
|
-
env_manager.update_package_host_configuration(
|
|
2673
|
-
env_name=env_name,
|
|
2674
|
-
package_name=pkg_name,
|
|
2675
|
-
hostname=host,
|
|
2676
|
-
server_config=server_config_dict,
|
|
2677
|
-
)
|
|
2678
|
-
except Exception as e:
|
|
2679
|
-
# Log but don't fail the sync operation
|
|
2680
|
-
print(
|
|
2681
|
-
f"[WARNING] Failed to update package metadata for {pkg_name}: {e}"
|
|
2682
|
-
)
|
|
2683
|
-
else:
|
|
2684
|
-
print(
|
|
2685
|
-
f"[ERROR] Failed to configure {server_config.name} ({pkg_name}) on {host}: {result.error_message}"
|
|
2686
|
-
)
|
|
2687
|
-
|
|
2688
|
-
except Exception as e:
|
|
2689
|
-
print(
|
|
2690
|
-
f"[ERROR] Error configuring {server_config.name} ({pkg_name}) on {host}: {e}"
|
|
2691
|
-
)
|
|
2692
|
-
|
|
2693
|
-
except ValueError as e:
|
|
2694
|
-
print(f"✗ Invalid host '{host}': {e}")
|
|
2695
|
-
continue
|
|
2696
|
-
|
|
2697
|
-
# Report results
|
|
2698
|
-
if success_count == total_operations:
|
|
2699
|
-
package_desc = (
|
|
2700
|
-
f"package '{args.package_name}'"
|
|
2701
|
-
if len(server_configs) == 1
|
|
2702
|
-
else f"{len(server_configs)} packages"
|
|
2703
|
-
)
|
|
2704
|
-
print(
|
|
2705
|
-
f"Successfully synchronized {package_desc} to all {len(hosts)} host(s)"
|
|
2706
|
-
)
|
|
2707
|
-
return 0
|
|
2708
|
-
elif success_count > 0:
|
|
2709
|
-
print(
|
|
2710
|
-
f"Partially synchronized: {success_count}/{total_operations} operations succeeded"
|
|
2711
|
-
)
|
|
2712
|
-
return 1
|
|
2713
|
-
else:
|
|
2714
|
-
package_desc = (
|
|
2715
|
-
f"package '{args.package_name}'"
|
|
2716
|
-
if len(server_configs) == 1
|
|
2717
|
-
else f"{len(server_configs)} packages"
|
|
2718
|
-
)
|
|
2719
|
-
print(f"Failed to synchronize {package_desc} to any hosts")
|
|
2720
|
-
return 1
|
|
2721
|
-
|
|
2722
|
-
except ValueError as e:
|
|
2723
|
-
print(f"Error: {e}")
|
|
2724
|
-
return 1
|
|
2725
|
-
|
|
2726
|
-
else:
|
|
2727
|
-
parser.print_help()
|
|
2728
|
-
return 1
|
|
2729
|
-
|
|
2730
|
-
elif args.command == "mcp":
|
|
2731
|
-
if args.mcp_command == "discover":
|
|
2732
|
-
if args.discover_command == "hosts":
|
|
2733
|
-
return handle_mcp_discover_hosts()
|
|
2734
|
-
elif args.discover_command == "servers":
|
|
2735
|
-
return handle_mcp_discover_servers(env_manager, args.env)
|
|
2736
|
-
else:
|
|
2737
|
-
print("Unknown discover command")
|
|
2738
|
-
return 1
|
|
2739
|
-
|
|
2740
|
-
elif args.mcp_command == "list":
|
|
2741
|
-
if args.list_command == "hosts":
|
|
2742
|
-
return handle_mcp_list_hosts(env_manager, args.env, args.detailed)
|
|
2743
|
-
elif args.list_command == "servers":
|
|
2744
|
-
return handle_mcp_list_servers(env_manager, args.env)
|
|
2745
|
-
else:
|
|
2746
|
-
print("Unknown list command")
|
|
2747
|
-
return 1
|
|
2748
|
-
|
|
2749
|
-
elif args.mcp_command == "backup":
|
|
2750
|
-
if args.backup_command == "restore":
|
|
2751
|
-
return handle_mcp_backup_restore(
|
|
2752
|
-
env_manager,
|
|
2753
|
-
args.host,
|
|
2754
|
-
args.backup_file,
|
|
2755
|
-
args.dry_run,
|
|
2756
|
-
args.auto_approve,
|
|
2757
|
-
)
|
|
2758
|
-
elif args.backup_command == "list":
|
|
2759
|
-
return handle_mcp_backup_list(args.host, args.detailed)
|
|
2760
|
-
elif args.backup_command == "clean":
|
|
2761
|
-
return handle_mcp_backup_clean(
|
|
2762
|
-
args.host,
|
|
2763
|
-
args.older_than_days,
|
|
2764
|
-
args.keep_count,
|
|
2765
|
-
args.dry_run,
|
|
2766
|
-
args.auto_approve,
|
|
2767
|
-
)
|
|
2768
|
-
else:
|
|
2769
|
-
print("Unknown backup command")
|
|
2770
|
-
return 1
|
|
2771
|
-
|
|
2772
|
-
elif args.mcp_command == "configure":
|
|
2773
|
-
return handle_mcp_configure(
|
|
2774
|
-
args.host,
|
|
2775
|
-
args.server_name,
|
|
2776
|
-
args.server_command,
|
|
2777
|
-
args.args,
|
|
2778
|
-
getattr(args, "env_var", None),
|
|
2779
|
-
args.url,
|
|
2780
|
-
args.header,
|
|
2781
|
-
getattr(args, "timeout", None),
|
|
2782
|
-
getattr(args, "trust", False),
|
|
2783
|
-
getattr(args, "cwd", None),
|
|
2784
|
-
getattr(args, "env_file", None),
|
|
2785
|
-
getattr(args, "http_url", None),
|
|
2786
|
-
getattr(args, "include_tools", None),
|
|
2787
|
-
getattr(args, "exclude_tools", None),
|
|
2788
|
-
getattr(args, "input", None),
|
|
2789
|
-
getattr(args, "disabled", None),
|
|
2790
|
-
getattr(args, "auto_approve_tools", None),
|
|
2791
|
-
getattr(args, "disable_tools", None),
|
|
2792
|
-
getattr(args, "env_vars", None),
|
|
2793
|
-
getattr(args, "startup_timeout", None),
|
|
2794
|
-
getattr(args, "tool_timeout", None),
|
|
2795
|
-
getattr(args, "enabled", None),
|
|
2796
|
-
getattr(args, "bearer_token_env_var", None),
|
|
2797
|
-
getattr(args, "env_header", None),
|
|
2798
|
-
args.no_backup,
|
|
2799
|
-
args.dry_run,
|
|
2800
|
-
args.auto_approve,
|
|
2801
|
-
)
|
|
2802
|
-
|
|
2803
|
-
elif args.mcp_command == "remove":
|
|
2804
|
-
if args.remove_command == "server":
|
|
2805
|
-
return handle_mcp_remove_server(
|
|
2806
|
-
env_manager,
|
|
2807
|
-
args.server_name,
|
|
2808
|
-
args.host,
|
|
2809
|
-
args.env,
|
|
2810
|
-
args.no_backup,
|
|
2811
|
-
args.dry_run,
|
|
2812
|
-
args.auto_approve,
|
|
2813
|
-
)
|
|
2814
|
-
elif args.remove_command == "host":
|
|
2815
|
-
return handle_mcp_remove_host(
|
|
2816
|
-
env_manager,
|
|
2817
|
-
args.host_name,
|
|
2818
|
-
args.no_backup,
|
|
2819
|
-
args.dry_run,
|
|
2820
|
-
args.auto_approve,
|
|
2821
|
-
)
|
|
2822
|
-
else:
|
|
2823
|
-
print("Unknown remove command")
|
|
2824
|
-
return 1
|
|
2825
|
-
|
|
2826
|
-
elif args.mcp_command == "sync":
|
|
2827
|
-
return handle_mcp_sync(
|
|
2828
|
-
from_env=getattr(args, "from_env", None),
|
|
2829
|
-
from_host=getattr(args, "from_host", None),
|
|
2830
|
-
to_hosts=args.to_host,
|
|
2831
|
-
servers=getattr(args, "servers", None),
|
|
2832
|
-
pattern=getattr(args, "pattern", None),
|
|
2833
|
-
dry_run=args.dry_run,
|
|
2834
|
-
auto_approve=args.auto_approve,
|
|
2835
|
-
no_backup=args.no_backup,
|
|
2836
|
-
)
|
|
2837
|
-
|
|
2838
|
-
else:
|
|
2839
|
-
print("Unknown MCP command")
|
|
2840
|
-
return 1
|
|
2841
|
-
|
|
2842
|
-
else:
|
|
2843
|
-
parser.print_help()
|
|
2844
|
-
return 1
|
|
2845
|
-
|
|
2846
|
-
return 0
|
|
2847
|
-
|
|
2848
117
|
|
|
2849
|
-
|
|
2850
|
-
|
|
118
|
+
__all__ = [
|
|
119
|
+
# Entry point
|
|
120
|
+
'main',
|
|
121
|
+
# Exit codes
|
|
122
|
+
'EXIT_SUCCESS',
|
|
123
|
+
'EXIT_ERROR',
|
|
124
|
+
# Utilities
|
|
125
|
+
'get_hatch_version',
|
|
126
|
+
'request_confirmation',
|
|
127
|
+
'parse_env_vars',
|
|
128
|
+
'parse_header',
|
|
129
|
+
'parse_input',
|
|
130
|
+
'parse_host_list',
|
|
131
|
+
'get_package_mcp_server_config',
|
|
132
|
+
# MCP handlers
|
|
133
|
+
'handle_mcp_discover_hosts',
|
|
134
|
+
'handle_mcp_discover_servers',
|
|
135
|
+
'handle_mcp_list_hosts',
|
|
136
|
+
'handle_mcp_list_servers',
|
|
137
|
+
'handle_mcp_show',
|
|
138
|
+
'handle_mcp_backup_restore',
|
|
139
|
+
'handle_mcp_backup_list',
|
|
140
|
+
'handle_mcp_backup_clean',
|
|
141
|
+
'handle_mcp_configure',
|
|
142
|
+
'handle_mcp_remove',
|
|
143
|
+
'handle_mcp_remove_server',
|
|
144
|
+
'handle_mcp_remove_host',
|
|
145
|
+
'handle_mcp_sync',
|
|
146
|
+
# Environment handlers
|
|
147
|
+
'handle_env_create',
|
|
148
|
+
'handle_env_remove',
|
|
149
|
+
'handle_env_list',
|
|
150
|
+
'handle_env_use',
|
|
151
|
+
'handle_env_current',
|
|
152
|
+
'handle_env_show',
|
|
153
|
+
'handle_env_python_init',
|
|
154
|
+
'handle_env_python_info',
|
|
155
|
+
'handle_env_python_remove',
|
|
156
|
+
'handle_env_python_shell',
|
|
157
|
+
'handle_env_python_add_hatch_mcp',
|
|
158
|
+
# Package handlers
|
|
159
|
+
'handle_package_add',
|
|
160
|
+
'handle_package_remove',
|
|
161
|
+
'handle_package_list',
|
|
162
|
+
'handle_package_sync',
|
|
163
|
+
# System handlers
|
|
164
|
+
'handle_create',
|
|
165
|
+
'handle_validate',
|
|
166
|
+
# Types
|
|
167
|
+
'HatchEnvironmentManager',
|
|
168
|
+
'MCPHostConfigurationManager',
|
|
169
|
+
'MCPHostRegistry',
|
|
170
|
+
'MCPHostType',
|
|
171
|
+
'MCPServerConfig',
|
|
172
|
+
]
|