Skip to content

Commit 9ed6ec6

Browse files
author
LittleCoinCoin
committed
feat(mcp): implement advanced synchronization backend
Add sync_configurations method to MCPHostConfigurationManager with: - Cross-environment synchronization (--from-env to --to-host) - Host-to-host synchronization (--from-host to --to-host) - Server filtering by name list and regex pattern - Multi-host support with atomic operations - Comprehensive error handling and validation Key features: - Source resolution for environment data and host configurations - Server filtering with mutually exclusive options - Backup integration for safe operations - Detailed result reporting with per-host status - Proper ConfigurationResult validation with required hostname field Implements Phase 3f backend requirements following architectural analysis. Maintains backward compatibility with existing sync_environment_to_hosts.
1 parent 969c793 commit 9ed6ec6

File tree

1 file changed

+191
-0
lines changed

1 file changed

+191
-0
lines changed

hatch/mcp_host_config/host_management.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,194 @@ def remove_host_configuration(self, hostname: str, no_backup: bool = False) -> C
353353
hostname=hostname,
354354
error_message=str(e)
355355
)
356+
357+
def sync_configurations(self,
358+
from_env: Optional[str] = None,
359+
from_host: Optional[str] = None,
360+
to_hosts: Optional[List[str]] = None,
361+
servers: Optional[List[str]] = None,
362+
pattern: Optional[str] = None,
363+
no_backup: bool = False) -> SyncResult:
364+
"""Advanced synchronization with multiple source/target options.
365+
366+
Args:
367+
from_env (str, optional): Source environment name
368+
from_host (str, optional): Source host name
369+
to_hosts (List[str], optional): Target host names
370+
servers (List[str], optional): Specific server names to sync
371+
pattern (str, optional): Regex pattern for server selection
372+
no_backup (bool, optional): Skip backup creation. Defaults to False.
373+
374+
Returns:
375+
SyncResult: Result of the synchronization operation
376+
377+
Raises:
378+
ValueError: If source specification is invalid
379+
"""
380+
import re
381+
from hatch.environment_manager import HatchEnvironmentManager
382+
383+
# Validate source specification
384+
if not from_env and not from_host:
385+
raise ValueError("Must specify either from_env or from_host as source")
386+
if from_env and from_host:
387+
raise ValueError("Cannot specify both from_env and from_host as source")
388+
389+
# Default to all available hosts if no targets specified
390+
if not to_hosts:
391+
to_hosts = [host.value for host in self.host_registry.detect_available_hosts()]
392+
393+
try:
394+
# Resolve source data
395+
if from_env:
396+
# Get environment data
397+
env_manager = HatchEnvironmentManager()
398+
env_data = env_manager.get_environment_data(from_env)
399+
if not env_data:
400+
return SyncResult(
401+
success=False,
402+
results=[ConfigurationResult(
403+
success=False,
404+
hostname="",
405+
error_message=f"Environment '{from_env}' not found"
406+
)],
407+
servers_synced=0,
408+
hosts_updated=0
409+
)
410+
411+
# Extract servers from environment
412+
source_servers = {}
413+
for package in env_data.get_mcp_packages():
414+
# Use package name as server name (single server per package)
415+
source_servers[package.name] = package.configured_hosts
416+
417+
else: # from_host
418+
# Read host configuration
419+
try:
420+
host_type = MCPHostType(from_host)
421+
strategy = self.host_registry.get_strategy(host_type)
422+
host_config = strategy.read_configuration()
423+
424+
# Extract servers from host configuration
425+
source_servers = {}
426+
for server_name, server_config in host_config.servers.items():
427+
source_servers[server_name] = {
428+
from_host: {"server_config": server_config}
429+
}
430+
431+
except ValueError:
432+
return SyncResult(
433+
success=False,
434+
results=[ConfigurationResult(
435+
success=False,
436+
hostname="",
437+
error_message=f"Invalid source host '{from_host}'"
438+
)],
439+
servers_synced=0,
440+
hosts_updated=0
441+
)
442+
443+
# Apply server filtering
444+
if servers:
445+
# Filter by specific server names
446+
filtered_servers = {name: config for name, config in source_servers.items()
447+
if name in servers}
448+
source_servers = filtered_servers
449+
elif pattern:
450+
# Filter by regex pattern
451+
regex = re.compile(pattern)
452+
filtered_servers = {name: config for name, config in source_servers.items()
453+
if regex.match(name)}
454+
source_servers = filtered_servers
455+
456+
# Apply synchronization to target hosts
457+
results = []
458+
servers_synced = 0
459+
460+
for target_host in to_hosts:
461+
try:
462+
host_type = MCPHostType(target_host)
463+
strategy = self.host_registry.get_strategy(host_type)
464+
465+
# Read current target configuration
466+
current_config = strategy.read_configuration()
467+
468+
# Create backup if requested
469+
backup_path = None
470+
if not no_backup and self.backup_manager:
471+
config_path = strategy.get_config_path()
472+
if config_path and config_path.exists():
473+
backup_result = self.backup_manager.create_backup(config_path, target_host)
474+
if backup_result.success:
475+
backup_path = backup_result.backup_path
476+
477+
# Add servers to target configuration
478+
host_servers_added = 0
479+
for server_name, server_hosts in source_servers.items():
480+
# Find appropriate server config for this target host
481+
server_config = None
482+
483+
if from_env:
484+
# For environment source, look for host-specific config
485+
if target_host in server_hosts:
486+
server_config = server_hosts[target_host]["server_config"]
487+
elif "claude-desktop" in server_hosts:
488+
# Fallback to claude-desktop config for compatibility
489+
server_config = server_hosts["claude-desktop"]["server_config"]
490+
else:
491+
# For host source, use the server config directly
492+
if from_host in server_hosts:
493+
server_config = server_hosts[from_host]["server_config"]
494+
495+
if server_config:
496+
current_config.add_server(server_name, server_config)
497+
host_servers_added += 1
498+
499+
# Write updated configuration
500+
success = strategy.write_configuration(current_config, no_backup=no_backup)
501+
502+
results.append(ConfigurationResult(
503+
success=success,
504+
hostname=target_host,
505+
backup_created=backup_path is not None,
506+
backup_path=backup_path
507+
))
508+
509+
if success:
510+
servers_synced += host_servers_added
511+
512+
except ValueError:
513+
results.append(ConfigurationResult(
514+
success=False,
515+
hostname=target_host,
516+
error_message=f"Invalid target host '{target_host}'"
517+
))
518+
except Exception as e:
519+
results.append(ConfigurationResult(
520+
success=False,
521+
hostname=target_host,
522+
error_message=str(e)
523+
))
524+
525+
# Calculate summary statistics
526+
successful_results = [r for r in results if r.success]
527+
hosts_updated = len(successful_results)
528+
529+
return SyncResult(
530+
success=hosts_updated > 0,
531+
results=results,
532+
servers_synced=servers_synced,
533+
hosts_updated=hosts_updated
534+
)
535+
536+
except Exception as e:
537+
return SyncResult(
538+
success=False,
539+
results=[ConfigurationResult(
540+
success=False,
541+
hostname="",
542+
error_message=f"Synchronization failed: {str(e)}"
543+
)],
544+
servers_synced=0,
545+
hosts_updated=0
546+
)

0 commit comments

Comments
 (0)