diff --git a/SuperClaude/MCP/configs/context7.json b/SuperClaude/MCP/configs/context7.json new file mode 100644 index 0000000..76125cc --- /dev/null +++ b/SuperClaude/MCP/configs/context7.json @@ -0,0 +1,9 @@ +{ + "context7": { + "command": "npx", + "args": [ + "-y", + "@upstash/context7-mcp@latest" + ] + } +} \ No newline at end of file diff --git a/SuperClaude/MCP/configs/magic.json b/SuperClaude/MCP/configs/magic.json new file mode 100644 index 0000000..dd3d7f0 --- /dev/null +++ b/SuperClaude/MCP/configs/magic.json @@ -0,0 +1,12 @@ +{ + "magic": { + "type": "stdio", + "command": "npx", + "args": [ + "@21st-dev/magic" + ], + "env": { + "TWENTYFIRST_API_KEY": "" + } + } +} \ No newline at end of file diff --git a/SuperClaude/MCP/configs/morphllm.json b/SuperClaude/MCP/configs/morphllm.json new file mode 100644 index 0000000..9eed493 --- /dev/null +++ b/SuperClaude/MCP/configs/morphllm.json @@ -0,0 +1,13 @@ +{ + "morphllm-fast-apply": { + "command": "npx", + "args": [ + "@morph-llm/morph-fast-apply", + "/app/" + ], + "env": { + "MORPH_API_KEY": "", + "ALL_TOOLS": "true" + } + } +} \ No newline at end of file diff --git a/SuperClaude/MCP/configs/playwright.json b/SuperClaude/MCP/configs/playwright.json new file mode 100644 index 0000000..7817848 --- /dev/null +++ b/SuperClaude/MCP/configs/playwright.json @@ -0,0 +1,8 @@ +{ + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + } +} \ No newline at end of file diff --git a/SuperClaude/MCP/configs/sequential.json b/SuperClaude/MCP/configs/sequential.json new file mode 100644 index 0000000..ea14ecb --- /dev/null +++ b/SuperClaude/MCP/configs/sequential.json @@ -0,0 +1,9 @@ +{ + "sequential-thinking": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-sequential-thinking" + ] + } +} \ No newline at end of file diff --git a/SuperClaude/MCP/configs/serena.json b/SuperClaude/MCP/configs/serena.json new file mode 100644 index 0000000..3e7bb0c --- /dev/null +++ b/SuperClaude/MCP/configs/serena.json @@ -0,0 +1,13 @@ +{ + "serena": { + "command": "uv", + "args": [ + "run", + "serena", + "start-mcp-server", + "--context", + "ide-assistant" + ], + "cwd": "$HOME/.claude/serena" + } +} \ No newline at end of file diff --git a/config/features.json b/config/features.json index e9d9197..c4859fc 100644 --- a/config/features.json +++ b/config/features.json @@ -20,21 +20,30 @@ }, "mcp": { "name": "mcp", - "version": "3.0.0", - "description": "MCP server integration (Context7, Sequential, Magic, Playwright, Morphllm, Serena)", + "version": "4.0.0", + "description": "MCP server configuration management via .claude.json", "category": "integration", "dependencies": ["core"], "enabled": true, - "required_tools": ["node", "claude_cli"] + "required_tools": [] }, - "serena": { - "name": "serena", - "version": "3.0.0", - "description": "Semantic code analysis and intelligent editing with project-aware context management", - "category": "integration", - "dependencies": ["core", "mcp"], + "modes": { + "name": "modes", + "version": "4.0.0", + "description": "SuperClaude behavioral modes (Brainstorming, Introspection, Task Management, Token Efficiency)", + "category": "modes", + "dependencies": ["core"], "enabled": true, - "required_tools": ["uvx", "python3", "claude_cli"] + "required_tools": [] + }, + "mcp_docs": { + "name": "mcp_docs", + "version": "4.0.0", + "description": "MCP server documentation and usage guides", + "category": "documentation", + "dependencies": ["core"], + "enabled": true, + "required_tools": [] }, "agents": { "name": "agents", @@ -44,15 +53,6 @@ "dependencies": ["core"], "enabled": true, "required_tools": [] - }, - "hooks": { - "name": "hooks", - "version": "2.0.0", - "description": "Enhanced Task Management System - Hook Infrastructure (NOT YET IMPLEMENTED)", - "category": "integration", - "dependencies": ["core"], - "enabled": false, - "required_tools": ["python3"] } } } \ No newline at end of file diff --git a/profiles/developer.json b/profiles/developer.json index 0f0bc49..36b68f6 100644 --- a/profiles/developer.json +++ b/profiles/developer.json @@ -5,7 +5,9 @@ "core", "commands", "agents", - "mcp" + "modes", + "mcp", + "mcp_docs" ], "features": { "auto_update": false, diff --git a/profiles/quick.json b/profiles/quick.json index d38fc64..2ab780b 100644 --- a/profiles/quick.json +++ b/profiles/quick.json @@ -4,7 +4,8 @@ "components": [ "core", "commands", - "agents" + "agents", + "modes" ], "features": { "auto_update": false, diff --git a/setup/components/__init__.py b/setup/components/__init__.py index cb0cac4..73e909e 100644 --- a/setup/components/__init__.py +++ b/setup/components/__init__.py @@ -4,12 +4,14 @@ from .core import CoreComponent from .commands import CommandsComponent from .mcp import MCPComponent from .agents import AgentsComponent -# from .hooks import HooksComponent # Commented out - not yet implemented +from .modes import ModesComponent +from .mcp_docs import MCPDocsComponent __all__ = [ 'CoreComponent', 'CommandsComponent', 'MCPComponent', 'AgentsComponent', - # 'HooksComponent' # Commented out - not yet implemented + 'ModesComponent', + 'MCPDocsComponent' ] \ No newline at end of file diff --git a/setup/components/hooks.py b/setup/components/hooks.py deleted file mode 100644 index 8c3eb9b..0000000 --- a/setup/components/hooks.py +++ /dev/null @@ -1,366 +0,0 @@ -""" -Hooks component for Claude Code hooks integration (future-ready) -""" - -from typing import Dict, List, Tuple, Optional, Any -from pathlib import Path - -from ..base.component import Component - - -class HooksComponent(Component): - """Claude Code hooks integration component""" - - def __init__(self, install_dir: Optional[Path] = None): - """Initialize hooks component""" - super().__init__(install_dir, Path("hooks")) - - # Define hook files to install (when hooks are ready) - self.hook_files = [ - "pre_tool_use.py", - "post_tool_use.py", - "error_handler.py", - "context_accumulator.py", - "performance_monitor.py" - ] - - def get_metadata(self) -> Dict[str, str]: - """Get component metadata""" - return { - "name": "hooks", - "version": "3.0.0", - "description": "Claude Code hooks integration (NOT YET IMPLEMENTED - future-ready)", - "category": "integration" - } - def get_metadata_modifications(self) -> Dict[str, Any]: - # Build hooks configuration based on available files - hook_config = {} - for filename in self.hook_files: - hook_path = self.install_component_subdir / filename - if hook_path.exists(): - hook_name = filename.replace('.py', '') - hook_config[hook_name] = [str(hook_path)] - - metadata_mods = { - "components": { - "hooks": { - "version": "3.0.0", - "installed": True, - "files_count": len(hook_config) - } - } - } - - # Only add hooks configuration if we have actual hook files - if hook_config: - metadata_mods["hooks"] = { - "enabled": True, - **hook_config - } - - - return metadata_mods - - def _install(self, config: Dict[str, Any]) -> bool: - """Install hooks component""" - self.logger.info("Installing SuperClaude hooks component...") - - # This component is future-ready - hooks aren't implemented yet - source_dir = self._get_source_dir() - - if not source_dir.exists() or (source_dir / "PLACEHOLDER.py").exists : - self.logger.info("Hooks are not yet implemented - installing placeholder component") - - # Create placeholder hooks directory - if not self.file_manager.ensure_directory(self.install_component_subdir): - self.logger.error(f"Could not create hooks directory: {self.install_component_subdir}") - return False - - # Create placeholder file - placeholder_content = '''""" -SuperClaude Hooks - Future Implementation - -This directory is reserved for Claude Code hooks integration. -Hooks will provide lifecycle management and automation capabilities. - -Planned hooks: -- pre_tool_use: Execute before tool usage -- post_tool_use: Execute after tool completion -- error_handler: Handle tool errors and recovery -- context_accumulator: Manage context across operations -- performance_monitor: Track and optimize performance - -For more information, see SuperClaude documentation. -""" - -# Placeholder for future hooks implementation -def placeholder_hook(): -"""Placeholder hook function""" -pass -''' - - placeholder_path = self.install_component_subdir / "PLACEHOLDER.py" - try: - with open(placeholder_path, 'w') as f: - f.write(placeholder_content) - self.logger.debug("Created hooks placeholder file") - except Exception as e: - self.logger.warning(f"Could not create placeholder file: {e}") - - # Update settings with placeholder registration - try: - metadata_mods = { - "components": { - "hooks": { - "version": "3.0.0", - "installed": True, - "status": "placeholder", - "files_count": 0 - } - } - } - self.settings_manager.update_metadata(metadata_mods) - self.logger.info("Updated metadata with hooks component registration") - except Exception as e: - self.logger.error(f"Failed to update metadata for hooks component: {e}") - return False - - self.logger.success("Hooks component installed successfully (placeholder)") - return True - - # If hooks source directory exists, install actual hooks - self.logger.info("Installing actual hook files...") - - # Validate installation - success, errors = self.validate_prerequisites(Path("hooks")) - if not success: - for error in errors: - self.logger.error(error) - return False - - # Get files to install - files_to_install = self.get_files_to_install() - - if not files_to_install: - self.logger.warning("No hook files found to install") - return False - - # Copy hook files - success_count = 0 - for source, target in files_to_install: - self.logger.debug(f"Copying {source.name} to {target}") - - if self.file_manager.copy_file(source, target): - success_count += 1 - self.logger.debug(f"Successfully copied {source.name}") - else: - self.logger.error(f"Failed to copy {source.name}") - - if success_count != len(files_to_install): - self.logger.error(f"Only {success_count}/{len(files_to_install)} hook files copied successfully") - return False - - self.logger.success(f"Hooks component installed successfully ({success_count} hook files)") - - return self._post_install() - - def _post_install(self): - # Update metadata - try: - metadata_mods = self.get_metadata_modifications() - self.settings_manager.update_metadata(metadata_mods) - self.logger.info("Updated metadata with hooks configuration") - - # Add hook registration to metadata - self.settings_manager.add_component_registration("hooks", { - "version": "3.0.0", - "category": "commands", - "files_count": len(self.hook_files) - }) - - self.logger.info("Updated metadata with commands component registration") - except Exception as e: - self.logger.error(f"Failed to update metadata: {e}") - return False - - return True - - def uninstall(self) -> bool: - """Uninstall hooks component""" - try: - self.logger.info("Uninstalling SuperClaude hooks component...") - - # Remove hook files and placeholder - removed_count = 0 - - # Remove actual hook files - for filename in self.hook_files: - file_path = self.install_component_subdir / filename - if self.file_manager.remove_file(file_path): - removed_count += 1 - self.logger.debug(f"Removed {filename}") - - # Remove placeholder file - placeholder_path = self.install_component_subdir / "PLACEHOLDER.py" - if self.file_manager.remove_file(placeholder_path): - removed_count += 1 - self.logger.debug("Removed hooks placeholder") - - # Remove hooks directory if empty - try: - if self.install_component_subdir.exists(): - remaining_files = list(self.install_component_subdir.iterdir()) - if not remaining_files: - self.install_component_subdir.rmdir() - self.logger.debug("Removed empty hooks directory") - except Exception as e: - self.logger.warning(f"Could not remove hooks directory: {e}") - - # Update settings.json to remove hooks component and configuration - try: - if self.settings_manager.is_component_installed("hooks"): - self.settings_manager.remove_component_registration("hooks") - - # Also remove hooks configuration section if it exists - settings = self.settings_manager.load_settings() - if "hooks" in settings: - del settings["hooks"] - self.settings_manager.save_settings(settings) - - self.logger.info("Removed hooks component and configuration from settings.json") - except Exception as e: - self.logger.warning(f"Could not update settings.json: {e}") - - self.logger.success(f"Hooks component uninstalled ({removed_count} files removed)") - return True - - except Exception as e: - self.logger.exception(f"Unexpected error during hooks uninstallation: {e}") - return False - - def get_dependencies(self) -> List[str]: - """Get dependencies""" - return ["core"] - - def update(self, config: Dict[str, Any]) -> bool: - """Update hooks component""" - try: - self.logger.info("Updating SuperClaude hooks component...") - - # Check current version - current_version = self.settings_manager.get_component_version("hooks") - target_version = self.get_metadata()["version"] - - if current_version == target_version: - self.logger.info(f"Hooks component already at version {target_version}") - return True - - self.logger.info(f"Updating hooks component from {current_version} to {target_version}") - - # Create backup of existing hook files - backup_files = [] - - if self.install_component_subdir.exists(): - for filename in self.hook_files + ["PLACEHOLDER.py"]: - file_path = self.install_component_subdir / filename - if file_path.exists(): - backup_path = self.file_manager.backup_file(file_path) - if backup_path: - backup_files.append(backup_path) - self.logger.debug(f"Backed up {filename}") - - # Perform installation (overwrites existing files) - success = self.install(config) - - if success: - # Remove backup files on successful update - for backup_path in backup_files: - try: - backup_path.unlink() - except Exception: - pass # Ignore cleanup errors - - self.logger.success(f"Hooks component updated to version {target_version}") - else: - # Restore from backup on failure - self.logger.warning("Update failed, restoring from backup...") - for backup_path in backup_files: - try: - original_path = backup_path.with_suffix('') - backup_path.rename(original_path) - self.logger.debug(f"Restored {original_path.name}") - except Exception as e: - self.logger.error(f"Could not restore {backup_path}: {e}") - - return success - - except Exception as e: - self.logger.exception(f"Unexpected error during hooks update: {e}") - return False - - def validate_installation(self) -> Tuple[bool, List[str]]: - """Validate hooks component installation""" - errors = [] - - # Check if hooks directory exists - if not self.install_component_subdir.exists(): - errors.append("Hooks directory not found") - return False, errors - - # Check settings.json registration - if not self.settings_manager.is_component_installed("hooks"): - errors.append("Hooks component not registered in settings.json") - else: - # Check version matches - installed_version = self.settings_manager.get_component_version("hooks") - expected_version = self.get_metadata()["version"] - if installed_version != expected_version: - errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}") - - # Check if we have either actual hooks or placeholder - has_placeholder = (self.install_component_subdir / "PLACEHOLDER.py").exists() - has_actual_hooks = any((self.install_component_subdir / filename).exists() for filename in self.hook_files) - - if not has_placeholder and not has_actual_hooks: - errors.append("No hook files or placeholder found") - - return len(errors) == 0, errors - - def _get_source_dir(self) -> Path: - """Get source directory for hook files""" - # Assume we're in SuperClaude/setup/components/hooks.py - # and hook files are in SuperClaude/SuperClaude/Hooks/ - project_root = Path(__file__).parent.parent.parent - return project_root / "SuperClaude" / "Hooks" - - def get_size_estimate(self) -> int: - """Get estimated installation size""" - # Estimate based on placeholder or actual files - source_dir = self._get_source_dir() - total_size = 0 - - if source_dir.exists(): - for filename in self.hook_files: - file_path = source_dir / filename - if file_path.exists(): - total_size += file_path.stat().st_size - - # Add placeholder overhead or minimum size - total_size = max(total_size, 10240) # At least 10KB - - return total_size - - def get_installation_summary(self) -> Dict[str, Any]: - """Get installation summary""" - source_dir = self._get_source_dir() - status = "placeholder" if not source_dir.exists() else "implemented" - - return { - "component": self.get_metadata()["name"], - "version": self.get_metadata()["version"], - "status": status, - "hook_files": self.hook_files if source_dir.exists() else ["PLACEHOLDER.py"], - "estimated_size": self.get_size_estimate(), - "install_directory": str(self.install_dir / "hooks"), - "dependencies": self.get_dependencies() - } diff --git a/setup/components/mcp.py b/setup/components/mcp.py index 90cb1f5..564e9ee 100644 --- a/setup/components/mcp.py +++ b/setup/components/mcp.py @@ -1,9 +1,9 @@ """ -MCP component for MCP server integration +MCP component for MCP server configuration via .claude.json """ -import subprocess -import sys +import json +import shutil from typing import Dict, List, Tuple, Optional, Any from pathlib import Path @@ -12,360 +12,289 @@ from ..utils.ui import display_info, display_warning class MCPComponent(Component): - """MCP servers integration component""" + """MCP servers configuration component""" def __init__(self, install_dir: Optional[Path] = None): """Initialize MCP component""" super().__init__(install_dir) - # Define MCP servers to install + # Define MCP servers available for configuration self.mcp_servers = { - "sequential-thinking": { - "name": "sequential-thinking", - "description": "Multi-step problem solving and systematic analysis", - "npm_package": "@modelcontextprotocol/server-sequential-thinking", - "required": True - }, "context7": { - "name": "context7", + "name": "context7", "description": "Official library documentation and code examples", - "npm_package": "@upstash/context7-mcp", - "required": True + "config_file": "context7.json", + "requires_api_key": False + }, + "sequential": { + "name": "sequential-thinking", + "description": "Multi-step problem solving and systematic analysis", + "config_file": "sequential.json", + "requires_api_key": False }, "magic": { "name": "magic", "description": "Modern UI component generation and design systems", - "npm_package": "@21st-dev/magic", - "required": False, - "api_key_env": "TWENTYFIRST_API_KEY", - "api_key_description": "21st.dev API key for UI component generation" + "config_file": "magic.json", + "requires_api_key": True, + "api_key_env": "TWENTYFIRST_API_KEY" }, "playwright": { "name": "playwright", "description": "Cross-browser E2E testing and automation", - "npm_package": "@playwright/mcp@latest", - "required": False + "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" } } + + # This will be set during installation + self.selected_servers = [] def get_metadata(self) -> Dict[str, str]: """Get component metadata""" return { "name": "mcp", - "version": "3.0.0", - "description": "MCP server integration (Context7, Sequential, Magic, Playwright)", + "version": "4.0.0", + "description": "MCP server configuration management via .claude.json", "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""" + """ + Check prerequisites for MCP component + """ 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 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 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") + # 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") return len(errors) == 0, errors def get_files_to_install(self) -> List[Tuple[Path, Path]]: - """Get files to install (none for MCP component)""" + """MCP component doesn't install files - it modifies .claude.json""" return [] - 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 _check_mcp_server_installed(self, server_name: str) -> bool: - """Check if MCP server is already installed""" - try: - 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 - - # 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 _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"] + 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" - command = "npx" + if not config_dir.exists(): + return None + + return config_dir + + 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""" + claude_config_path = Path.home() / ".claude.json" try: - 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 + with open(claude_config_path, 'r') as f: + config = json.load(f) + return config, claude_config_path except Exception as e: - self.logger.error(f"Error installing MCP server {server_name}: {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""" + try: + # Create backup + 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 open(config_path, 'w') as f: + json.dump(config, f, indent=2) + + self.logger.debug("Updated Claude configuration") + return True + except Exception as e: + self.logger.error(f"Failed to save Claude config: {e}") return False - def _uninstall_mcp_server(self, server_name: str) -> bool: - """Uninstall a single MCP server""" + 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 + 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 + with open(config_path, 'r') as f: + return json.load(f) except Exception as e: - self.logger.error(f"Error uninstalling MCP server {server_name}: {e}") - return False + self.logger.error(f"Failed to load MCP config for {server_key}: {e}") + return None def _install(self, config: Dict[str, Any]) -> bool: - """Install MCP component""" - self.logger.info("Installing SuperClaude MCP servers...") - + """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) + # Validate prerequisites success, errors = self.validate_prerequisites() if not success: for error in errors: self.logger.error(error) return False - - # 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)") + + # 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") + + # Merge server config into Claude config + claude_config["mcpServers"].update(server_config) + 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() else: - self.logger.success(f"MCP component installed successfully ({installed_count} servers)") - - return self._post_install() - + return False + def _post_install(self) -> bool: - # Update metadata + """Post-installation tasks""" try: - metadata_mods = self.get_metadata_modifications() + # Update metadata + metadata_mods = { + "components": { + "mcp": { + "version": "4.0.0", + "installed": True, + "servers_configured": len(self.selected_servers), + "configured_servers": self.selected_servers + } + } + } 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""" + """Uninstall MCP component by removing servers from .claude.json""" try: - self.logger.info("Uninstalling SuperClaude MCP servers...") + self.logger.info("Removing MCP server configurations...") - # Uninstall each MCP server - uninstalled_count = 0 + # 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 - for server_name in self.mcp_servers.keys(): - if self._uninstall_mcp_server(server_name): - uninstalled_count += 1 + if "mcpServers" not in claude_config: + self.logger.info("No MCP servers configured") + return True - # Update metadata to remove MCP component + # Remove all servers we know about + removed_count = 0 + for server_info in self.mcp_servers.values(): + server_name = server_info["name"] + if server_name in claude_config["mcpServers"]: + del claude_config["mcpServers"][server_name] + removed_count += 1 + self.logger.debug(f"Removed 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 try: if self.settings_manager.is_component_installed("mcp"): self.settings_manager.remove_component_registration("mcp") - # 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") + self.logger.info("Removed MCP component from settings.json") except Exception as e: - self.logger.warning(f"Could not update metadata: {e}") + self.logger.warning(f"Could not update settings.json: {e}") - self.logger.success(f"MCP component uninstalled ({uninstalled_count} servers removed)") + self.logger.success(f"MCP component uninstalled ({removed_count} servers removed)") return True except Exception as e: @@ -376,123 +305,6 @@ class MCPComponent(Component): """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 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"] - } + """Get estimated size - minimal since we only modify config""" + return 4096 # 4KB - just config modifications \ No newline at end of file diff --git a/setup/components/mcp_docs.py b/setup/components/mcp_docs.py new file mode 100644 index 0000000..745edcf --- /dev/null +++ b/setup/components/mcp_docs.py @@ -0,0 +1,225 @@ +""" +MCP Documentation component for SuperClaude MCP server documentation +""" + +from typing import Dict, List, Tuple, Optional, Any +from pathlib import Path + +from ..base.component import Component + + +class MCPDocsComponent(Component): + """MCP documentation component - installs docs for selected MCP servers""" + + def __init__(self, install_dir: Optional[Path] = None): + """Initialize MCP docs component""" + super().__init__(install_dir, Path("mcp")) + + # Map server names to documentation files + self.server_docs_map = { + "context7": "MCP_Context7.md", + "sequential": "MCP_Sequential.md", + "magic": "MCP_Magic.md", + "playwright": "MCP_Playwright.md", + "serena": "MCP_Serena.md", + "morphllm": "MCP_Morphllm.md" + } + + # This will be set during installation + self.selected_servers = [] + + def get_metadata(self) -> Dict[str, str]: + """Get component metadata""" + return { + "name": "mcp_docs", + "version": "4.0.0", + "description": "MCP server documentation and usage guides", + "category": "documentation" + } + + def set_selected_servers(self, selected_servers: List[str]) -> None: + """Set which MCP servers were selected for documentation installation""" + self.selected_servers = selected_servers + self.logger.debug(f"MCP docs will be installed for: {selected_servers}") + + def get_files_to_install(self) -> List[Tuple[Path, Path]]: + """ + Return list of files to install based on selected MCP servers + + Returns: + List of tuples (source_path, target_path) + """ + source_dir = self._get_source_dir() + files = [] + + if source_dir and self.selected_servers: + for server_name in self.selected_servers: + if server_name in self.server_docs_map: + doc_file = self.server_docs_map[server_name] + source = source_dir / doc_file + target = self.install_component_subdir / doc_file + if source.exists(): + files.append((source, target)) + self.logger.debug(f"Will install documentation for {server_name}: {doc_file}") + else: + self.logger.warning(f"Documentation file not found for {server_name}: {doc_file}") + + return files + + def _discover_component_files(self) -> List[str]: + """ + Override parent method to dynamically discover files based on selected servers + """ + files = [] + if self.selected_servers: + for server_name in self.selected_servers: + if server_name in self.server_docs_map: + files.append(self.server_docs_map[server_name]) + return files + + def _install(self, config: Dict[str, Any]) -> bool: + """Install MCP documentation component""" + self.logger.info("Installing MCP server documentation...") + + # Get selected servers from config + selected_servers = config.get("selected_mcp_servers", []) + if not selected_servers: + self.logger.info("No MCP servers selected - skipping documentation installation") + return True + + self.set_selected_servers(selected_servers) + + # Update component files based on selection + self.component_files = self._discover_component_files() + + # Validate installation + success, errors = self.validate_prerequisites() + if not success: + for error in errors: + self.logger.error(error) + return False + + # Get files to install + files_to_install = self.get_files_to_install() + + if not files_to_install: + self.logger.warning("No MCP documentation files found to install") + return True # Not an error - just no docs to install + + # Copy documentation files + success_count = 0 + for source, target in files_to_install: + self.logger.debug(f"Copying {source.name} to {target}") + + if self.file_manager.copy_file(source, target): + success_count += 1 + self.logger.debug(f"Successfully copied {source.name}") + else: + self.logger.error(f"Failed to copy {source.name}") + + if success_count != len(files_to_install): + self.logger.error(f"Only {success_count}/{len(files_to_install)} documentation files copied successfully") + return False + + self.logger.success(f"MCP documentation installed successfully ({success_count} files for {len(selected_servers)} servers)") + + return self._post_install() + + def _post_install(self) -> bool: + """Post-installation tasks""" + try: + # Update metadata + metadata_mods = { + "components": { + "mcp_docs": { + "version": "4.0.0", + "installed": True, + "files_count": len(self.component_files), + "servers_documented": self.selected_servers + } + } + } + self.settings_manager.update_metadata(metadata_mods) + self.logger.info("Updated metadata with MCP docs component registration") + + return True + except Exception as e: + self.logger.error(f"Failed to update metadata: {e}") + return False + + def uninstall(self) -> bool: + """Uninstall MCP documentation component""" + try: + self.logger.info("Uninstalling MCP documentation component...") + + # Remove all MCP documentation files + removed_count = 0 + source_dir = self._get_source_dir() + + if source_dir and source_dir.exists(): + # Remove all possible MCP doc files + for doc_file in self.server_docs_map.values(): + file_path = self.install_component_subdir / doc_file + if self.file_manager.remove_file(file_path): + removed_count += 1 + self.logger.debug(f"Removed {doc_file}") + + # Remove mcp directory if empty + try: + if self.install_component_subdir.exists(): + remaining_files = list(self.install_component_subdir.iterdir()) + if not remaining_files: + self.install_component_subdir.rmdir() + self.logger.debug("Removed empty mcp directory") + except Exception as e: + self.logger.warning(f"Could not remove mcp directory: {e}") + + # Update settings.json + try: + if self.settings_manager.is_component_installed("mcp_docs"): + self.settings_manager.remove_component_registration("mcp_docs") + self.logger.info("Removed MCP docs component from settings.json") + except Exception as e: + self.logger.warning(f"Could not update settings.json: {e}") + + self.logger.success(f"MCP documentation uninstalled ({removed_count} files removed)") + return True + + except Exception as e: + self.logger.exception(f"Unexpected error during MCP docs uninstallation: {e}") + return False + + def get_dependencies(self) -> List[str]: + """Get dependencies""" + return ["core"] + + def _get_source_dir(self) -> Optional[Path]: + """Get source directory for MCP documentation files""" + # Assume we're in SuperClaude/setup/components/mcp_docs.py + # and MCP docs are in SuperClaude/SuperClaude/MCP/ + project_root = Path(__file__).parent.parent.parent + mcp_dir = project_root / "SuperClaude" / "MCP" + + # Return None if directory doesn't exist to prevent warning + if not mcp_dir.exists(): + return None + + return mcp_dir + + def get_size_estimate(self) -> int: + """Get estimated installation size""" + source_dir = self._get_source_dir() + total_size = 0 + + if source_dir and source_dir.exists() and self.selected_servers: + for server_name in self.selected_servers: + if server_name in self.server_docs_map: + doc_file = self.server_docs_map[server_name] + file_path = source_dir / doc_file + if file_path.exists(): + total_size += file_path.stat().st_size + + # Minimum size estimate + total_size = max(total_size, 10240) # At least 10KB + + return total_size \ No newline at end of file diff --git a/setup/components/modes.py b/setup/components/modes.py new file mode 100644 index 0000000..b71f2ff --- /dev/null +++ b/setup/components/modes.py @@ -0,0 +1,153 @@ +""" +Modes component for SuperClaude behavioral modes +""" + +from typing import Dict, List, Tuple, Optional, Any +from pathlib import Path + +from ..base.component import Component + + +class ModesComponent(Component): + """SuperClaude behavioral modes component""" + + def __init__(self, install_dir: Optional[Path] = None): + """Initialize modes component""" + super().__init__(install_dir, Path("modes")) + + def get_metadata(self) -> Dict[str, str]: + """Get component metadata""" + return { + "name": "modes", + "version": "4.0.0", + "description": "SuperClaude behavioral modes (Brainstorming, Introspection, Task Management, Token Efficiency)", + "category": "modes" + } + + def _install(self, config: Dict[str, Any]) -> bool: + """Install modes component""" + self.logger.info("Installing SuperClaude behavioral modes...") + + # Validate installation + success, errors = self.validate_prerequisites() + if not success: + for error in errors: + self.logger.error(error) + return False + + # Get files to install + files_to_install = self.get_files_to_install() + + if not files_to_install: + self.logger.warning("No mode files found to install") + return False + + # Copy mode files + success_count = 0 + for source, target in files_to_install: + self.logger.debug(f"Copying {source.name} to {target}") + + if self.file_manager.copy_file(source, target): + success_count += 1 + self.logger.debug(f"Successfully copied {source.name}") + else: + self.logger.error(f"Failed to copy {source.name}") + + if success_count != len(files_to_install): + self.logger.error(f"Only {success_count}/{len(files_to_install)} mode files copied successfully") + return False + + self.logger.success(f"Modes component installed successfully ({success_count} mode files)") + + return self._post_install() + + def _post_install(self) -> bool: + """Post-installation tasks""" + try: + # Update metadata + metadata_mods = { + "components": { + "modes": { + "version": "4.0.0", + "installed": True, + "files_count": len(self.component_files) + } + } + } + self.settings_manager.update_metadata(metadata_mods) + self.logger.info("Updated metadata with modes component registration") + + return True + except Exception as e: + self.logger.error(f"Failed to update metadata: {e}") + return False + + def uninstall(self) -> bool: + """Uninstall modes component""" + try: + self.logger.info("Uninstalling SuperClaude modes component...") + + # Remove mode files + removed_count = 0 + for _, target in self.get_files_to_install(): + if self.file_manager.remove_file(target): + removed_count += 1 + self.logger.debug(f"Removed {target.name}") + + # Remove modes directory if empty + try: + if self.install_component_subdir.exists(): + remaining_files = list(self.install_component_subdir.iterdir()) + if not remaining_files: + self.install_component_subdir.rmdir() + self.logger.debug("Removed empty modes directory") + except Exception as e: + self.logger.warning(f"Could not remove modes directory: {e}") + + # Update settings.json + try: + if self.settings_manager.is_component_installed("modes"): + self.settings_manager.remove_component_registration("modes") + self.logger.info("Removed modes component from settings.json") + except Exception as e: + self.logger.warning(f"Could not update settings.json: {e}") + + self.logger.success(f"Modes component uninstalled ({removed_count} files removed)") + return True + + except Exception as e: + self.logger.exception(f"Unexpected error during modes uninstallation: {e}") + return False + + def get_dependencies(self) -> List[str]: + """Get dependencies""" + return ["core"] + + def _get_source_dir(self) -> Optional[Path]: + """Get source directory for mode files""" + # Assume we're in SuperClaude/setup/components/modes.py + # and mode files are in SuperClaude/SuperClaude/Modes/ + project_root = Path(__file__).parent.parent.parent + modes_dir = project_root / "SuperClaude" / "Modes" + + # Return None if directory doesn't exist to prevent warning + if not modes_dir.exists(): + return None + + return modes_dir + + def get_size_estimate(self) -> int: + """Get estimated installation size""" + source_dir = self._get_source_dir() + total_size = 0 + + if source_dir and source_dir.exists(): + for filename in self.component_files: + file_path = source_dir / filename + if file_path.exists(): + total_size += file_path.stat().st_size + + # Minimum size estimate + total_size = max(total_size, 20480) # At least 20KB + + return total_size \ No newline at end of file diff --git a/setup/operations/install.py b/setup/operations/install.py index a147656..db36151 100644 --- a/setup/operations/install.py +++ b/setup/operations/install.py @@ -138,7 +138,7 @@ def get_components_to_install(args: argparse.Namespace, registry: ComponentRegis # Explicit components specified if args.components: if 'all' in args.components: - return ["core", "commands", "agents", "mcp"] # hooks removed - not yet implemented + return ["core", "commands", "agents", "modes", "mcp", "mcp_docs"] return args.components # Profile-based selection @@ -169,38 +169,141 @@ def get_components_to_install(args: argparse.Namespace, registry: ComponentRegis return interactive_component_selection(registry, config_manager) -def interactive_component_selection(registry: ComponentRegistry, config_manager: ConfigManager) -> Optional[List[str]]: - """Interactive component selection""" +def select_mcp_servers(registry: ComponentRegistry) -> List[str]: + """Stage 1: MCP Server Selection""" logger = get_logger() try: - # Get available components - available_components = registry.list_components() + # Get MCP component to access server list + mcp_instance = registry.get_component_instance("mcp", Path.home() / ".claude") + if not mcp_instance or not hasattr(mcp_instance, 'mcp_servers'): + logger.error("Could not access MCP server information") + return [] - if not available_components: - logger.error("No components available for installation") - return None + # Create MCP server menu + mcp_servers = mcp_instance.mcp_servers + server_options = [] - # Create component menu with descriptions - menu_options = [] + for server_key, server_info in mcp_servers.items(): + description = server_info["description"] + api_key_note = " (requires API key)" if server_info.get("requires_api_key", False) else "" + server_options.append(f"{server_key} - {description}{api_key_note}") + + print(f"\n{Colors.CYAN}{Colors.BRIGHT}═══════════════════════════════════════════════════{Colors.RESET}") + print(f"{Colors.CYAN}{Colors.BRIGHT}Stage 1: MCP Server Selection (Optional){Colors.RESET}") + print(f"{Colors.CYAN}{Colors.BRIGHT}═══════════════════════════════════════════════════{Colors.RESET}") + print(f"\n{Colors.BLUE}MCP servers extend Claude Code with specialized capabilities.{Colors.RESET}") + print(f"{Colors.BLUE}Select servers to configure (you can always add more later):{Colors.RESET}") + + # Add option to skip MCP + server_options.append("Skip MCP Server installation") + + menu = Menu("Select MCP servers to configure:", server_options, multi_select=True) + selections = menu.display() + + if not selections: + logger.info("No MCP servers selected") + return [] + + # Filter out the "skip" option and return server keys + server_keys = list(mcp_servers.keys()) + selected_servers = [] + + for i in selections: + if i < len(server_keys): # Not the "skip" option + selected_servers.append(server_keys[i]) + + if selected_servers: + logger.info(f"Selected MCP servers: {', '.join(selected_servers)}") + else: + logger.info("No MCP servers selected") + + return selected_servers + + except Exception as e: + logger.error(f"Error in MCP server selection: {e}") + return [] + + +def select_framework_components(registry: ComponentRegistry, config_manager: ConfigManager, selected_mcp_servers: List[str]) -> List[str]: + """Stage 2: Framework Component Selection""" + logger = get_logger() + + try: + # Framework components (excluding MCP-related ones) + framework_components = ["core", "modes", "commands", "agents"] + + # Create component menu + component_options = [] component_info = {} - for component_name in available_components: + for component_name in framework_components: metadata = registry.get_component_metadata(component_name) if metadata: description = metadata.get("description", "No description") - category = metadata.get("category", "unknown") - menu_options.append(f"{component_name} ({category}) - {description}") + component_options.append(f"{component_name} - {description}") component_info[component_name] = metadata - else: - menu_options.append(f"{component_name} - Component description unavailable") - component_info[component_name] = {"description": "Unknown"} - # Add preset options + # Add MCP documentation option + if selected_mcp_servers: + mcp_docs_desc = f"MCP documentation for {', '.join(selected_mcp_servers)} (auto-selected)" + component_options.append(f"mcp_docs - {mcp_docs_desc}") + auto_selected_mcp_docs = True + else: + component_options.append("mcp_docs - MCP server documentation (none selected)") + auto_selected_mcp_docs = False + + print(f"\n{Colors.CYAN}{Colors.BRIGHT}═══════════════════════════════════════════════════{Colors.RESET}") + print(f"{Colors.CYAN}{Colors.BRIGHT}Stage 2: Framework Component Selection{Colors.RESET}") + print(f"{Colors.CYAN}{Colors.BRIGHT}═══════════════════════════════════════════════════{Colors.RESET}") + print(f"\n{Colors.BLUE}Select SuperClaude framework components to install:{Colors.RESET}") + + menu = Menu("Select components (Core is recommended):", component_options, multi_select=True) + selections = menu.display() + + if not selections: + # Default to core if nothing selected + logger.info("No components selected, defaulting to core") + selected_components = ["core"] + else: + selected_components = [] + all_components = framework_components + ["mcp_docs"] + + for i in selections: + if i < len(all_components): + selected_components.append(all_components[i]) + + # Auto-select MCP docs if not explicitly deselected and we have MCP servers + if auto_selected_mcp_docs and "mcp_docs" not in selected_components: + # Check if user explicitly deselected it + mcp_docs_index = len(framework_components) # Index of mcp_docs in the menu + if mcp_docs_index not in selections: + # User didn't select it, but we auto-select it + selected_components.append("mcp_docs") + logger.info("Auto-selected MCP documentation for configured servers") + + # Always include MCP component if servers were selected + if selected_mcp_servers and "mcp" not in selected_components: + selected_components.append("mcp") + + logger.info(f"Selected framework components: {', '.join(selected_components)}") + return selected_components + + except Exception as e: + logger.error(f"Error in framework component selection: {e}") + return ["core"] # Fallback to core + + +def interactive_component_selection(registry: ComponentRegistry, config_manager: ConfigManager) -> Optional[List[str]]: + """Two-stage interactive component selection""" + logger = get_logger() + + try: + # Add preset options first preset_options = [ "Quick Installation (recommended components)", "Minimal Installation (core only)", - "Custom Selection" + "Custom Two-Stage Selection" ] print(f"\n{Colors.CYAN}SuperClaude Installation Options:{Colors.RESET}") @@ -218,16 +321,19 @@ def interactive_component_selection(registry: ComponentRegistry, config_manager: return ["core"] elif choice == 1: # Minimal return ["core"] - elif choice == 2: # Custom - print(f"\n{Colors.CYAN}Available Components:{Colors.RESET}") - component_menu = Menu("Select components to install:", menu_options, multi_select=True) - selections = component_menu.display() + elif choice == 2: # Custom Two-Stage + # Stage 1: MCP Server Selection + selected_mcp_servers = select_mcp_servers(registry) - if not selections: - logger.warning("No components selected") - return None + # Stage 2: Framework Component Selection + selected_components = select_framework_components(registry, config_manager, selected_mcp_servers) - return [available_components[i] for i in selections] + # Store selected MCP servers for components to use + if not hasattr(config_manager, '_installation_context'): + config_manager._installation_context = {} + config_manager._installation_context["selected_mcp_servers"] = selected_mcp_servers + + return selected_components return None @@ -331,7 +437,7 @@ def run_system_diagnostics(validator: Validator) -> None: print(" 3. Run 'SuperClaude install --diagnose' again to verify") -def perform_installation(components: List[str], args: argparse.Namespace) -> bool: +def perform_installation(components: List[str], args: argparse.Namespace, config_manager: ConfigManager = None) -> bool: """Perform the actual installation""" logger = get_logger() start_time = time.time() @@ -370,7 +476,8 @@ def perform_installation(components: List[str], args: argparse.Namespace) -> boo config = { "force": args.force, "backup": not args.no_backup, - "dry_run": args.dry_run + "dry_run": args.dry_run, + "selected_mcp_servers": getattr(config_manager, '_installation_context', {}).get("selected_mcp_servers", []) } success = installer.install_components(ordered_components, config) @@ -518,7 +625,7 @@ def run(args: argparse.Namespace) -> int: return 0 # Perform installation - success = perform_installation(components, args) + success = perform_installation(components, args, config_manager) if success: if not args.quiet: