Add API key management during SuperClaude MCP setup

Features:
- Secure API key collection via getpass (hidden input)
- Cross-platform environment variable setup
- Automatic .claude.json configuration with ${ENV_VAR} syntax
- Seamless integration with existing MCP server selection flow
- Skip options for manual configuration later

Implementation:
- Added prompt_api_key() function to setup/utils/ui.py
- Created setup/utils/environment.py for cross-platform env management
- Enhanced MCP server selection in setup/cli/commands/install.py
- Updated MCP component to handle API key configuration
- Preserves user customizations while adding environment variables

Security:
- Hidden input prevents API keys from being displayed
- No logging of sensitive data
- OS-native environment variable storage
- Basic validation with user confirmation

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
NomenAK
2025-08-15 13:48:14 +02:00
parent c05aa872b2
commit 01b8d2a05a
5 changed files with 1221 additions and 2 deletions

View File

@@ -15,8 +15,9 @@ from ...services.config import ConfigService
from ...core.validator import Validator
from ...utils.ui import (
display_header, display_info, display_success, display_error,
display_warning, Menu, confirm, ProgressBar, Colors, format_size
display_warning, Menu, confirm, ProgressBar, Colors, format_size, prompt_api_key
)
from ...utils.environment import setup_environment_variables
from ...utils.logger import get_logger
from ... import DEFAULT_INSTALL_DIR, PROJECT_ROOT, DATA_DIR
from . import OperationBase
@@ -127,8 +128,47 @@ def get_components_to_install(args: argparse.Namespace, registry: ComponentRegis
return interactive_component_selection(registry, config_manager)
def collect_api_keys_for_servers(selected_servers: List[str], mcp_instance) -> Dict[str, str]:
"""
Collect API keys for servers that require them
Args:
selected_servers: List of selected server keys
mcp_instance: MCP component instance
Returns:
Dictionary of environment variable names to API key values
"""
# Filter servers needing keys
servers_needing_keys = [
(server_key, mcp_instance.mcp_servers[server_key])
for server_key in selected_servers
if server_key in mcp_instance.mcp_servers and
mcp_instance.mcp_servers[server_key].get("requires_api_key", False)
]
if not servers_needing_keys:
return {}
# Display API key configuration header
print(f"\n{Colors.CYAN}{Colors.BRIGHT}═══ API Key Configuration ═══{Colors.RESET}")
print(f"{Colors.YELLOW}The following servers require API keys for full functionality:{Colors.RESET}\n")
collected_keys = {}
for server_key, server_info in servers_needing_keys:
api_key_env = server_info.get("api_key_env")
service_name = server_info["name"]
if api_key_env:
key = prompt_api_key(service_name, api_key_env)
if key:
collected_keys[api_key_env] = key
return collected_keys
def select_mcp_servers(registry: ComponentRegistry) -> List[str]:
"""Stage 1: MCP Server Selection"""
"""Stage 1: MCP Server Selection with API Key Collection"""
logger = get_logger()
try:
@@ -173,6 +213,16 @@ def select_mcp_servers(registry: ComponentRegistry) -> List[str]:
if selected_servers:
logger.info(f"Selected MCP servers: {', '.join(selected_servers)}")
# NEW: Collect API keys for selected servers
collected_keys = collect_api_keys_for_servers(selected_servers, mcp_instance)
# Set up environment variables
if collected_keys:
setup_environment_variables(collected_keys)
# Store keys for MCP component to use during installation
mcp_instance.collected_api_keys = collected_keys
else:
logger.info("No MCP servers selected")

View File

@@ -75,6 +75,9 @@ class MCPComponent(Component):
# This will be set during installation - initialize as empty list
self.selected_servers: List[str] = []
# Store collected API keys for configuration
self.collected_api_keys: Dict[str, str] = {}
def _lock_file(self, file_handle, exclusive: bool = False):
"""Cross-platform file locking"""
@@ -225,9 +228,23 @@ class MCPComponent(Component):
else:
self.logger.debug(f"Preserved user customization for '{server_name}.{key}'")
# NEW: Apply environment variable references for API keys
if "env" in existing_server and self.collected_api_keys:
for env_key, env_value in existing_server["env"].items():
if env_key in self.collected_api_keys and env_value == "":
# Update to use environment variable reference
existing_server["env"][env_key] = f"${{{env_key}}}"
self.logger.info(f"Configured {env_key} to use environment variable")
self.logger.info(f"Updated existing MCP server '{server_name}' (preserved user customizations)")
else:
# New server - add complete configuration
# Apply environment variable references if we have collected keys
if "env" in server_def and self.collected_api_keys:
for env_key in server_def["env"]:
if env_key in self.collected_api_keys and server_def["env"][env_key] == "":
server_def["env"][env_key] = f"${{{env_key}}}"
existing_config[server_name] = server_def
self.logger.info(f"Added new MCP server '{server_name}' from {server_key}")
@@ -264,6 +281,10 @@ class MCPComponent(Component):
self.set_selected_servers(selected_servers)
# NEW: Log collected API keys information
if hasattr(self, 'collected_api_keys') and self.collected_api_keys:
self.logger.info(f"Using {len(self.collected_api_keys)} collected API keys for configuration")
# Validate prerequisites
success, errors = self.validate_prerequisites()
if not success:

216
setup/utils/environment.py Normal file
View File

@@ -0,0 +1,216 @@
"""
Environment variable management for SuperClaude
Cross-platform utilities for setting up persistent environment variables
"""
import os
import sys
import subprocess
from pathlib import Path
from typing import Dict, Optional
from .ui import display_info, display_success, display_warning, Colors
from .logger import get_logger
def detect_shell_config() -> Optional[Path]:
"""
Detect user's shell configuration file
Returns:
Path to the shell configuration file, or None if not found
"""
home = Path.home()
# Check in order of preference
configs = [
home / ".zshrc", # Zsh (Mac default)
home / ".bashrc", # Bash
home / ".profile", # Generic shell profile
home / ".bash_profile" # Mac Bash profile
]
for config in configs:
if config.exists():
return config
# Default to .bashrc if none exist (will be created)
return home / ".bashrc"
def setup_environment_variables(api_keys: Dict[str, str]) -> bool:
"""
Set up environment variables across platforms
Args:
api_keys: Dictionary of environment variable names to values
Returns:
True if all variables were set successfully, False otherwise
"""
logger = get_logger()
success = True
if not api_keys:
return True
print(f"\n{Colors.BLUE}[INFO] Setting up environment variables...{Colors.RESET}")
for env_var, value in api_keys.items():
try:
# Set for current session
os.environ[env_var] = value
if os.name == 'nt': # Windows
# Use setx for persistent user variable
result = subprocess.run(
['setx', env_var, value],
capture_output=True,
text=True
)
if result.returncode != 0:
display_warning(f"Could not set {env_var} persistently: {result.stderr.strip()}")
success = False
else:
logger.info(f"Windows environment variable {env_var} set persistently")
else: # Unix-like systems
shell_config = detect_shell_config()
# Check if the export already exists
export_line = f'export {env_var}="{value}"'
try:
with open(shell_config, 'r') as f:
content = f.read()
# Check if this environment variable is already set
if f'export {env_var}=' in content:
# Variable exists - don't duplicate
logger.info(f"Environment variable {env_var} already exists in {shell_config.name}")
else:
# Append export to shell config
with open(shell_config, 'a') as f:
f.write(f'\n# SuperClaude API Key\n{export_line}\n')
display_info(f"Added {env_var} to {shell_config.name}")
logger.info(f"Added {env_var} to {shell_config}")
except Exception as e:
display_warning(f"Could not update {shell_config.name}: {e}")
success = False
logger.info(f"Environment variable {env_var} configured for current session")
except Exception as e:
logger.error(f"Failed to set {env_var}: {e}")
display_warning(f"Failed to set {env_var}: {e}")
success = False
if success:
display_success("Environment variables configured successfully")
if os.name != 'nt':
display_info("Restart your terminal or run 'source ~/.bashrc' to apply changes")
else:
display_info("New environment variables will be available in new terminal sessions")
else:
display_warning("Some environment variables could not be set persistently")
display_info("You can set them manually or check the logs for details")
return success
def validate_environment_setup(env_vars: Dict[str, str]) -> bool:
"""
Validate that environment variables are properly set
Args:
env_vars: Dictionary of environment variable names to expected values
Returns:
True if all variables are set correctly, False otherwise
"""
logger = get_logger()
all_valid = True
for env_var, expected_value in env_vars.items():
current_value = os.environ.get(env_var)
if current_value is None:
logger.warning(f"Environment variable {env_var} is not set")
all_valid = False
elif current_value != expected_value:
logger.warning(f"Environment variable {env_var} has unexpected value")
all_valid = False
else:
logger.info(f"Environment variable {env_var} is set correctly")
return all_valid
def get_shell_name() -> str:
"""
Get the name of the current shell
Returns:
Name of the shell (e.g., 'bash', 'zsh', 'fish')
"""
shell_path = os.environ.get('SHELL', '')
if shell_path:
return Path(shell_path).name
return 'unknown'
def create_env_file(api_keys: Dict[str, str], env_file_path: Optional[Path] = None) -> bool:
"""
Create a .env file with the API keys (alternative to shell config)
Args:
api_keys: Dictionary of environment variable names to values
env_file_path: Path to the .env file (defaults to home directory)
Returns:
True if .env file was created successfully, False otherwise
"""
if env_file_path is None:
env_file_path = Path.home() / ".env"
logger = get_logger()
try:
# Read existing .env file if it exists
existing_content = ""
if env_file_path.exists():
with open(env_file_path, 'r') as f:
existing_content = f.read()
# Prepare new content
new_lines = []
for env_var, value in api_keys.items():
line = f'{env_var}="{value}"'
# Check if this variable already exists
if f'{env_var}=' in existing_content:
logger.info(f"Variable {env_var} already exists in .env file")
else:
new_lines.append(line)
# Append new lines if any
if new_lines:
with open(env_file_path, 'a') as f:
if existing_content and not existing_content.endswith('\n'):
f.write('\n')
f.write('# SuperClaude API Keys\n')
for line in new_lines:
f.write(line + '\n')
# Set file permissions (readable only by owner)
env_file_path.chmod(0o600)
display_success(f"Created .env file at {env_file_path}")
logger.info(f"Created .env file with {len(new_lines)} new variables")
return True
except Exception as e:
logger.error(f"Failed to create .env file: {e}")
display_warning(f"Could not create .env file: {e}")
return False

View File

@@ -6,6 +6,7 @@ Cross-platform console UI with colors and progress indication
import sys
import time
import shutil
import getpass
from typing import List, Optional, Any, Dict, Union
from enum import Enum
@@ -339,6 +340,43 @@ def display_table(headers: List[str], rows: List[List[str]], title: str = '') ->
print()
def prompt_api_key(service_name: str, env_var_name: str) -> Optional[str]:
"""
Prompt for API key with security and UX best practices
Args:
service_name: Human-readable service name (e.g., "Magic", "Morphllm")
env_var_name: Environment variable name (e.g., "TWENTYFIRST_API_KEY")
Returns:
API key string if provided, None if skipped
"""
print(f"{Colors.BLUE}[API KEY] {service_name} requires: {Colors.BRIGHT}{env_var_name}{Colors.RESET}")
print(f"{Colors.WHITE}Visit the service documentation to obtain your API key{Colors.RESET}")
print(f"{Colors.YELLOW}Press Enter to skip (you can set this manually later){Colors.RESET}")
try:
# Use getpass for hidden input
api_key = getpass.getpass(f"Enter {env_var_name}: ").strip()
if not api_key:
print(f"{Colors.YELLOW}[SKIPPED] {env_var_name} - set manually later{Colors.RESET}")
return None
# Basic validation (non-empty, reasonable length)
if len(api_key) < 10:
print(f"{Colors.RED}[WARNING] API key seems too short. Continue anyway? (y/N){Colors.RESET}")
if not confirm("", default=False):
return None
print(f"{Colors.GREEN}[✓] {env_var_name} configured{Colors.RESET}")
return api_key
except KeyboardInterrupt:
print(f"\n{Colors.YELLOW}[SKIPPED] {env_var_name}{Colors.RESET}")
return None
def wait_for_key(message: str = "Press Enter to continue...") -> None:
"""Wait for user to press a key"""
try: