refactor: consolidate MCP integration to unified gateway

**Changes**:
- Remove individual MCP server docs (superclaude/mcp/*.md)
- Remove MCP server configs (superclaude/mcp/configs/*.json)
- Delete MCP docs component (setup/components/mcp_docs.py)
- Simplify installer (setup/core/installer.py)
- Update components for unified gateway approach

**Rationale**:
- Unified gateway (airis-mcp-gateway) provides all MCP servers
- Individual docs/configs no longer needed (managed centrally)
- Reduces maintenance burden and file count
- Simplifies installation process

**Files Removed**: 17 MCP files (docs + configs)
**Installer Changes**: Removed legacy MCP installation logic

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
kazuki
2025-10-17 07:24:01 +09:00
parent 4a5d7499a4
commit 01ddc14354
30 changed files with 253 additions and 1242 deletions

View File

@@ -138,12 +138,12 @@ def get_components_to_install(
# Explicit components specified
if args.components:
if "all" in args.components:
components = ["framework_docs", "commands", "agents", "modes", "mcp", "mcp_docs"]
components = ["framework_docs", "commands", "agents", "modes", "mcp"]
else:
components = args.components
# If mcp or mcp_docs is specified, handle MCP server selection
if ("mcp" in components or "mcp_docs" in components) and not args.yes:
# If mcp is specified, handle MCP server selection
if "mcp" in components and not args.yes:
selected_servers = select_mcp_servers(registry)
if not hasattr(config_manager, "_installation_context"):
config_manager._installation_context = {}
@@ -151,26 +151,16 @@ def get_components_to_install(
selected_servers
)
# If the user selected some servers, ensure both mcp and mcp_docs are included
# If the user selected some servers, ensure mcp is included
if selected_servers:
if "mcp" not in components:
components.append("mcp")
logger.debug(
f"Auto-added 'mcp' component for selected servers: {selected_servers}"
)
if "mcp_docs" not in components:
components.append("mcp_docs")
logger.debug(
f"Auto-added 'mcp_docs' component for selected servers: {selected_servers}"
)
logger.info(f"Final components to install: {components}")
# If mcp_docs was explicitly requested but no servers selected, allow auto-detection
elif not selected_servers and "mcp_docs" in components:
logger.info("mcp_docs component will auto-detect existing MCP servers")
logger.info("Documentation will be installed for any detected servers")
return components
# Interactive two-stage selection
@@ -227,7 +217,7 @@ def select_mcp_servers(registry: ComponentRegistry) -> List[str]:
try:
# Get MCP component to access server list
mcp_instance = registry.get_component_instance(
"mcp", get_home_directory() / ".claude"
"mcp", DEFAULT_INSTALL_DIR
)
if not mcp_instance or not hasattr(mcp_instance, "mcp_servers"):
logger.error("Could not access MCP server information")
@@ -325,16 +315,7 @@ def select_framework_components(
component_options.append(f"{component_name} - {description}")
component_info[component_name] = metadata
# 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
# MCP documentation is integrated into airis-mcp-gateway, no separate component needed
print(f"\n{Colors.CYAN}{Colors.BRIGHT}{'='*51}{Colors.RESET}")
print(
@@ -358,21 +339,12 @@ def select_framework_components(
selected_components = ["framework_docs"]
else:
selected_components = []
all_components = framework_components + ["mcp_docs"]
all_components = framework_components
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")
@@ -601,9 +573,6 @@ def perform_installation(
if summary["installed"]:
logger.info(f"Installed components: {', '.join(summary['installed'])}")
if summary["backup_path"]:
logger.info(f"Backup created: {summary['backup_path']}")
else:
logger.error(
f"Installation completed with errors in {duration:.1f} seconds"

View File

@@ -79,14 +79,6 @@ def verify_superclaude_file(file_path: Path, component: str) -> bool:
"MODE_Task_Management.md",
"MODE_Token_Efficiency.md",
],
"mcp_docs": [
"MCP_Context7.md",
"MCP_Sequential.md",
"MCP_Magic.md",
"MCP_Playwright.md",
"MCP_Morphllm.md",
"MCP_Serena.md",
],
}
# For commands component, verify it's in the sc/ subdirectory
@@ -427,8 +419,7 @@ def _custom_component_selection(
"core": "Core Framework Files (CLAUDE.md, FLAGS.md, PRINCIPLES.md, etc.)",
"commands": "superclaude Commands (commands/sc/*.md)",
"agents": "Specialized Agents (agents/*.md)",
"mcp": "MCP Server Configurations",
"mcp_docs": "MCP Documentation",
"mcp": "MCP Server Configurations (airis-mcp-gateway)",
"modes": "superclaude Modes",
}
@@ -568,9 +559,8 @@ def display_component_details(component: str, info: Dict[str, Any]) -> Dict[str,
},
"mcp": {
"files": "MCP server configurations in .claude.json",
"description": "MCP server configurations",
"description": "MCP server configurations (airis-mcp-gateway)",
},
"mcp_docs": {"files": "MCP/*.md", "description": "MCP documentation files"},
"modes": {"files": "MODE_*.md", "description": "superclaude operational modes"},
}

View File

@@ -389,9 +389,6 @@ def perform_update(
if summary.get("updated"):
logger.info(f"Updated components: {', '.join(summary['updated'])}")
if summary.get("backup_path"):
logger.info(f"Backup created: {summary['backup_path']}")
else:
logger.error(f"Update completed with errors in {duration:.1f} seconds")

View File

@@ -5,7 +5,6 @@ from .commands import CommandsComponent
from .mcp import MCPComponent
from .agents import AgentsComponent
from .modes import ModesComponent
from .mcp_docs import MCPDocsComponent
__all__ = [
"FrameworkDocsComponent",
@@ -13,5 +12,4 @@ __all__ = [
"MCPComponent",
"AgentsComponent",
"ModesComponent",
"MCPDocsComponent",
]

View File

@@ -25,6 +25,13 @@ class AgentsComponent(Component):
"category": "agents",
}
def is_reinstallable(self) -> bool:
"""
Agents should always be synced to latest version.
SuperClaude agent files always overwrite existing files.
"""
return True
def get_metadata_modifications(self) -> Dict[str, Any]:
"""Get metadata modifications for agents"""
return {
@@ -64,14 +71,14 @@ class AgentsComponent(Component):
self.settings_manager.update_metadata(metadata_mods)
self.logger.info("Updated metadata with agents configuration")
# Add component registration
# Add component registration (with file list for sync)
self.settings_manager.add_component_registration(
"agents",
{
"version": __version__,
"category": "agents",
"agents_count": len(self.component_files),
"agents_list": self.component_files,
"files": list(self.component_files), # Track for sync/deletion
},
)
@@ -129,57 +136,51 @@ class AgentsComponent(Component):
return ["framework_docs"]
def update(self, config: Dict[str, Any]) -> bool:
"""Update agents component"""
"""
Sync agents component (overwrite + delete obsolete files).
No backup needed - SuperClaude source files are always authoritative.
"""
try:
self.logger.info("Updating SuperClaude agents component...")
self.logger.info("Syncing SuperClaude agents component...")
# Check current version
current_version = self.settings_manager.get_component_version("agents")
target_version = self.get_metadata()["version"]
if current_version == target_version:
self.logger.info(
f"Agents component already at version {target_version}"
)
return True
self.logger.info(
f"Updating agents component from {current_version} to {target_version}"
# Get previously installed files from metadata
metadata = self.settings_manager.load_metadata()
previous_files = set(
metadata.get("components", {}).get("agents", {}).get("files", [])
)
# Create backup of existing agents
backup_files = []
for filename in self.component_files:
# Get current files from source
current_files = set(self.component_files)
# Files to delete (were installed before, but no longer in source)
files_to_delete = previous_files - current_files
# Delete obsolete files
deleted_count = 0
for filename in files_to_delete:
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 agent: {filename}")
# Perform installation (will overwrite existing files)
if self._install(config):
self.logger.success(
f"Agents component updated to version {target_version}"
)
return True
else:
# Restore backups on failure
self.logger.error("Agents update failed, restoring backups...")
for backup_path in backup_files:
try:
original_path = (
self.install_component_subdir
/ backup_path.name.replace(".backup", "")
)
self.file_manager.copy_file(backup_path, original_path)
self.logger.debug(f"Restored {original_path.name}")
file_path.unlink()
deleted_count += 1
self.logger.info(f"Deleted obsolete agent: {filename}")
except Exception as e:
self.logger.warning(f"Could not restore {backup_path}: {e}")
return False
self.logger.warning(f"Could not delete {filename}: {e}")
# Install/overwrite current files (no backup)
success = self._install(config)
if success:
self.logger.success(
f"Agents synced: {len(current_files)} files, {deleted_count} obsolete files removed"
)
else:
self.logger.error("Agents sync failed")
return success
except Exception as e:
self.logger.exception(f"Unexpected error during agents update: {e}")
self.logger.exception(f"Unexpected error during agents sync: {e}")
return False
def _get_source_dir(self) -> Path:

View File

@@ -14,6 +14,15 @@ class CommandsComponent(Component):
def __init__(self, install_dir: Optional[Path] = None):
"""Initialize commands component"""
if install_dir is None:
install_dir = Path.home() / ".claude"
# Commands are installed directly to ~/.claude/commands/sc/
# not under superclaude/ subdirectory (Claude Code official location)
if "superclaude" in str(install_dir):
# ~/.claude/superclaude -> ~/.claude
install_dir = install_dir.parent
super().__init__(install_dir, Path("commands/sc"))
def get_metadata(self) -> Dict[str, str]:
@@ -25,6 +34,13 @@ class CommandsComponent(Component):
"category": "commands",
}
def is_reinstallable(self) -> bool:
"""
Commands should always be synced to latest version.
SuperClaude command files always overwrite existing files.
"""
return True
def get_metadata_modifications(self) -> Dict[str, Any]:
"""Get metadata modifications for commands component"""
return {
@@ -54,13 +70,14 @@ class CommandsComponent(Component):
self.settings_manager.update_metadata(metadata_mods)
self.logger.info("Updated metadata with commands configuration")
# Add component registration to metadata
# Add component registration to metadata (with file list for sync)
self.settings_manager.add_component_registration(
"commands",
{
"version": __version__,
"category": "commands",
"files_count": len(self.component_files),
"files": list(self.component_files), # Track for sync/deletion
},
)
self.logger.info("Updated metadata with commands component registration")
@@ -68,6 +85,16 @@ class CommandsComponent(Component):
self.logger.error(f"Failed to update metadata: {e}")
return False
# Clean up old commands directory in superclaude/ (from previous versions)
try:
old_superclaude_commands = Path.home() / ".claude" / "superclaude" / "commands"
if old_superclaude_commands.exists():
import shutil
shutil.rmtree(old_superclaude_commands)
self.logger.info("Removed old commands directory from superclaude/")
except Exception as e:
self.logger.debug(f"Could not remove old commands directory: {e}")
return True
def uninstall(self) -> bool:
@@ -156,66 +183,63 @@ class CommandsComponent(Component):
return ["framework_docs"]
def update(self, config: Dict[str, Any]) -> bool:
"""Update commands component"""
"""
Sync commands component (overwrite + delete obsolete files).
No backup needed - SuperClaude source files are always authoritative.
"""
try:
self.logger.info("Updating SuperClaude commands component...")
self.logger.info("Syncing SuperClaude commands component...")
# Check current version
current_version = self.settings_manager.get_component_version("commands")
target_version = self.get_metadata()["version"]
if current_version == target_version:
self.logger.info(
f"Commands component already at version {target_version}"
)
return True
self.logger.info(
f"Updating commands component from {current_version} to {target_version}"
# Get previously installed files from metadata
metadata = self.settings_manager.load_metadata()
previous_files = set(
metadata.get("components", {}).get("commands", {}).get("files", [])
)
# Create backup of existing command files
# Get current files from source
current_files = set(self.component_files)
# Files to delete (were installed before, but no longer in source)
files_to_delete = previous_files - current_files
# Delete obsolete files
deleted_count = 0
commands_dir = self.install_dir / "commands" / "sc"
backup_files = []
for filename in files_to_delete:
file_path = commands_dir / filename
if file_path.exists():
try:
file_path.unlink()
deleted_count += 1
self.logger.info(f"Deleted obsolete command: {filename}")
except Exception as e:
self.logger.warning(f"Could not delete {filename}: {e}")
if commands_dir.exists():
for filename in self.component_files:
file_path = commands_dir / 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)
# Install/overwrite current files (no backup)
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
# Update metadata with current file list
self.settings_manager.add_component_registration(
"commands",
{
"version": __version__,
"category": "commands",
"files_count": len(current_files),
"files": list(current_files), # Track installed files
},
)
self.logger.success(
f"Commands component updated to version {target_version}"
f"Commands synced: {len(current_files)} files, {deleted_count} obsolete files removed"
)
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}")
self.logger.error("Commands sync failed")
return success
except Exception as e:
self.logger.exception(f"Unexpected error during commands update: {e}")
self.logger.exception(f"Unexpected error during commands sync: {e}")
return False
def validate_installation(self) -> Tuple[bool, List[str]]:

View File

@@ -28,6 +28,13 @@ class FrameworkDocsComponent(Component):
"category": "documentation",
}
def is_reinstallable(self) -> bool:
"""
Framework docs should always be updated to latest version.
SuperClaude-related documentation should always overwrite existing files.
"""
return True
def get_metadata_modifications(self) -> Dict[str, Any]:
"""Get metadata modifications for SuperClaude"""
return {
@@ -59,13 +66,14 @@ class FrameworkDocsComponent(Component):
self.settings_manager.update_metadata(metadata_mods)
self.logger.info("Updated metadata with framework configuration")
# Add component registration to metadata
# Add component registration to metadata (with file list for sync)
self.settings_manager.add_component_registration(
"framework_docs",
{
"version": __version__,
"category": "documentation",
"files_count": len(self.component_files),
"files": list(self.component_files), # Track for sync/deletion
},
)
@@ -144,61 +152,64 @@ class FrameworkDocsComponent(Component):
return []
def update(self, config: Dict[str, Any]) -> bool:
"""Update framework docs component"""
"""
Sync framework docs component (overwrite + delete obsolete files).
No backup needed - SuperClaude source files are always authoritative.
"""
try:
self.logger.info("Updating SuperClaude framework docs component...")
self.logger.info("Syncing SuperClaude framework docs component...")
# Check current version
current_version = self.settings_manager.get_component_version("framework_docs")
target_version = self.get_metadata()["version"]
if current_version == target_version:
self.logger.info(f"Framework docs component already at version {target_version}")
return True
self.logger.info(
f"Updating framework docs component from {current_version} to {target_version}"
# Get previously installed files from metadata
metadata = self.settings_manager.load_metadata()
previous_files = set(
metadata.get("components", {})
.get("framework_docs", {})
.get("files", [])
)
# Create backup of existing files
backup_files = []
for filename in self.component_files:
# Get current files from source
current_files = set(self.component_files)
# Files to delete (were installed before, but no longer in source)
files_to_delete = previous_files - current_files
# Delete obsolete files
deleted_count = 0
for filename in files_to_delete:
file_path = self.install_dir / 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}")
try:
file_path.unlink()
deleted_count += 1
self.logger.info(f"Deleted obsolete file: {filename}")
except Exception as e:
self.logger.warning(f"Could not delete {filename}: {e}")
# Perform installation (overwrites existing files)
# Install/overwrite current files (no backup)
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
# Update metadata with current file list
self.settings_manager.add_component_registration(
"framework_docs",
{
"version": __version__,
"category": "documentation",
"files_count": len(current_files),
"files": list(current_files), # Track installed files
},
)
self.logger.success(
f"Framework docs component updated to version {target_version}"
f"Framework docs synced: {len(current_files)} files, {deleted_count} obsolete files removed"
)
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("")
shutil.move(str(backup_path), str(original_path))
self.logger.debug(f"Restored {original_path.name}")
except Exception as e:
self.logger.error(f"Could not restore {backup_path}: {e}")
self.logger.error("Framework docs sync failed")
return success
except Exception as e:
self.logger.exception(f"Unexpected error during framework docs update: {e}")
self.logger.exception(f"Unexpected error during framework docs sync: {e}")
return False
def validate_installation(self) -> Tuple[bool, List[str]]:

View File

@@ -1,374 +0,0 @@
"""
MCP Documentation component for SuperClaude MCP server documentation
"""
from typing import Dict, List, Tuple, Optional, Any
from pathlib import Path
from ..core.base import Component
from setup import __version__
from ..services.claude_md import CLAUDEMdService
class MCPDocsComponent(Component):
"""MCP documentation component - installs docs for selected MCP servers"""
def __init__(self, install_dir: Optional[Path] = None):
"""Initialize MCP docs component"""
# Initialize attributes before calling parent constructor
# because parent calls _discover_component_files() which needs these
self.selected_servers: List[str] = []
# Map server names to documentation files
self.server_docs_map = {
"context7": "MCP_Context7.md",
"sequential": "MCP_Sequential.md",
"sequential-thinking": "MCP_Sequential.md", # Handle both naming conventions
"magic": "MCP_Magic.md",
"playwright": "MCP_Playwright.md",
"serena": "MCP_Serena.md",
"morphllm": "MCP_Morphllm.md",
"morphllm-fast-apply": "MCP_Morphllm.md", # Handle both naming conventions
"tavily": "MCP_Tavily.md",
}
super().__init__(install_dir, Path(""))
def get_metadata(self) -> Dict[str, str]:
"""Get component metadata"""
return {
"name": "mcp_docs",
"version": __version__,
"description": "MCP server documentation and usage guides",
"category": "documentation",
}
def is_reinstallable(self) -> bool:
"""
Allow mcp_docs to be reinstalled to handle different server selections.
This enables users to add or change MCP server documentation.
"""
return True
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_dir / 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 = []
# Check if selected_servers is not empty
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 _detect_existing_mcp_servers_from_config(self) -> List[str]:
"""Detect existing MCP servers from Claude Desktop config"""
detected_servers = []
try:
# Try to find Claude Desktop config file
config_paths = [
self.install_dir / "claude_desktop_config.json",
Path.home() / ".claude" / "claude_desktop_config.json",
Path.home() / ".claude.json", # Claude CLI config
Path.home()
/ "AppData"
/ "Roaming"
/ "Claude"
/ "claude_desktop_config.json", # Windows
Path.home()
/ "Library"
/ "Application Support"
/ "Claude"
/ "claude_desktop_config.json", # macOS
]
config_file = None
for path in config_paths:
if path.exists():
config_file = path
break
if not config_file:
self.logger.debug("No Claude Desktop config file found")
return detected_servers
import json
with open(config_file, "r") as f:
config = json.load(f)
# Extract MCP server names from mcpServers section
mcp_servers = config.get("mcpServers", {})
for server_name in mcp_servers.keys():
# Map common name variations to our doc file names
normalized_name = self._normalize_server_name(server_name)
if normalized_name and normalized_name in self.server_docs_map:
detected_servers.append(normalized_name)
if detected_servers:
self.logger.info(
f"Detected existing MCP servers from config: {detected_servers}"
)
except Exception as e:
self.logger.warning(f"Could not read Claude Desktop config: {e}")
return detected_servers
def _normalize_server_name(self, server_name: str) -> Optional[str]:
"""Normalize server name to match our documentation mapping"""
if not server_name:
return None
server_name = server_name.lower().strip()
# Map common variations to our server_docs_map keys
name_mappings = {
"context7": "context7",
"sequential-thinking": "sequential-thinking",
"sequential": "sequential-thinking",
"magic": "magic",
"playwright": "playwright",
"serena": "serena",
"morphllm": "morphllm",
"morphllm-fast-apply": "morphllm",
"morph": "morphllm",
}
return name_mappings.get(server_name)
def _install(self, config: Dict[str, Any]) -> bool:
"""Install MCP documentation component with auto-detection"""
self.logger.info("Installing MCP server documentation...")
# Auto-detect existing servers
self.logger.info("Auto-detecting existing MCP servers for documentation...")
detected_servers = self._detect_existing_mcp_servers_from_config()
# Get selected servers from config
selected_servers = config.get("selected_mcp_servers", [])
# Get previously documented servers from metadata
previous_servers = self.settings_manager.get_metadata_setting(
"components.mcp_docs.servers_documented", []
)
# Merge all server lists
all_servers = list(set(detected_servers + selected_servers + previous_servers))
# Filter to only servers we have documentation for
valid_servers = [s for s in all_servers if s in self.server_docs_map]
if not valid_servers:
self.logger.info(
"No MCP servers detected or selected for documentation installation"
)
# Still proceed to update metadata
self.set_selected_servers([])
self.component_files = []
return self._post_install()
self.logger.info(
f"Installing documentation for MCP servers: {', '.join(valid_servers)}"
)
if detected_servers:
self.logger.info(f" - Detected from config: {detected_servers}")
if selected_servers:
self.logger.info(f" - Newly selected: {selected_servers}")
if previous_servers:
self.logger.info(f" - Previously documented: {previous_servers}")
# Set the servers for which we'll install documentation
self.set_selected_servers(valid_servers)
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 False
# Copy documentation files
success_count = 0
successfully_copied_files = []
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
successfully_copied_files.append(source.name)
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
# Update component_files to only include successfully copied files
self.component_files = successfully_copied_files
self.logger.success(
f"MCP documentation installed successfully ({success_count} files for {len(valid_servers)} servers)"
)
return self._post_install()
def _post_install(self) -> bool:
"""Post-installation tasks"""
try:
# Update metadata
metadata_mods = {
"components": {
"mcp_docs": {
"version": __version__,
"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")
# Update CLAUDE.md with MCP documentation imports
try:
manager = CLAUDEMdService(self.install_dir)
manager.add_imports(self.component_files, category="MCP Documentation")
self.logger.info("Updated CLAUDE.md with MCP documentation imports")
except Exception as e:
self.logger.warning(
f"Failed to update CLAUDE.md with MCP documentation imports: {e}"
)
# Don't fail the whole installation for this
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 ["framework_docs"]
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

View File

@@ -26,6 +26,13 @@ class ModesComponent(Component):
"category": "modes",
}
def is_reinstallable(self) -> bool:
"""
Modes should always be synced to latest version.
SuperClaude mode files always overwrite existing files.
"""
return True
def _install(self, config: Dict[str, Any]) -> bool:
"""Install modes component"""
self.logger.info("Installing SuperClaude behavioral modes...")
@@ -77,6 +84,7 @@ class ModesComponent(Component):
"version": __version__,
"installed": True,
"files_count": len(self.component_files),
"files": list(self.component_files), # Track for sync/deletion
}
}
}
@@ -142,6 +150,67 @@ class ModesComponent(Component):
"""Get dependencies"""
return ["framework_docs"]
def update(self, config: Dict[str, Any]) -> bool:
"""
Sync modes component (overwrite + delete obsolete files).
No backup needed - SuperClaude source files are always authoritative.
"""
try:
self.logger.info("Syncing SuperClaude modes component...")
# Get previously installed files from metadata
metadata = self.settings_manager.load_metadata()
previous_files = set(
metadata.get("components", {}).get("modes", {}).get("files", [])
)
# Get current files from source
current_files = set(self.component_files)
# Files to delete (were installed before, but no longer in source)
files_to_delete = previous_files - current_files
# Delete obsolete files
deleted_count = 0
for filename in files_to_delete:
file_path = self.install_dir / filename
if file_path.exists():
try:
file_path.unlink()
deleted_count += 1
self.logger.info(f"Deleted obsolete mode: {filename}")
except Exception as e:
self.logger.warning(f"Could not delete {filename}: {e}")
# Install/overwrite current files (no backup)
success = self.install(config)
if success:
# Update metadata with current file list
metadata_mods = {
"components": {
"modes": {
"version": __version__,
"installed": True,
"files_count": len(current_files),
"files": list(current_files), # Track installed files
}
}
}
self.settings_manager.update_metadata(metadata_mods)
self.logger.success(
f"Modes synced: {len(current_files)} files, {deleted_count} obsolete files removed"
)
else:
self.logger.error("Modes sync failed")
return success
except Exception as e:
self.logger.exception(f"Unexpected error during modes sync: {e}")
return False
def _get_source_dir(self) -> Optional[Path]:
"""Get source directory for mode files"""
# Assume we're in superclaude/setup/components/modes.py

View File

@@ -37,7 +37,6 @@ class Installer:
self.failed_components: Set[str] = set()
self.skipped_components: Set[str] = set()
self.backup_path: Optional[Path] = None
self.logger = get_logger()
def register_component(self, component: Component) -> None:
@@ -132,59 +131,6 @@ class Installer:
return len(errors) == 0, errors
def create_backup(self) -> Optional[Path]:
"""
Create backup of existing installation
Returns:
Path to backup archive or None if no existing installation
"""
if not self.install_dir.exists():
return None
if self.dry_run:
return self.install_dir / "backup_dryrun.tar.gz"
# Create backup directory
backup_dir = self.install_dir / "backups"
backup_dir.mkdir(exist_ok=True)
# Create timestamped backup
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_name = f"superclaude_backup_{timestamp}"
backup_path = backup_dir / f"{backup_name}.tar.gz"
# Create temporary directory for backup
with tempfile.TemporaryDirectory() as temp_dir:
temp_backup = Path(temp_dir) / backup_name
# Ensure temp backup directory exists
temp_backup.mkdir(parents=True, exist_ok=True)
# Copy all files except backups and local directories
for item in self.install_dir.iterdir():
if item.name not in ["backups", "local"]:
try:
if item.is_file():
shutil.copy2(item, temp_backup / item.name)
elif item.is_dir():
shutil.copytree(item, temp_backup / item.name)
except Exception as e:
# Log warning but continue backup process
self.logger.warning(f"Could not backup {item.name}: {e}")
# Always create an archive, even if empty, to ensure it's a valid tarball
base_path = backup_dir / backup_name
shutil.make_archive(str(base_path), "gztar", temp_backup)
if not any(temp_backup.iterdir()):
self.logger.warning(
f"No files to backup, created empty backup archive: {backup_path.name}"
)
self.backup_path = backup_path
return backup_path
def install_component(self, component_name: str, config: Dict[str, Any]) -> bool:
"""
Install a single component
@@ -201,9 +147,9 @@ class Installer:
component = self.components[component_name]
# Framework components (agents, commands, modes, core, mcp_docs) are ALWAYS updated to latest version
# Framework components are ALWAYS updated to latest version
# These are SuperClaude implementation files, not user configurations
framework_components = {'agents', 'commands', 'modes', 'core', 'mcp_docs', 'mcp'}
framework_components = {'framework_docs', 'agents', 'commands', 'modes', 'core', 'mcp'}
if component_name in framework_components:
# Always update framework components to latest version
@@ -233,13 +179,17 @@ class Installer:
self.failed_components.add(component_name)
return False
# Perform installation
# Perform installation or update
try:
if self.dry_run:
self.logger.info(f"[DRY RUN] Would install {component_name}")
success = True
else:
success = component.install(config)
# If component is already installed and this is a framework component, call update() instead of install()
if component_name in self.installed_components and component_name in framework_components:
success = component.update(config)
else:
success = component.install(config)
if success:
self.installed_components.add(component_name)
@@ -284,15 +234,6 @@ class Installer:
self.logger.error(f" - {error}")
return False
# Create backup if updating
if self.install_dir.exists() and not self.dry_run:
self.logger.info("Creating backup of existing installation...")
try:
self.create_backup()
except Exception as e:
self.logger.error(f"Failed to create backup: {e}")
return False
# Install each component
all_success = True
for name in ordered_names:
@@ -352,7 +293,6 @@ class Installer:
"installed": list(self.installed_components),
"failed": list(self.failed_components),
"skipped": list(self.skipped_components),
"backup_path": str(self.backup_path) if self.backup_path else None,
"install_dir": str(self.install_dir),
"dry_run": self.dry_run,
}
@@ -361,5 +301,4 @@ class Installer:
return {
"updated": list(self.updated_components),
"failed": list(self.failed_components),
"backup_path": str(self.backup_path) if self.backup_path else None,
}

View File

@@ -36,15 +36,6 @@
"enabled": true,
"required_tools": []
},
"mcp_docs": {
"name": "mcp_docs",
"version": "4.1.5",
"description": "MCP server documentation and usage guides",
"category": "documentation",
"dependencies": ["core"],
"enabled": true,
"required_tools": []
},
"agents": {
"name": "agents",
"version": "4.1.5",