"""Configuration management for the File System MCP Server."""
import json
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Dict, Any
logger = logging.getLogger(__name__)
@dataclass
class Config:
"""Configuration settings for the File System MCP Server."""
backup_directory: str = "./.mcp_backups"
max_file_size: int = 10 * 1024 * 1024 # 10MB
max_recursion_depth: int = 10
allowed_extensions: Optional[List[str]] = None # None = all allowed
protected_paths: List[str] = field(default_factory=lambda: ["/etc", "/usr", "/bin", "/System"])
enable_backups: bool = True
log_level: str = "INFO"
def __post_init__(self) -> None:
"""Validate configuration after initialization."""
self._validate_config()
def _validate_config(self) -> None:
"""Validate configuration values."""
if self.max_file_size <= 0:
raise ValueError("max_file_size must be positive")
if self.max_recursion_depth <= 0:
raise ValueError("max_recursion_depth must be positive")
if self.log_level not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
logger.warning(f"Invalid log_level '{self.log_level}', using INFO")
self.log_level = "INFO"
# Ensure backup directory path is valid
try:
Path(self.backup_directory).resolve()
except Exception as e:
raise ValueError(f"Invalid backup_directory path: {e}")
class ConfigManager:
"""Manages configuration loading and validation."""
DEFAULT_CONFIG_PATH = "config.json"
@classmethod
def load_config(cls, config_path: Optional[str] = None) -> Config:
"""Load configuration from file or create default."""
config_file = Path(config_path or cls.DEFAULT_CONFIG_PATH)
if config_file.exists():
try:
return cls._load_from_file(config_file)
except Exception as e:
logger.error(f"Failed to load config from {config_file}: {e}")
logger.info("Using default configuration")
return cls._create_default_config(config_file)
else:
logger.info(f"Config file {config_file} not found, creating default")
return cls._create_default_config(config_file)
@classmethod
def _load_from_file(cls, config_path: Path) -> Config:
"""Load configuration from JSON file."""
try:
with open(config_path, 'r', encoding='utf-8') as f:
config_data = json.load(f)
# Filter out unknown fields
valid_fields = {f.name for f in Config.__dataclass_fields__.values()}
filtered_data = {k: v for k, v in config_data.items() if k in valid_fields}
config = Config(**filtered_data)
logger.info(f"Configuration loaded from {config_path}")
return config
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in config file: {e}")
except TypeError as e:
raise ValueError(f"Invalid configuration format: {e}")
@classmethod
def _create_default_config(cls, config_path: Path) -> Config:
"""Create and save default configuration."""
config = Config()
try:
cls.save_config(config, config_path)
logger.info(f"Default configuration created at {config_path}")
except Exception as e:
logger.warning(f"Could not save default config to {config_path}: {e}")
return config
@classmethod
def save_config(cls, config: Config, config_path: Path) -> None:
"""Save configuration to JSON file."""
config_dict = {
"backup_directory": config.backup_directory,
"max_file_size": config.max_file_size,
"max_recursion_depth": config.max_recursion_depth,
"allowed_extensions": config.allowed_extensions,
"protected_paths": config.protected_paths,
"enable_backups": config.enable_backups,
"log_level": config.log_level
}
# Ensure parent directory exists
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(config_dict, f, indent=2)
@classmethod
def validate_paths(cls, config: Config) -> List[str]:
"""Validate that configured paths are accessible."""
issues = []
# Check backup directory
try:
backup_path = Path(config.backup_directory).resolve()
backup_path.mkdir(parents=True, exist_ok=True)
if not backup_path.is_dir():
issues.append(f"Backup directory is not accessible: {backup_path}")
except Exception as e:
issues.append(f"Cannot access backup directory: {e}")
return issues