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.
Files changed (93) hide show
  1. hatch/__init__.py +21 -0
  2. hatch/cli_hatch.py +2748 -0
  3. hatch/environment_manager.py +1375 -0
  4. hatch/installers/__init__.py +25 -0
  5. hatch/installers/dependency_installation_orchestrator.py +636 -0
  6. hatch/installers/docker_installer.py +545 -0
  7. hatch/installers/hatch_installer.py +198 -0
  8. hatch/installers/installation_context.py +109 -0
  9. hatch/installers/installer_base.py +195 -0
  10. hatch/installers/python_installer.py +342 -0
  11. hatch/installers/registry.py +179 -0
  12. hatch/installers/system_installer.py +588 -0
  13. hatch/mcp_host_config/__init__.py +38 -0
  14. hatch/mcp_host_config/backup.py +458 -0
  15. hatch/mcp_host_config/host_management.py +572 -0
  16. hatch/mcp_host_config/models.py +602 -0
  17. hatch/mcp_host_config/reporting.py +181 -0
  18. hatch/mcp_host_config/strategies.py +513 -0
  19. hatch/package_loader.py +263 -0
  20. hatch/python_environment_manager.py +734 -0
  21. hatch/registry_explorer.py +171 -0
  22. hatch/registry_retriever.py +335 -0
  23. hatch/template_generator.py +179 -0
  24. hatch_xclam-0.7.0.dist-info/METADATA +150 -0
  25. hatch_xclam-0.7.0.dist-info/RECORD +93 -0
  26. hatch_xclam-0.7.0.dist-info/WHEEL +5 -0
  27. hatch_xclam-0.7.0.dist-info/entry_points.txt +2 -0
  28. hatch_xclam-0.7.0.dist-info/licenses/LICENSE +661 -0
  29. hatch_xclam-0.7.0.dist-info/top_level.txt +2 -0
  30. tests/__init__.py +1 -0
  31. tests/run_environment_tests.py +124 -0
  32. tests/test_cli_version.py +122 -0
  33. tests/test_data/packages/basic/base_pkg/hatch_mcp_server.py +18 -0
  34. tests/test_data/packages/basic/base_pkg/mcp_server.py +21 -0
  35. tests/test_data/packages/basic/base_pkg_v2/hatch_mcp_server.py +18 -0
  36. tests/test_data/packages/basic/base_pkg_v2/mcp_server.py +21 -0
  37. tests/test_data/packages/basic/utility_pkg/hatch_mcp_server.py +18 -0
  38. tests/test_data/packages/basic/utility_pkg/mcp_server.py +21 -0
  39. tests/test_data/packages/dependencies/complex_dep_pkg/hatch_mcp_server.py +18 -0
  40. tests/test_data/packages/dependencies/complex_dep_pkg/mcp_server.py +21 -0
  41. tests/test_data/packages/dependencies/docker_dep_pkg/hatch_mcp_server.py +18 -0
  42. tests/test_data/packages/dependencies/docker_dep_pkg/mcp_server.py +21 -0
  43. tests/test_data/packages/dependencies/mixed_dep_pkg/hatch_mcp_server.py +18 -0
  44. tests/test_data/packages/dependencies/mixed_dep_pkg/mcp_server.py +21 -0
  45. tests/test_data/packages/dependencies/python_dep_pkg/hatch_mcp_server.py +18 -0
  46. tests/test_data/packages/dependencies/python_dep_pkg/mcp_server.py +21 -0
  47. tests/test_data/packages/dependencies/simple_dep_pkg/hatch_mcp_server.py +18 -0
  48. tests/test_data/packages/dependencies/simple_dep_pkg/mcp_server.py +21 -0
  49. tests/test_data/packages/dependencies/system_dep_pkg/hatch_mcp_server.py +18 -0
  50. tests/test_data/packages/dependencies/system_dep_pkg/mcp_server.py +21 -0
  51. tests/test_data/packages/error_scenarios/circular_dep_pkg/hatch_mcp_server.py +18 -0
  52. tests/test_data/packages/error_scenarios/circular_dep_pkg/mcp_server.py +21 -0
  53. tests/test_data/packages/error_scenarios/circular_dep_pkg_b/hatch_mcp_server.py +18 -0
  54. tests/test_data/packages/error_scenarios/circular_dep_pkg_b/mcp_server.py +21 -0
  55. tests/test_data/packages/error_scenarios/invalid_dep_pkg/hatch_mcp_server.py +18 -0
  56. tests/test_data/packages/error_scenarios/invalid_dep_pkg/mcp_server.py +21 -0
  57. tests/test_data/packages/error_scenarios/version_conflict_pkg/hatch_mcp_server.py +18 -0
  58. tests/test_data/packages/error_scenarios/version_conflict_pkg/mcp_server.py +21 -0
  59. tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py +11 -0
  60. tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py +11 -0
  61. tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py +18 -0
  62. tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py +21 -0
  63. tests/test_data_utils.py +472 -0
  64. tests/test_dependency_orchestrator_consent.py +266 -0
  65. tests/test_docker_installer.py +524 -0
  66. tests/test_env_manip.py +991 -0
  67. tests/test_hatch_installer.py +179 -0
  68. tests/test_installer_base.py +221 -0
  69. tests/test_mcp_atomic_operations.py +276 -0
  70. tests/test_mcp_backup_integration.py +308 -0
  71. tests/test_mcp_cli_all_host_specific_args.py +303 -0
  72. tests/test_mcp_cli_backup_management.py +295 -0
  73. tests/test_mcp_cli_direct_management.py +453 -0
  74. tests/test_mcp_cli_discovery_listing.py +582 -0
  75. tests/test_mcp_cli_host_config_integration.py +823 -0
  76. tests/test_mcp_cli_package_management.py +360 -0
  77. tests/test_mcp_cli_partial_updates.py +859 -0
  78. tests/test_mcp_environment_integration.py +520 -0
  79. tests/test_mcp_host_config_backup.py +257 -0
  80. tests/test_mcp_host_configuration_manager.py +331 -0
  81. tests/test_mcp_host_registry_decorator.py +348 -0
  82. tests/test_mcp_pydantic_architecture_v4.py +603 -0
  83. tests/test_mcp_server_config_models.py +242 -0
  84. tests/test_mcp_server_config_type_field.py +221 -0
  85. tests/test_mcp_sync_functionality.py +316 -0
  86. tests/test_mcp_user_feedback_reporting.py +359 -0
  87. tests/test_non_tty_integration.py +281 -0
  88. tests/test_online_package_loader.py +202 -0
  89. tests/test_python_environment_manager.py +882 -0
  90. tests/test_python_installer.py +327 -0
  91. tests/test_registry.py +51 -0
  92. tests/test_registry_retriever.py +250 -0
  93. 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
+ )