diff --git a/setup/components/mcp.py b/setup/components/mcp.py index 4a3bc52..211d242 100644 --- a/setup/components/mcp.py +++ b/setup/components/mcp.py @@ -1,473 +1,498 @@ """ -MCP component for MCP server configuration via .claude.json +MCP component for MCP server integration """ -import json -import shutil -import time +import subprocess import sys from typing import Dict, List, Tuple, Optional, Any from pathlib import Path -# Platform-specific file locking imports -try: - if sys.platform == "win32": - import msvcrt - LOCKING_AVAILABLE = "windows" - else: - import fcntl - LOCKING_AVAILABLE = "unix" -except ImportError: - LOCKING_AVAILABLE = None - -from ..core.base import Component -from setup import __version__ +from ..base.component import Component from ..utils.ui import display_info, display_warning - +from setup import __version__ class MCPComponent(Component): - """MCP servers configuration component""" + """MCP servers integration component""" def __init__(self, install_dir: Optional[Path] = None): """Initialize MCP component""" super().__init__(install_dir) - # Define MCP servers available for configuration + # Define MCP servers to install self.mcp_servers = { - "context7": { - "name": "context7", - "description": "Official library documentation and code examples", - "config_file": "context7.json", - "requires_api_key": False - }, - "sequential": { - "name": "sequential-thinking", + "sequential-thinking": { + "name": "sequential-thinking", "description": "Multi-step problem solving and systematic analysis", - "config_file": "sequential.json", - "requires_api_key": False + "npm_package": "@modelcontextprotocol/server-sequential-thinking", + "required": True + }, + "context7": { + "name": "context7", + "description": "Official library documentation and code examples", + "npm_package": "@upstash/context7-mcp", + "required": True }, "magic": { "name": "magic", "description": "Modern UI component generation and design systems", - "config_file": "magic.json", - "requires_api_key": True, - "api_key_env": "TWENTYFIRST_API_KEY" + "npm_package": "@21st-dev/magic", + "required": False, + "api_key_env": "TWENTYFIRST_API_KEY", + "api_key_description": "21st.dev API key for UI component generation" }, "playwright": { "name": "playwright", "description": "Cross-browser E2E testing and automation", - "config_file": "playwright.json", - "requires_api_key": False - }, - "serena": { - "name": "serena", - "description": "Semantic code analysis and intelligent editing", - "config_file": "serena.json", - "requires_api_key": False - }, - "morphllm": { - "name": "morphllm-fast-apply", - "description": "Fast Apply capability for context-aware code modifications", - "config_file": "morphllm.json", - "requires_api_key": True, - "api_key_env": "MORPH_API_KEY" + "npm_package": "@playwright/mcp@latest", + "required": False } } - - # This will be set during installation - initialize as empty list - self.selected_servers: List[str] = [] - - # Store collected API keys for configuration - self.collected_api_keys: Dict[str, str] = {} - - def _lock_file(self, file_handle, exclusive: bool = False): - """Cross-platform file locking""" - if LOCKING_AVAILABLE == "unix": - lock_type = fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH - fcntl.flock(file_handle.fileno(), lock_type) - elif LOCKING_AVAILABLE == "windows": - # Windows locking using msvcrt - if exclusive: - msvcrt.locking(file_handle.fileno(), msvcrt.LK_LOCK, 1) - # If no locking available, continue without locking - - def _unlock_file(self, file_handle): - """Cross-platform file unlocking""" - if LOCKING_AVAILABLE == "unix": - fcntl.flock(file_handle.fileno(), fcntl.LOCK_UN) - elif LOCKING_AVAILABLE == "windows": - msvcrt.locking(file_handle.fileno(), msvcrt.LK_UNLCK, 1) - # If no locking available, continue without unlocking def get_metadata(self) -> Dict[str, str]: """Get component metadata""" return { "name": "mcp", "version": __version__, - "description": "MCP server configuration management via .claude.json", + "description": "MCP server integration (Context7, Sequential, Magic, Playwright)", "category": "integration" } - def set_selected_servers(self, selected_servers: List[str]) -> None: - """Set which MCP servers were selected for configuration""" - self.selected_servers = selected_servers - self.logger.debug(f"MCP servers to configure: {selected_servers}") - def validate_prerequisites(self, installSubPath: Optional[Path] = None) -> Tuple[bool, List[str]]: - """ - Check prerequisites for MCP component - """ + """Check prerequisites""" errors = [] - # Check if config source directory exists - source_dir = self._get_config_source_dir() - if not source_dir or not source_dir.exists(): - errors.append(f"MCP config source directory not found: {source_dir}") - return False, errors + # Check if Node.js is available + try: + result = subprocess.run( + ["node", "--version"], + capture_output=True, + text=True, + timeout=10, + shell=(sys.platform == "win32") + ) + if result.returncode != 0: + errors.append("Node.js not found - required for MCP servers") + else: + version = result.stdout.strip() + self.logger.debug(f"Found Node.js {version}") + + # Check version (require 18+) + try: + version_num = int(version.lstrip('v').split('.')[0]) + if version_num < 18: + errors.append(f"Node.js version {version} found, but version 18+ required") + except: + self.logger.warning(f"Could not parse Node.js version: {version}") + except (subprocess.TimeoutExpired, FileNotFoundError): + errors.append("Node.js not found - required for MCP servers") - # Check if user's Claude config exists - claude_config = Path.home() / ".claude.json" - if not claude_config.exists(): - errors.append(f"Claude configuration file not found: {claude_config}") - errors.append("Please run Claude Code at least once to create the configuration file") + # Check if Claude CLI is available + try: + result = subprocess.run( + ["claude", "--version"], + capture_output=True, + text=True, + timeout=10, + shell=(sys.platform == "win32") + ) + if result.returncode != 0: + errors.append("Claude CLI not found - required for MCP server management") + else: + version = result.stdout.strip() + self.logger.debug(f"Found Claude CLI {version}") + except (subprocess.TimeoutExpired, FileNotFoundError): + errors.append("Claude CLI not found - required for MCP server management") + + # Check if npm is available + try: + result = subprocess.run( + ["npm", "--version"], + capture_output=True, + text=True, + timeout=10, + shell=(sys.platform == "win32") + ) + if result.returncode != 0: + errors.append("npm not found - required for MCP server installation") + else: + version = result.stdout.strip() + self.logger.debug(f"Found npm {version}") + except (subprocess.TimeoutExpired, FileNotFoundError): + errors.append("npm not found - required for MCP server installation") return len(errors) == 0, errors def get_files_to_install(self) -> List[Tuple[Path, Path]]: - """MCP component doesn't install files - it modifies .claude.json""" + """Get files to install (none for MCP component)""" return [] - def _get_config_source_dir(self) -> Optional[Path]: - """Get source directory for MCP config files""" - project_root = Path(__file__).parent.parent.parent - config_dir = project_root / "SuperClaude" / "MCP" / "configs" - - if not config_dir.exists(): - return None - - return config_dir + def get_metadata_modifications(self) -> Dict[str, Any]: + """Get metadata modifications for MCP component""" + return { + "components": { + "mcp": { + "version": "3.0.0", + "installed": True, + "servers_count": len(self.mcp_servers) + } + }, + "mcp": { + "enabled": True, + "servers": list(self.mcp_servers.keys()), + "auto_update": False + } + } - def _get_source_dir(self) -> Optional[Path]: - """Override parent method - MCP component doesn't use traditional file installation""" - return self._get_config_source_dir() - - def _load_claude_config(self) -> Tuple[Optional[Dict], Path]: - """Load user's Claude configuration with file locking""" - claude_config_path = Path.home() / ".claude.json" - + def _check_mcp_server_installed(self, server_name: str) -> bool: + """Check if MCP server is already installed""" try: - with open(claude_config_path, 'r') as f: - # Apply shared lock for reading - self._lock_file(f, exclusive=False) - try: - config = json.load(f) - return config, claude_config_path - finally: - self._unlock_file(f) - except Exception as e: - self.logger.error(f"Failed to load Claude config: {e}") - return None, claude_config_path - - def _save_claude_config(self, config: Dict, config_path: Path) -> bool: - """Save user's Claude configuration with backup and file locking""" - max_retries = 3 - retry_delay = 0.1 - - for attempt in range(max_retries): - try: - # Create backup first - if config_path.exists(): - backup_path = config_path.with_suffix('.json.backup') - shutil.copy2(config_path, backup_path) - self.logger.debug(f"Created backup: {backup_path}") - - # Save updated config with exclusive lock - with open(config_path, 'w') as f: - # Apply exclusive lock for writing - self._lock_file(f, exclusive=True) - try: - json.dump(config, f, indent=2) - f.flush() # Ensure data is written - finally: - self._unlock_file(f) - - self.logger.debug("Updated Claude configuration") - return True - - except (OSError, IOError) as e: - if attempt < max_retries - 1: - self.logger.warning(f"File lock attempt {attempt + 1} failed, retrying: {e}") - time.sleep(retry_delay * (2 ** attempt)) # Exponential backoff - continue - else: - self.logger.error(f"Failed to save Claude config after {max_retries} attempts: {e}") - return False - except Exception as e: - self.logger.error(f"Failed to save Claude config: {e}") + result = subprocess.run( + ["claude", "mcp", "list"], + capture_output=True, + text=True, + timeout=15, + shell=(sys.platform == "win32") + ) + + if result.returncode != 0: + self.logger.warning(f"Could not list MCP servers: {result.stderr}") return False - - return False + + # Parse output to check if server is installed + output = result.stdout.lower() + return server_name.lower() in output + + except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e: + self.logger.warning(f"Error checking MCP server status: {e}") + return False - def _merge_mcp_server_config(self, existing_config: Dict, new_config: Dict, server_key: str) -> None: - """Precisely merge MCP server config, preserving user customizations + def _install_mcp_server(self, server_info: Dict[str, Any], config: Dict[str, Any]) -> bool: + """Install a single MCP server""" + server_name = server_info["name"] + npm_package = server_info["npm_package"] - Args: - existing_config: User's current mcpServers configuration - new_config: New MCP server configuration to merge - server_key: Server key for logging purposes - """ - for server_name, server_def in new_config.items(): - if server_name in existing_config: - # Server already exists - preserve user customizations - existing_server = existing_config[server_name] - - # Only add missing keys, never overwrite existing ones - for key, value in server_def.items(): - if key not in existing_server: - existing_server[key] = value - self.logger.debug(f"Added missing key '{key}' to existing server '{server_name}'") - else: - self.logger.debug(f"Preserved user customization for '{server_name}.{key}'") - - # NEW: Apply environment variable references for API keys - if "env" in existing_server and self.collected_api_keys: - for env_key, env_value in existing_server["env"].items(): - if env_key in self.collected_api_keys and env_value == "": - # Update to use environment variable reference - existing_server["env"][env_key] = f"${{{env_key}}}" - self.logger.info(f"Configured {env_key} to use environment variable") - - self.logger.info(f"Updated existing MCP server '{server_name}' (preserved user customizations)") - else: - # New server - add complete configuration - # Apply environment variable references if we have collected keys - if "env" in server_def and self.collected_api_keys: - for env_key in server_def["env"]: - if env_key in self.collected_api_keys and server_def["env"][env_key] == "": - server_def["env"][env_key] = f"${{{env_key}}}" - - existing_config[server_name] = server_def - self.logger.info(f"Added new MCP server '{server_name}' from {server_key}") - - def _load_mcp_server_config(self, server_key: str) -> Optional[Dict]: - """Load MCP server configuration snippet""" - if server_key not in self.mcp_servers: - return None - - server_info = self.mcp_servers[server_key] - config_file = server_info["config_file"] - config_source_dir = self._get_config_source_dir() - - if not config_source_dir: - return None - - config_path = config_source_dir / config_file + command = "npx" try: - with open(config_path, 'r') as f: - return json.load(f) + self.logger.info(f"Installing MCP server: {server_name}") + + # Check if already installed + if self._check_mcp_server_installed(server_name): + self.logger.info(f"MCP server {server_name} already installed") + return True + + # Handle API key requirements + if "api_key_env" in server_info: + api_key_env = server_info["api_key_env"] + api_key_desc = server_info.get("api_key_description", f"API key for {server_name}") + + if not config.get("dry_run", False): + display_info(f"MCP server '{server_name}' requires an API key") + display_info(f"Environment variable: {api_key_env}") + display_info(f"Description: {api_key_desc}") + + # Check if API key is already set + import os + if not os.getenv(api_key_env): + display_warning(f"API key {api_key_env} not found in environment") + self.logger.warning(f"Proceeding without {api_key_env} - server may not function properly") + + # Install using Claude CLI + if config.get("dry_run"): + self.logger.info(f"Would install MCP server (user scope): claude mcp add -s user {server_name} {command} -y {npm_package}") + return True + + self.logger.debug(f"Running: claude mcp add -s user {server_name} {command} -y {npm_package}") + + result = subprocess.run( + ["claude", "mcp", "add", "-s", "user", "--", server_name, command, "-y", npm_package], + capture_output=True, + text=True, + timeout=120, # 2 minutes timeout for installation + shell=(sys.platform == "win32") + ) + + if result.returncode == 0: + self.logger.success(f"Successfully installed MCP server (user scope): {server_name}") + return True + else: + error_msg = result.stderr.strip() if result.stderr else "Unknown error" + self.logger.error(f"Failed to install MCP server {server_name}: {error_msg}") + return False + + except subprocess.TimeoutExpired: + self.logger.error(f"Timeout installing MCP server {server_name}") + return False except Exception as e: - self.logger.error(f"Failed to load MCP config for {server_key}: {e}") - return None + self.logger.error(f"Error installing MCP server {server_name}: {e}") + return False + + def _uninstall_mcp_server(self, server_name: str) -> bool: + """Uninstall a single MCP server""" + try: + self.logger.info(f"Uninstalling MCP server: {server_name}") + + # Check if installed + if not self._check_mcp_server_installed(server_name): + self.logger.info(f"MCP server {server_name} not installed") + return True + + self.logger.debug(f"Running: claude mcp remove {server_name} (auto-detect scope)") + + result = subprocess.run( + ["claude", "mcp", "remove", server_name], + capture_output=True, + text=True, + timeout=60, + shell=(sys.platform == "win32") + ) + + if result.returncode == 0: + self.logger.success(f"Successfully uninstalled MCP server: {server_name}") + return True + else: + error_msg = result.stderr.strip() if result.stderr else "Unknown error" + self.logger.error(f"Failed to uninstall MCP server {server_name}: {error_msg}") + return False + + except subprocess.TimeoutExpired: + self.logger.error(f"Timeout uninstalling MCP server {server_name}") + return False + except Exception as e: + self.logger.error(f"Error uninstalling MCP server {server_name}: {e}") + return False def _install(self, config: Dict[str, Any]) -> bool: - """Install MCP component by configuring .claude.json""" - self.logger.info("Configuring MCP servers in Claude...") - - # Get selected servers from config - selected_servers = config.get("selected_mcp_servers", []) - if not selected_servers: - self.logger.info("No MCP servers selected - skipping MCP configuration") - return True - - self.set_selected_servers(selected_servers) - - # NEW: Log collected API keys information - if hasattr(self, 'collected_api_keys') and self.collected_api_keys: - self.logger.info(f"Using {len(self.collected_api_keys)} collected API keys for configuration") - + """Install MCP component""" + self.logger.info("Installing SuperClaude MCP servers...") + # Validate prerequisites success, errors = self.validate_prerequisites() if not success: for error in errors: self.logger.error(error) return False - - # Load Claude configuration - claude_config, config_path = self._load_claude_config() - if claude_config is None: - return False - - # Ensure mcpServers section exists - if "mcpServers" not in claude_config: - claude_config["mcpServers"] = {} - - # Configure each selected server - configured_count = 0 - for server_key in selected_servers: - if server_key not in self.mcp_servers: - self.logger.warning(f"Unknown MCP server: {server_key}") - continue - - server_info = self.mcp_servers[server_key] - server_config = self._load_mcp_server_config(server_key) - - if server_config is None: - self.logger.error(f"Failed to load configuration for {server_key}") - continue - - # Handle API key requirements - if server_info.get("requires_api_key", False): - api_key_env = server_info.get("api_key_env") - if api_key_env: - display_info(f"Server '{server_key}' requires API key: {api_key_env}") - display_info("You can set this environment variable later") - - # Precisely merge server config, preserving user customizations - self._merge_mcp_server_config(claude_config["mcpServers"], server_config, server_key) - configured_count += 1 - - self.logger.info(f"Configured MCP server: {server_info['name']}") - - if configured_count == 0: - self.logger.error("No MCP servers were successfully configured") - return False - - # Save updated configuration - success = self._save_claude_config(claude_config, config_path) - - if success: - self.logger.success(f"Successfully configured {configured_count} MCP servers") - return self._post_install() + + # Install each MCP server + installed_count = 0 + failed_servers = [] + + for server_name, server_info in self.mcp_servers.items(): + if self._install_mcp_server(server_info, config): + installed_count += 1 + else: + failed_servers.append(server_name) + + # Check if this is a required server + if server_info.get("required", False): + self.logger.error(f"Required MCP server {server_name} failed to install") + return False + + # Verify installation + if not config.get("dry_run", False): + self.logger.info("Verifying MCP server installation...") + try: + result = subprocess.run( + ["claude", "mcp", "list"], + capture_output=True, + text=True, + timeout=15, + shell=(sys.platform == "win32") + ) + + if result.returncode == 0: + self.logger.debug("MCP servers list:") + for line in result.stdout.strip().split('\n'): + if line.strip(): + self.logger.debug(f" {line.strip()}") + else: + self.logger.warning("Could not verify MCP server installation") + + except Exception as e: + self.logger.warning(f"Could not verify MCP installation: {e}") + + if failed_servers: + self.logger.warning(f"Some MCP servers failed to install: {failed_servers}") + self.logger.success(f"MCP component partially installed ({installed_count} servers)") else: - return False - + self.logger.success(f"MCP component installed successfully ({installed_count} servers)") + + return self._post_install() + def _post_install(self) -> bool: - """Post-installation tasks""" + # Update metadata try: - # Update metadata - metadata_mods = { - "components": { - "mcp": { - "version": __version__, - "installed": True, - "servers_configured": len(self.selected_servers), - "configured_servers": self.selected_servers - } - } - } + metadata_mods = self.get_metadata_modifications() self.settings_manager.update_metadata(metadata_mods) + + # Add component registration to metadata + self.settings_manager.add_component_registration("mcp", { + "version": "3.0.0", + "category": "integration", + "servers_count": len(self.mcp_servers) + }) + self.logger.info("Updated metadata with MCP component registration") - - return True except Exception as e: self.logger.error(f"Failed to update metadata: {e}") return False + + return True + def uninstall(self) -> bool: - """Uninstall MCP component by removing servers from .claude.json""" + """Uninstall MCP component""" try: - self.logger.info("Removing MCP server configurations...") + self.logger.info("Uninstalling SuperClaude MCP servers...") - # Load Claude configuration - claude_config, config_path = self._load_claude_config() - if claude_config is None: - self.logger.warning("Could not load Claude config for cleanup") - return True # Not a failure if config doesn't exist + # Uninstall each MCP server + uninstalled_count = 0 - if "mcpServers" not in claude_config: - self.logger.info("No MCP servers configured") - return True + for server_name in self.mcp_servers.keys(): + if self._uninstall_mcp_server(server_name): + uninstalled_count += 1 - # Only remove servers that were installed by SuperClaude - removed_count = 0 - installed_servers = self._get_installed_servers() - - for server_name in installed_servers: - if server_name in claude_config["mcpServers"]: - # Check if this server was installed by SuperClaude by comparing with our configs - if self._is_superclaude_managed_server(claude_config["mcpServers"][server_name], server_name): - del claude_config["mcpServers"][server_name] - removed_count += 1 - self.logger.debug(f"Removed SuperClaude-managed MCP server: {server_name}") - else: - self.logger.info(f"Preserved user-customized MCP server: {server_name}") - - # Save updated configuration - if removed_count > 0: - success = self._save_claude_config(claude_config, config_path) - if not success: - self.logger.warning("Failed to save updated Claude configuration") - - # Update settings.json + # Update metadata to remove MCP component try: if self.settings_manager.is_component_installed("mcp"): self.settings_manager.remove_component_registration("mcp") - self.logger.info("Removed MCP component from settings.json") + # Also remove MCP configuration from metadata + metadata = self.settings_manager.load_metadata() + if "mcp" in metadata: + del metadata["mcp"] + self.settings_manager.save_metadata(metadata) + self.logger.info("Removed MCP component from metadata") except Exception as e: - self.logger.warning(f"Could not update settings.json: {e}") + self.logger.warning(f"Could not update metadata: {e}") - if removed_count > 0: - self.logger.success(f"MCP component uninstalled ({removed_count} SuperClaude-managed servers removed)") - else: - self.logger.info("MCP component uninstalled (no SuperClaude-managed servers to remove)") + self.logger.success(f"MCP component uninstalled ({uninstalled_count} servers removed)") return True except Exception as e: self.logger.exception(f"Unexpected error during MCP uninstallation: {e}") return False - def _get_installed_servers(self) -> List[str]: - """Get list of servers that were installed by SuperClaude""" - try: - metadata = self.settings_manager.get_metadata_setting("components") - if metadata and "mcp" in metadata: - return metadata["mcp"].get("configured_servers", []) - except Exception: - pass - return [] - - def _is_superclaude_managed_server(self, server_config: Dict, server_name: str) -> bool: - """Check if a server configuration matches SuperClaude's templates - - This helps determine if a server was installed by SuperClaude or manually - configured by the user, allowing us to preserve user customizations. - """ - # Find the server key that maps to this server name - server_key = None - for key, info in self.mcp_servers.items(): - if info["name"] == server_name: - server_key = key - break - - if not server_key: - return False # Unknown server, don't remove - - # Load our template config for comparison - template_config = self._load_mcp_server_config(server_key) - if not template_config or server_name not in template_config: - return False - - template_server = template_config[server_name] - - # Check if the current config has the same structure as our template - # If user has customized it, the structure might be different - required_keys = {"command", "args"} - - # Check if all required keys exist and match our template - for key in required_keys: - if key not in server_config or key not in template_server: - return False - # For command and basic structure, they should match our template - if key == "command" and server_config[key] != template_server[key]: - return False - - return True - def get_dependencies(self) -> List[str]: """Get dependencies""" return ["core"] + def update(self, config: Dict[str, Any]) -> bool: + """Update MCP component""" + try: + self.logger.info("Updating SuperClaude MCP servers...") + + # Check current version + current_version = self.settings_manager.get_component_version("mcp") + target_version = self.get_metadata()["version"] + + if current_version == target_version: + self.logger.info(f"MCP component already at version {target_version}") + return True + + self.logger.info(f"Updating MCP component from {current_version} to {target_version}") + + # For MCP servers, update means reinstall to get latest versions + updated_count = 0 + failed_servers = [] + + for server_name, server_info in self.mcp_servers.items(): + try: + # Uninstall old version + if self._check_mcp_server_installed(server_name): + self._uninstall_mcp_server(server_name) + + # Install new version + if self._install_mcp_server(server_info, config): + updated_count += 1 + else: + failed_servers.append(server_name) + + except Exception as e: + self.logger.error(f"Error updating MCP server {server_name}: {e}") + failed_servers.append(server_name) + + # Update metadata + try: + # Update component version in metadata + metadata = self.settings_manager.load_metadata() + if "components" in metadata and "mcp" in metadata["components"]: + metadata["components"]["mcp"]["version"] = target_version + metadata["components"]["mcp"]["servers_count"] = len(self.mcp_servers) + if "mcp" in metadata: + metadata["mcp"]["servers"] = list(self.mcp_servers.keys()) + self.settings_manager.save_metadata(metadata) + except Exception as e: + self.logger.warning(f"Could not update metadata: {e}") + + if failed_servers: + self.logger.warning(f"Some MCP servers failed to update: {failed_servers}") + return False + else: + self.logger.success(f"MCP component updated to version {target_version}") + return True + + except Exception as e: + self.logger.exception(f"Unexpected error during MCP update: {e}") + return False + + def validate_installation(self) -> Tuple[bool, List[str]]: + """Validate MCP component installation""" + errors = [] + + # Check metadata registration + if not self.settings_manager.is_component_installed("mcp"): + errors.append("MCP component not registered in metadata") + return False, errors + + # Check version matches + installed_version = self.settings_manager.get_component_version("mcp") + expected_version = self.get_metadata()["version"] + if installed_version != expected_version: + errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}") + + # Check if Claude CLI is available + try: + result = subprocess.run( + ["claude", "mcp", "list"], + capture_output=True, + text=True, + timeout=15, + shell=(sys.platform == "win32") + ) + + if result.returncode != 0: + errors.append("Could not communicate with Claude CLI for MCP server verification") + else: + # Check if required servers are installed + output = result.stdout.lower() + for server_name, server_info in self.mcp_servers.items(): + if server_info.get("required", False): + if server_name.lower() not in output: + errors.append(f"Required MCP server not found: {server_name}") + + except Exception as e: + errors.append(f"Could not verify MCP server installation: {e}") + + return len(errors) == 0, errors + + def _get_source_dir(self): + """Get source directory for framework files""" + return None + def get_size_estimate(self) -> int: - """Get estimated size - minimal since we only modify config""" - return 4096 # 4KB - just config modifications \ No newline at end of file + """Get estimated installation size""" + # MCP servers are installed via npm, estimate based on typical sizes + base_size = 50 * 1024 * 1024 # ~50MB for all servers combined + return base_size + + def get_installation_summary(self) -> Dict[str, Any]: + """Get installation summary""" + return { + "component": self.get_metadata()["name"], + "version": self.get_metadata()["version"], + "servers_count": len(self.mcp_servers), + "mcp_servers": list(self.mcp_servers.keys()), + "estimated_size": self.get_size_estimate(), + "dependencies": self.get_dependencies(), + "required_tools": ["node", "npm", "claude"] + }