hatch-xclam 0.7.0__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 +21 -0
- hatch/cli_hatch.py +2748 -0
- hatch/environment_manager.py +1375 -0
- hatch/installers/__init__.py +25 -0
- hatch/installers/dependency_installation_orchestrator.py +636 -0
- hatch/installers/docker_installer.py +545 -0
- hatch/installers/hatch_installer.py +198 -0
- hatch/installers/installation_context.py +109 -0
- hatch/installers/installer_base.py +195 -0
- hatch/installers/python_installer.py +342 -0
- hatch/installers/registry.py +179 -0
- hatch/installers/system_installer.py +588 -0
- hatch/mcp_host_config/__init__.py +38 -0
- hatch/mcp_host_config/backup.py +458 -0
- hatch/mcp_host_config/host_management.py +572 -0
- hatch/mcp_host_config/models.py +602 -0
- hatch/mcp_host_config/reporting.py +181 -0
- hatch/mcp_host_config/strategies.py +513 -0
- hatch/package_loader.py +263 -0
- hatch/python_environment_manager.py +734 -0
- hatch/registry_explorer.py +171 -0
- hatch/registry_retriever.py +335 -0
- hatch/template_generator.py +179 -0
- hatch_xclam-0.7.0.dist-info/METADATA +150 -0
- hatch_xclam-0.7.0.dist-info/RECORD +93 -0
- hatch_xclam-0.7.0.dist-info/WHEEL +5 -0
- hatch_xclam-0.7.0.dist-info/entry_points.txt +2 -0
- hatch_xclam-0.7.0.dist-info/licenses/LICENSE +661 -0
- hatch_xclam-0.7.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/run_environment_tests.py +124 -0
- tests/test_cli_version.py +122 -0
- tests/test_data/packages/basic/base_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg/mcp_server.py +21 -0
- tests/test_data/packages/basic/base_pkg_v2/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg_v2/mcp_server.py +21 -0
- tests/test_data/packages/basic/utility_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/utility_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/python_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/python_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/system_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/system_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/mcp_server.py +21 -0
- tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py +21 -0
- tests/test_data_utils.py +472 -0
- tests/test_dependency_orchestrator_consent.py +266 -0
- tests/test_docker_installer.py +524 -0
- tests/test_env_manip.py +991 -0
- tests/test_hatch_installer.py +179 -0
- tests/test_installer_base.py +221 -0
- tests/test_mcp_atomic_operations.py +276 -0
- tests/test_mcp_backup_integration.py +308 -0
- tests/test_mcp_cli_all_host_specific_args.py +303 -0
- tests/test_mcp_cli_backup_management.py +295 -0
- tests/test_mcp_cli_direct_management.py +453 -0
- tests/test_mcp_cli_discovery_listing.py +582 -0
- tests/test_mcp_cli_host_config_integration.py +823 -0
- tests/test_mcp_cli_package_management.py +360 -0
- tests/test_mcp_cli_partial_updates.py +859 -0
- tests/test_mcp_environment_integration.py +520 -0
- tests/test_mcp_host_config_backup.py +257 -0
- tests/test_mcp_host_configuration_manager.py +331 -0
- tests/test_mcp_host_registry_decorator.py +348 -0
- tests/test_mcp_pydantic_architecture_v4.py +603 -0
- tests/test_mcp_server_config_models.py +242 -0
- tests/test_mcp_server_config_type_field.py +221 -0
- tests/test_mcp_sync_functionality.py +316 -0
- tests/test_mcp_user_feedback_reporting.py +359 -0
- tests/test_non_tty_integration.py +281 -0
- tests/test_online_package_loader.py +202 -0
- tests/test_python_environment_manager.py +882 -0
- tests/test_python_installer.py +327 -0
- tests/test_registry.py +51 -0
- tests/test_registry_retriever.py +250 -0
- tests/test_system_installer.py +733 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP host configuration management with decorator-based strategy registration.
|
|
3
|
+
|
|
4
|
+
This module provides the core host management infrastructure including
|
|
5
|
+
decorator-based strategy registration following Hatchling patterns,
|
|
6
|
+
host registry, and configuration manager with consolidated model support.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, List, Type, Optional, Callable, Any
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
from .models import (
|
|
15
|
+
MCPHostType, MCPServerConfig, HostConfiguration, EnvironmentData,
|
|
16
|
+
ConfigurationResult, SyncResult
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MCPHostRegistry:
|
|
23
|
+
"""Registry for MCP host strategies with decorator-based registration."""
|
|
24
|
+
|
|
25
|
+
_strategies: Dict[MCPHostType, Type["MCPHostStrategy"]] = {}
|
|
26
|
+
_instances: Dict[MCPHostType, "MCPHostStrategy"] = {}
|
|
27
|
+
_family_mappings: Dict[str, List[MCPHostType]] = {
|
|
28
|
+
"claude": [MCPHostType.CLAUDE_DESKTOP, MCPHostType.CLAUDE_CODE],
|
|
29
|
+
"cursor": [MCPHostType.CURSOR, MCPHostType.LMSTUDIO]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def register(cls, host_type: MCPHostType):
|
|
34
|
+
"""Decorator to register a host strategy class."""
|
|
35
|
+
def decorator(strategy_class: Type["MCPHostStrategy"]):
|
|
36
|
+
if not issubclass(strategy_class, MCPHostStrategy):
|
|
37
|
+
raise ValueError(f"Strategy class {strategy_class.__name__} must inherit from MCPHostStrategy")
|
|
38
|
+
|
|
39
|
+
if host_type in cls._strategies:
|
|
40
|
+
logger.warning(f"Overriding existing strategy for {host_type}: {cls._strategies[host_type].__name__} -> {strategy_class.__name__}")
|
|
41
|
+
|
|
42
|
+
cls._strategies[host_type] = strategy_class
|
|
43
|
+
logger.debug(f"Registered MCP host strategy '{host_type}' -> {strategy_class.__name__}")
|
|
44
|
+
return strategy_class
|
|
45
|
+
return decorator
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def get_strategy(cls, host_type: MCPHostType) -> "MCPHostStrategy":
|
|
49
|
+
"""Get strategy instance for host type."""
|
|
50
|
+
if host_type not in cls._strategies:
|
|
51
|
+
available = list(cls._strategies.keys())
|
|
52
|
+
raise ValueError(f"Unknown host type: '{host_type}'. Available: {available}")
|
|
53
|
+
|
|
54
|
+
if host_type not in cls._instances:
|
|
55
|
+
cls._instances[host_type] = cls._strategies[host_type]()
|
|
56
|
+
|
|
57
|
+
return cls._instances[host_type]
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def detect_available_hosts(cls) -> List[MCPHostType]:
|
|
61
|
+
"""Detect available hosts on the system."""
|
|
62
|
+
available_hosts = []
|
|
63
|
+
for host_type, strategy_class in cls._strategies.items():
|
|
64
|
+
try:
|
|
65
|
+
strategy = cls.get_strategy(host_type)
|
|
66
|
+
if strategy.is_host_available():
|
|
67
|
+
available_hosts.append(host_type)
|
|
68
|
+
except Exception:
|
|
69
|
+
# Host detection failed, skip
|
|
70
|
+
continue
|
|
71
|
+
return available_hosts
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def get_family_hosts(cls, family: str) -> List[MCPHostType]:
|
|
75
|
+
"""Get all hosts in a strategy family."""
|
|
76
|
+
return cls._family_mappings.get(family, [])
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def get_host_config_path(cls, host_type: MCPHostType) -> Optional[Path]:
|
|
80
|
+
"""Get configuration path for host type."""
|
|
81
|
+
strategy = cls.get_strategy(host_type)
|
|
82
|
+
return strategy.get_config_path()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def register_host_strategy(host_type: MCPHostType) -> Callable:
|
|
86
|
+
"""Convenience decorator for registering host strategies."""
|
|
87
|
+
return MCPHostRegistry.register(host_type)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class MCPHostStrategy:
|
|
91
|
+
"""Abstract base class for host configuration strategies."""
|
|
92
|
+
|
|
93
|
+
def get_config_path(self) -> Optional[Path]:
|
|
94
|
+
"""Get configuration file path for this host."""
|
|
95
|
+
raise NotImplementedError("Subclasses must implement get_config_path")
|
|
96
|
+
|
|
97
|
+
def is_host_available(self) -> bool:
|
|
98
|
+
"""Check if host is available on system."""
|
|
99
|
+
raise NotImplementedError("Subclasses must implement is_host_available")
|
|
100
|
+
|
|
101
|
+
def read_configuration(self) -> HostConfiguration:
|
|
102
|
+
"""Read and parse host configuration."""
|
|
103
|
+
raise NotImplementedError("Subclasses must implement read_configuration")
|
|
104
|
+
|
|
105
|
+
def write_configuration(self, config: HostConfiguration,
|
|
106
|
+
no_backup: bool = False) -> bool:
|
|
107
|
+
"""Write configuration to host file."""
|
|
108
|
+
raise NotImplementedError("Subclasses must implement write_configuration")
|
|
109
|
+
|
|
110
|
+
def validate_server_config(self, server_config: MCPServerConfig) -> bool:
|
|
111
|
+
"""Validate server configuration for this host."""
|
|
112
|
+
raise NotImplementedError("Subclasses must implement validate_server_config")
|
|
113
|
+
|
|
114
|
+
def get_config_key(self) -> str:
|
|
115
|
+
"""Get the root configuration key for MCP servers."""
|
|
116
|
+
return "mcpServers" # Default for most platforms
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class MCPHostConfigurationManager:
|
|
120
|
+
"""Central manager for MCP host configuration operations."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, backup_manager: Optional[Any] = None):
|
|
123
|
+
self.host_registry = MCPHostRegistry
|
|
124
|
+
self.backup_manager = backup_manager or self._create_default_backup_manager()
|
|
125
|
+
|
|
126
|
+
def _create_default_backup_manager(self):
|
|
127
|
+
"""Create default backup manager."""
|
|
128
|
+
try:
|
|
129
|
+
from .backup import MCPHostConfigBackupManager
|
|
130
|
+
return MCPHostConfigBackupManager()
|
|
131
|
+
except ImportError:
|
|
132
|
+
logger.warning("Backup manager not available")
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def configure_server(self, server_config: MCPServerConfig,
|
|
136
|
+
hostname: str, no_backup: bool = False) -> ConfigurationResult:
|
|
137
|
+
"""Configure MCP server on specified host."""
|
|
138
|
+
try:
|
|
139
|
+
host_type = MCPHostType(hostname)
|
|
140
|
+
strategy = self.host_registry.get_strategy(host_type)
|
|
141
|
+
|
|
142
|
+
# Validate server configuration for this host
|
|
143
|
+
if not strategy.validate_server_config(server_config):
|
|
144
|
+
return ConfigurationResult(
|
|
145
|
+
success=False,
|
|
146
|
+
hostname=hostname,
|
|
147
|
+
error_message=f"Server configuration invalid for {hostname}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Read current configuration
|
|
151
|
+
current_config = strategy.read_configuration()
|
|
152
|
+
|
|
153
|
+
# Create backup if requested
|
|
154
|
+
backup_path = None
|
|
155
|
+
if not no_backup and self.backup_manager:
|
|
156
|
+
config_path = strategy.get_config_path()
|
|
157
|
+
if config_path and config_path.exists():
|
|
158
|
+
backup_result = self.backup_manager.create_backup(config_path, hostname)
|
|
159
|
+
if backup_result.success:
|
|
160
|
+
backup_path = backup_result.backup_path
|
|
161
|
+
|
|
162
|
+
# Add server to configuration
|
|
163
|
+
server_name = getattr(server_config, 'name', 'default_server')
|
|
164
|
+
current_config.add_server(server_name, server_config)
|
|
165
|
+
|
|
166
|
+
# Write updated configuration
|
|
167
|
+
success = strategy.write_configuration(current_config, no_backup=no_backup)
|
|
168
|
+
|
|
169
|
+
return ConfigurationResult(
|
|
170
|
+
success=success,
|
|
171
|
+
hostname=hostname,
|
|
172
|
+
server_name=server_name,
|
|
173
|
+
backup_created=backup_path is not None,
|
|
174
|
+
backup_path=backup_path
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
return ConfigurationResult(
|
|
179
|
+
success=False,
|
|
180
|
+
hostname=hostname,
|
|
181
|
+
error_message=str(e)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def get_server_config(self, hostname: str, server_name: str) -> Optional[MCPServerConfig]:
|
|
185
|
+
"""
|
|
186
|
+
Get existing server configuration from host.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
hostname: The MCP host to query (e.g., 'claude-desktop', 'cursor')
|
|
190
|
+
server_name: Name of the server to retrieve
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
MCPServerConfig if server exists, None otherwise
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
host_type = MCPHostType(hostname)
|
|
197
|
+
strategy = self.host_registry.get_strategy(host_type)
|
|
198
|
+
current_config = strategy.read_configuration()
|
|
199
|
+
|
|
200
|
+
if server_name in current_config.servers:
|
|
201
|
+
return current_config.servers[server_name]
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.debug(f"Failed to retrieve server config for {server_name} on {hostname}: {e}")
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def remove_server(self, server_name: str, hostname: str,
|
|
209
|
+
no_backup: bool = False) -> ConfigurationResult:
|
|
210
|
+
"""Remove MCP server from specified host."""
|
|
211
|
+
try:
|
|
212
|
+
host_type = MCPHostType(hostname)
|
|
213
|
+
strategy = self.host_registry.get_strategy(host_type)
|
|
214
|
+
|
|
215
|
+
# Read current configuration
|
|
216
|
+
current_config = strategy.read_configuration()
|
|
217
|
+
|
|
218
|
+
# Check if server exists
|
|
219
|
+
if server_name not in current_config.servers:
|
|
220
|
+
return ConfigurationResult(
|
|
221
|
+
success=False,
|
|
222
|
+
hostname=hostname,
|
|
223
|
+
server_name=server_name,
|
|
224
|
+
error_message=f"Server '{server_name}' not found in {hostname} configuration"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Create backup if requested
|
|
228
|
+
backup_path = None
|
|
229
|
+
if not no_backup and self.backup_manager:
|
|
230
|
+
config_path = strategy.get_config_path()
|
|
231
|
+
if config_path and config_path.exists():
|
|
232
|
+
backup_result = self.backup_manager.create_backup(config_path, hostname)
|
|
233
|
+
if backup_result.success:
|
|
234
|
+
backup_path = backup_result.backup_path
|
|
235
|
+
|
|
236
|
+
# Remove server from configuration
|
|
237
|
+
current_config.remove_server(server_name)
|
|
238
|
+
|
|
239
|
+
# Write updated configuration
|
|
240
|
+
success = strategy.write_configuration(current_config, no_backup=no_backup)
|
|
241
|
+
|
|
242
|
+
return ConfigurationResult(
|
|
243
|
+
success=success,
|
|
244
|
+
hostname=hostname,
|
|
245
|
+
server_name=server_name,
|
|
246
|
+
backup_created=backup_path is not None,
|
|
247
|
+
backup_path=backup_path
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
except Exception as e:
|
|
251
|
+
return ConfigurationResult(
|
|
252
|
+
success=False,
|
|
253
|
+
hostname=hostname,
|
|
254
|
+
server_name=server_name,
|
|
255
|
+
error_message=str(e)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def sync_environment_to_hosts(self, env_data: EnvironmentData,
|
|
259
|
+
target_hosts: Optional[List[str]] = None,
|
|
260
|
+
no_backup: bool = False) -> SyncResult:
|
|
261
|
+
"""Synchronize environment MCP data to host configurations."""
|
|
262
|
+
if target_hosts is None:
|
|
263
|
+
target_hosts = [host.value for host in self.host_registry.detect_available_hosts()]
|
|
264
|
+
|
|
265
|
+
results = []
|
|
266
|
+
servers_synced = 0
|
|
267
|
+
|
|
268
|
+
for hostname in target_hosts:
|
|
269
|
+
try:
|
|
270
|
+
host_type = MCPHostType(hostname)
|
|
271
|
+
strategy = self.host_registry.get_strategy(host_type)
|
|
272
|
+
|
|
273
|
+
# Collect all MCP servers for this host from environment
|
|
274
|
+
host_servers = {}
|
|
275
|
+
for package in env_data.get_mcp_packages():
|
|
276
|
+
if hostname in package.configured_hosts:
|
|
277
|
+
host_config = package.configured_hosts[hostname]
|
|
278
|
+
# Use package name as server name (single server per package)
|
|
279
|
+
host_servers[package.name] = host_config.server_config
|
|
280
|
+
|
|
281
|
+
if not host_servers:
|
|
282
|
+
# No servers to sync for this host
|
|
283
|
+
results.append(ConfigurationResult(
|
|
284
|
+
success=True,
|
|
285
|
+
hostname=hostname,
|
|
286
|
+
error_message="No servers to sync"
|
|
287
|
+
))
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
# Read current host configuration
|
|
291
|
+
current_config = strategy.read_configuration()
|
|
292
|
+
|
|
293
|
+
# Create backup if requested
|
|
294
|
+
backup_path = None
|
|
295
|
+
if not no_backup and self.backup_manager:
|
|
296
|
+
config_path = strategy.get_config_path()
|
|
297
|
+
if config_path and config_path.exists():
|
|
298
|
+
backup_result = self.backup_manager.create_backup(config_path, hostname)
|
|
299
|
+
if backup_result.success:
|
|
300
|
+
backup_path = backup_result.backup_path
|
|
301
|
+
|
|
302
|
+
# Update configuration with environment servers
|
|
303
|
+
for server_name, server_config in host_servers.items():
|
|
304
|
+
current_config.add_server(server_name, server_config)
|
|
305
|
+
servers_synced += 1
|
|
306
|
+
|
|
307
|
+
# Write updated configuration
|
|
308
|
+
success = strategy.write_configuration(current_config, no_backup=no_backup)
|
|
309
|
+
|
|
310
|
+
results.append(ConfigurationResult(
|
|
311
|
+
success=success,
|
|
312
|
+
hostname=hostname,
|
|
313
|
+
backup_created=backup_path is not None,
|
|
314
|
+
backup_path=backup_path
|
|
315
|
+
))
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
results.append(ConfigurationResult(
|
|
319
|
+
success=False,
|
|
320
|
+
hostname=hostname,
|
|
321
|
+
error_message=str(e)
|
|
322
|
+
))
|
|
323
|
+
|
|
324
|
+
# Calculate summary statistics
|
|
325
|
+
successful_results = [r for r in results if r.success]
|
|
326
|
+
hosts_updated = len(successful_results)
|
|
327
|
+
|
|
328
|
+
return SyncResult(
|
|
329
|
+
success=hosts_updated > 0,
|
|
330
|
+
results=results,
|
|
331
|
+
servers_synced=servers_synced,
|
|
332
|
+
hosts_updated=hosts_updated
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def remove_host_configuration(self, hostname: str, no_backup: bool = False) -> ConfigurationResult:
|
|
336
|
+
"""Remove entire host configuration (all MCP servers).
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
hostname (str): Host identifier
|
|
340
|
+
no_backup (bool, optional): Skip backup creation. Defaults to False.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
ConfigurationResult: Result of the removal operation
|
|
344
|
+
"""
|
|
345
|
+
try:
|
|
346
|
+
host_type = MCPHostType(hostname)
|
|
347
|
+
strategy = self.host_registry.get_strategy(host_type)
|
|
348
|
+
config_path = strategy.get_config_path()
|
|
349
|
+
|
|
350
|
+
if not config_path or not config_path.exists():
|
|
351
|
+
return ConfigurationResult(
|
|
352
|
+
success=True,
|
|
353
|
+
hostname=hostname,
|
|
354
|
+
error_message="No configuration file to remove"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# Create backup if requested
|
|
358
|
+
backup_path = None
|
|
359
|
+
if not no_backup and self.backup_manager:
|
|
360
|
+
backup_result = self.backup_manager.create_backup(config_path, hostname)
|
|
361
|
+
if backup_result.success:
|
|
362
|
+
backup_path = backup_result.backup_path
|
|
363
|
+
|
|
364
|
+
# Remove configuration
|
|
365
|
+
# Create Empty HostConfiguration
|
|
366
|
+
empty_config = HostConfiguration()
|
|
367
|
+
strategy.write_configuration(empty_config, no_backup=no_backup)
|
|
368
|
+
|
|
369
|
+
return ConfigurationResult(
|
|
370
|
+
success=True,
|
|
371
|
+
hostname=hostname,
|
|
372
|
+
backup_created=backup_path is not None,
|
|
373
|
+
backup_path=backup_path
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
except Exception as e:
|
|
377
|
+
return ConfigurationResult(
|
|
378
|
+
success=False,
|
|
379
|
+
hostname=hostname,
|
|
380
|
+
error_message=str(e)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def sync_configurations(self,
|
|
384
|
+
from_env: Optional[str] = None,
|
|
385
|
+
from_host: Optional[str] = None,
|
|
386
|
+
to_hosts: Optional[List[str]] = None,
|
|
387
|
+
servers: Optional[List[str]] = None,
|
|
388
|
+
pattern: Optional[str] = None,
|
|
389
|
+
no_backup: bool = False) -> SyncResult:
|
|
390
|
+
"""Advanced synchronization with multiple source/target options.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
from_env (str, optional): Source environment name
|
|
394
|
+
from_host (str, optional): Source host name
|
|
395
|
+
to_hosts (List[str], optional): Target host names
|
|
396
|
+
servers (List[str], optional): Specific server names to sync
|
|
397
|
+
pattern (str, optional): Regex pattern for server selection
|
|
398
|
+
no_backup (bool, optional): Skip backup creation. Defaults to False.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
SyncResult: Result of the synchronization operation
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
ValueError: If source specification is invalid
|
|
405
|
+
"""
|
|
406
|
+
import re
|
|
407
|
+
from hatch.environment_manager import HatchEnvironmentManager
|
|
408
|
+
|
|
409
|
+
# Validate source specification
|
|
410
|
+
if not from_env and not from_host:
|
|
411
|
+
raise ValueError("Must specify either from_env or from_host as source")
|
|
412
|
+
if from_env and from_host:
|
|
413
|
+
raise ValueError("Cannot specify both from_env and from_host as source")
|
|
414
|
+
|
|
415
|
+
# Default to all available hosts if no targets specified
|
|
416
|
+
if not to_hosts:
|
|
417
|
+
to_hosts = [host.value for host in self.host_registry.detect_available_hosts()]
|
|
418
|
+
|
|
419
|
+
try:
|
|
420
|
+
# Resolve source data
|
|
421
|
+
if from_env:
|
|
422
|
+
# Get environment data
|
|
423
|
+
env_manager = HatchEnvironmentManager()
|
|
424
|
+
env_data = env_manager.get_environment_data(from_env)
|
|
425
|
+
if not env_data:
|
|
426
|
+
return SyncResult(
|
|
427
|
+
success=False,
|
|
428
|
+
results=[ConfigurationResult(
|
|
429
|
+
success=False,
|
|
430
|
+
hostname="",
|
|
431
|
+
error_message=f"Environment '{from_env}' not found"
|
|
432
|
+
)],
|
|
433
|
+
servers_synced=0,
|
|
434
|
+
hosts_updated=0
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Extract servers from environment
|
|
438
|
+
source_servers = {}
|
|
439
|
+
for package in env_data.get_mcp_packages():
|
|
440
|
+
# Use package name as server name (single server per package)
|
|
441
|
+
source_servers[package.name] = package.configured_hosts
|
|
442
|
+
|
|
443
|
+
else: # from_host
|
|
444
|
+
# Read host configuration
|
|
445
|
+
try:
|
|
446
|
+
host_type = MCPHostType(from_host)
|
|
447
|
+
strategy = self.host_registry.get_strategy(host_type)
|
|
448
|
+
host_config = strategy.read_configuration()
|
|
449
|
+
|
|
450
|
+
# Extract servers from host configuration
|
|
451
|
+
source_servers = {}
|
|
452
|
+
for server_name, server_config in host_config.servers.items():
|
|
453
|
+
source_servers[server_name] = {
|
|
454
|
+
from_host: {"server_config": server_config}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
except ValueError:
|
|
458
|
+
return SyncResult(
|
|
459
|
+
success=False,
|
|
460
|
+
results=[ConfigurationResult(
|
|
461
|
+
success=False,
|
|
462
|
+
hostname="",
|
|
463
|
+
error_message=f"Invalid source host '{from_host}'"
|
|
464
|
+
)],
|
|
465
|
+
servers_synced=0,
|
|
466
|
+
hosts_updated=0
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Apply server filtering
|
|
470
|
+
if servers:
|
|
471
|
+
# Filter by specific server names
|
|
472
|
+
filtered_servers = {name: config for name, config in source_servers.items()
|
|
473
|
+
if name in servers}
|
|
474
|
+
source_servers = filtered_servers
|
|
475
|
+
elif pattern:
|
|
476
|
+
# Filter by regex pattern
|
|
477
|
+
regex = re.compile(pattern)
|
|
478
|
+
filtered_servers = {name: config for name, config in source_servers.items()
|
|
479
|
+
if regex.match(name)}
|
|
480
|
+
source_servers = filtered_servers
|
|
481
|
+
|
|
482
|
+
# Apply synchronization to target hosts
|
|
483
|
+
results = []
|
|
484
|
+
servers_synced = 0
|
|
485
|
+
|
|
486
|
+
for target_host in to_hosts:
|
|
487
|
+
try:
|
|
488
|
+
host_type = MCPHostType(target_host)
|
|
489
|
+
strategy = self.host_registry.get_strategy(host_type)
|
|
490
|
+
|
|
491
|
+
# Read current target configuration
|
|
492
|
+
current_config = strategy.read_configuration()
|
|
493
|
+
|
|
494
|
+
# Create backup if requested
|
|
495
|
+
backup_path = None
|
|
496
|
+
if not no_backup and self.backup_manager:
|
|
497
|
+
config_path = strategy.get_config_path()
|
|
498
|
+
if config_path and config_path.exists():
|
|
499
|
+
backup_result = self.backup_manager.create_backup(config_path, target_host)
|
|
500
|
+
if backup_result.success:
|
|
501
|
+
backup_path = backup_result.backup_path
|
|
502
|
+
|
|
503
|
+
# Add servers to target configuration
|
|
504
|
+
host_servers_added = 0
|
|
505
|
+
for server_name, server_hosts in source_servers.items():
|
|
506
|
+
# Find appropriate server config for this target host
|
|
507
|
+
server_config = None
|
|
508
|
+
|
|
509
|
+
if from_env:
|
|
510
|
+
# For environment source, look for host-specific config
|
|
511
|
+
if target_host in server_hosts:
|
|
512
|
+
server_config = server_hosts[target_host]["server_config"]
|
|
513
|
+
elif "claude-desktop" in server_hosts:
|
|
514
|
+
# Fallback to claude-desktop config for compatibility
|
|
515
|
+
server_config = server_hosts["claude-desktop"]["server_config"]
|
|
516
|
+
else:
|
|
517
|
+
# For host source, use the server config directly
|
|
518
|
+
if from_host in server_hosts:
|
|
519
|
+
server_config = server_hosts[from_host]["server_config"]
|
|
520
|
+
|
|
521
|
+
if server_config:
|
|
522
|
+
current_config.add_server(server_name, server_config)
|
|
523
|
+
host_servers_added += 1
|
|
524
|
+
|
|
525
|
+
# Write updated configuration
|
|
526
|
+
success = strategy.write_configuration(current_config, no_backup=no_backup)
|
|
527
|
+
|
|
528
|
+
results.append(ConfigurationResult(
|
|
529
|
+
success=success,
|
|
530
|
+
hostname=target_host,
|
|
531
|
+
backup_created=backup_path is not None,
|
|
532
|
+
backup_path=backup_path
|
|
533
|
+
))
|
|
534
|
+
|
|
535
|
+
if success:
|
|
536
|
+
servers_synced += host_servers_added
|
|
537
|
+
|
|
538
|
+
except ValueError:
|
|
539
|
+
results.append(ConfigurationResult(
|
|
540
|
+
success=False,
|
|
541
|
+
hostname=target_host,
|
|
542
|
+
error_message=f"Invalid target host '{target_host}'"
|
|
543
|
+
))
|
|
544
|
+
except Exception as e:
|
|
545
|
+
results.append(ConfigurationResult(
|
|
546
|
+
success=False,
|
|
547
|
+
hostname=target_host,
|
|
548
|
+
error_message=str(e)
|
|
549
|
+
))
|
|
550
|
+
|
|
551
|
+
# Calculate summary statistics
|
|
552
|
+
successful_results = [r for r in results if r.success]
|
|
553
|
+
hosts_updated = len(successful_results)
|
|
554
|
+
|
|
555
|
+
return SyncResult(
|
|
556
|
+
success=hosts_updated > 0,
|
|
557
|
+
results=results,
|
|
558
|
+
servers_synced=servers_synced,
|
|
559
|
+
hosts_updated=hosts_updated
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
except Exception as e:
|
|
563
|
+
return SyncResult(
|
|
564
|
+
success=False,
|
|
565
|
+
results=[ConfigurationResult(
|
|
566
|
+
success=False,
|
|
567
|
+
hostname="",
|
|
568
|
+
error_message=f"Synchronization failed: {str(e)}"
|
|
569
|
+
)],
|
|
570
|
+
servers_synced=0,
|
|
571
|
+
hosts_updated=0
|
|
572
|
+
)
|