Skip to main content
Glama
mcp_manager.py15.3 kB
#!/usr/bin/env python3 """ MCP Server Manager for Claude Code CLI. This module provides the core functionality for managing MCP servers in the Claude Code CLI configuration. """ import json import os import shutil import sys from pathlib import Path from typing import Dict, List, Optional, Any from dataclasses import dataclass, field, asdict from datetime import datetime import platform import logging logger = logging.getLogger(__name__) # Custom Exceptions class MCPConfigError(Exception): """Base exception for MCP configuration errors.""" pass class MCPServerNotFoundError(MCPConfigError): """Raised when an MCP server is not found.""" pass class MCPServerAlreadyExistsError(MCPConfigError): """Raised when trying to add a server that already exists.""" pass @dataclass class MCPServerConfig: """Configuration for an MCP server.""" name: str command: str args: List[str] = field(default_factory=list) cwd: Optional[str] = None env: Dict[str, str] = field(default_factory=dict) def to_dict(self) -> Dict[str, Any]: """Convert configuration to dictionary format for JSON.""" config = { "command": self.command, "args": self.args } if self.cwd: config["cwd"] = self.cwd if self.env: config["env"] = self.env return config @classmethod def from_dict(cls, name: str, data: Dict[str, Any]) -> 'MCPServerConfig': """Create configuration from dictionary.""" return cls( name=name, command=data.get("command", ""), args=data.get("args", []), cwd=data.get("cwd"), env=data.get("env", {}) ) class MCPManager: """Manager for MCP server configurations in Claude Code CLI.""" # Default config paths for different platforms DEFAULT_CONFIG_PATHS = { "Windows": Path.home() / ".claude" / "claude_code_config.json", "Darwin": Path.home() / ".claude" / "claude_code_config.json", # macOS "Linux": Path.home() / ".claude" / "claude_code_config.json" } # Preset configurations for common MCP servers PRESETS = { "openrouter": { "command": "cmd" if sys.platform == "win32" else "npx", "args": ["/c", "npx", "@physics91/openrouter-mcp"] if sys.platform == "win32" else ["@physics91/openrouter-mcp"], "env": { "OPENROUTER_API_KEY": None, # Will be set by user "OPENROUTER_APP_NAME": "claude-code-mcp", "OPENROUTER_HTTP_REFERER": "https://localhost:3000", "HOST": "localhost", "PORT": "8000", "LOG_LEVEL": "info" }, "description": "OpenRouter API client for multiple AI models via NPX" }, "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem"] }, "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"] }, "memory": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-memory"] } } def __init__(self, config_path: Optional[Path] = None): """Initialize the MCP Manager. Args: config_path: Optional path to the configuration file. If not provided, uses the default for the current platform. """ if config_path is None: system = platform.system() config_path = self.DEFAULT_CONFIG_PATHS.get( system, self.DEFAULT_CONFIG_PATHS["Linux"] ) self.config_path = Path(config_path) self.config = self._load_config() def _load_config(self) -> Dict[str, Any]: """Load configuration from file.""" if not self.config_path.exists(): # Create default config if file doesn't exist self._ensure_config_dir() default_config = {"mcpServers": {}} self._save_config(default_config) return default_config try: with open(self.config_path, 'r', encoding='utf-8') as f: config = json.load(f) # Ensure mcpServers key exists if "mcpServers" not in config: config["mcpServers"] = {} return config except json.JSONDecodeError as e: raise MCPConfigError(f"Invalid configuration file: {e}") except Exception as e: raise MCPConfigError(f"Failed to load configuration: {e}") def _save_config(self, config: Optional[Dict[str, Any]] = None) -> None: """Save configuration to file.""" if config is None: config = self.config try: self._ensure_config_dir() with open(self.config_path, 'w', encoding='utf-8') as f: json.dump(config, f, indent=2, ensure_ascii=False) except Exception as e: raise MCPConfigError(f"Failed to save configuration: {e}") def _ensure_config_dir(self) -> None: """Ensure the configuration directory exists.""" config_dir = self.config_path.parent if not config_dir.exists(): config_dir.mkdir(parents=True, exist_ok=True) def save_config(self) -> None: """Public method to save current configuration.""" self._save_config() def add_server(self, config: MCPServerConfig, force: bool = False) -> None: """Add an MCP server to the configuration. Args: config: Server configuration to add force: If True, overwrites existing server with the same name Raises: MCPServerAlreadyExistsError: If server already exists and force is False """ if config.name in self.config["mcpServers"] and not force: raise MCPServerAlreadyExistsError( f"Server '{config.name}' already exists. Use force=True to overwrite." ) # Expand paths if needed if config.cwd: config.cwd = str(Path(config.cwd).expanduser().resolve()) self.config["mcpServers"][config.name] = config.to_dict() self._save_config() logger.info(f"Added MCP server: {config.name}") def remove_server(self, name: str) -> None: """Remove an MCP server from the configuration. Args: name: Name of the server to remove Raises: MCPServerNotFoundError: If server doesn't exist """ if name not in self.config["mcpServers"]: raise MCPServerNotFoundError(f"Server '{name}' not found") del self.config["mcpServers"][name] self._save_config() logger.info(f"Removed MCP server: {name}") def update_server(self, config: MCPServerConfig) -> None: """Update an existing MCP server configuration. Args: config: Updated server configuration Raises: MCPServerNotFoundError: If server doesn't exist """ if config.name not in self.config["mcpServers"]: raise MCPServerNotFoundError(f"Server '{config.name}' not found") # Expand paths if needed if config.cwd: config.cwd = str(Path(config.cwd).expanduser().resolve()) self.config["mcpServers"][config.name] = config.to_dict() self._save_config() logger.info(f"Updated MCP server: {config.name}") def get_server(self, name: str) -> MCPServerConfig: """Get a server configuration by name. Args: name: Name of the server Returns: Server configuration Raises: MCPServerNotFoundError: If server doesn't exist """ if name not in self.config["mcpServers"]: raise MCPServerNotFoundError(f"Server '{name}' not found") return MCPServerConfig.from_dict(name, self.config["mcpServers"][name]) def list_servers(self) -> List[str]: """List all installed MCP servers. Returns: List of server names """ return list(self.config["mcpServers"].keys()) def get_server_status(self, name: str) -> Dict[str, Any]: """Get detailed status of an MCP server. Args: name: Name of the server Returns: Dictionary with server status information Raises: MCPServerNotFoundError: If server doesn't exist """ config = self.get_server(name) return { "name": config.name, "installed": True, "command": config.command, "args": config.args, "cwd": config.cwd, "env": config.env, "config_path": str(self.config_path) } def validate_server(self, config: MCPServerConfig) -> bool: """Validate server configuration. Args: config: Server configuration to validate Returns: True if configuration is valid Raises: MCPConfigError: If configuration is invalid """ if not config.name: raise MCPConfigError("Server name is required") if not config.command: raise MCPConfigError("Server command is required") # Check if command exists (basic validation) if config.command in ["python", "node", "npx", "npm", "dotnet", "java"]: # Common commands are assumed to be valid return True # For absolute paths, check if file exists command_path = Path(config.command) if command_path.is_absolute() and not command_path.exists(): raise MCPConfigError(f"Command not found: {config.command}") return True def backup_config(self) -> Path: """Create a backup of the current configuration. Returns: Path to the backup file """ import time # Use timestamp with microseconds to ensure uniqueness timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # Add a small counter to ensure uniqueness counter = 0 while True: if counter == 0: backup_filename = f"{self.config_path.stem}.{timestamp}.backup" else: backup_filename = f"{self.config_path.stem}.{timestamp}_{counter}.backup" backup_path = self.config_path.parent / backup_filename # Check if file already exists if not backup_path.exists(): break counter += 1 if counter > 100: # Safety check to avoid infinite loop raise MCPConfigError("Unable to create unique backup filename") shutil.copy2(self.config_path, backup_path) logger.info(f"Created configuration backup: {backup_path}") return backup_path def restore_config(self, backup_path: Path) -> None: """Restore configuration from a backup. Args: backup_path: Path to the backup file Raises: MCPConfigError: If backup file doesn't exist or is invalid """ if not backup_path.exists(): raise MCPConfigError(f"Backup file not found: {backup_path}") try: # Validate backup file with open(backup_path, 'r', encoding='utf-8') as f: backup_config = json.load(f) if "mcpServers" not in backup_config: raise MCPConfigError("Invalid backup file: missing mcpServers") # Create current backup before restoring current_backup = self.backup_config() try: # Restore from backup shutil.copy2(backup_path, self.config_path) # Reload configuration after restore self.config = self._load_config() logger.info(f"Restored configuration from: {backup_path}") except Exception as restore_error: # If restore fails, try to rollback if current_backup.exists(): shutil.copy2(current_backup, self.config_path) self.config = self._load_config() raise MCPConfigError(f"Failed to restore, rolled back: {restore_error}") except json.JSONDecodeError: raise MCPConfigError("Invalid backup file: not valid JSON") except MCPConfigError: raise except Exception as e: raise MCPConfigError(f"Failed to restore configuration: {e}") def add_server_from_preset(self, preset_name: str, **kwargs) -> bool: """Add a server using a preset configuration. Args: preset_name: Name of the preset (e.g., 'openrouter', 'filesystem') **kwargs: Additional parameters for the preset (e.g., api_key) Returns: True if server was added successfully Raises: MCPConfigError: If preset doesn't exist """ if preset_name not in self.PRESETS: available = ", ".join(self.PRESETS.keys()) raise MCPConfigError( f"Unknown preset '{preset_name}'. Available presets: {available}" ) preset = self.PRESETS[preset_name].copy() # Handle special parameters for specific presets if preset_name == "openrouter": if "api_key" in kwargs: if "env" not in preset: preset["env"] = {} preset["env"]["OPENROUTER_API_KEY"] = kwargs["api_key"] # Set CWD to current OpenRouter project directory preset["cwd"] = str(Path(__file__).parent.parent.parent.parent.resolve()) elif preset_name == "filesystem": # Add directory arguments if provided if "directories" in kwargs: preset["args"].extend(kwargs["directories"]) elif preset_name == "github": if "token" in kwargs: if "env" not in preset: preset["env"] = {} preset["env"]["GITHUB_PERSONAL_ACCESS_TOKEN"] = kwargs["token"] # Create server config from preset config = MCPServerConfig( name=preset_name, command=preset["command"], args=preset.get("args", []), cwd=preset.get("cwd"), env=preset.get("env", {}) ) # Add the server self.add_server(config, force=kwargs.get("force", False)) return True

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/physics91/openrouter-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server