Skip to main content
Glama
config.py8.56 kB
""" Configuration management for the MCP Play Sound Server. This module handles loading, validation, and management of server configuration from environment variables and provides sensible defaults. """ import os from dataclasses import dataclass from pathlib import Path from typing import Optional, Set import logging logger = logging.getLogger(__name__) class ConfigurationError(Exception): """Raised when configuration is invalid.""" pass @dataclass class ServerConfig: """Configuration for the MCP Play Sound Server.""" # Core configuration custom_sound_path: Optional[str] = None volume_level: float = 0.8 enable_fallback: bool = True audio_device: Optional[str] = None # Advanced configuration max_file_size_mb: int = 10 playback_timeout_seconds: int = 30 audio_backend: str = "auto" enable_audio_cache: bool = True cache_size_limit: int = 5 # Security configuration allowed_audio_extensions: Set[str] = None restrict_to_user_home: bool = True def __post_init__(self): """Initialize default values that need computation.""" if self.allowed_audio_extensions is None: self.allowed_audio_extensions = {'.wav', '.mp3', '.flac', '.ogg', '.m4a'} @classmethod def from_environment(cls) -> 'ServerConfig': """Create configuration from environment variables.""" logger.debug("Loading configuration from environment variables") # Parse environment variables with defaults config = cls( custom_sound_path=os.getenv('CUSTOM_SOUND_PATH'), volume_level=cls._parse_float('VOLUME_LEVEL', 0.8), enable_fallback=cls._parse_bool('ENABLE_FALLBACK', True), audio_device=os.getenv('AUDIO_DEVICE'), max_file_size_mb=cls._parse_int('MAX_FILE_SIZE_MB', 10), playback_timeout_seconds=cls._parse_int('PLAYBACK_TIMEOUT_SECONDS', 30), audio_backend=os.getenv('AUDIO_BACKEND', 'auto'), enable_audio_cache=cls._parse_bool('ENABLE_AUDIO_CACHE', True), cache_size_limit=cls._parse_int('CACHE_SIZE_LIMIT', 5), allowed_audio_extensions=cls._parse_extensions('ALLOWED_AUDIO_EXTENSIONS'), restrict_to_user_home=cls._parse_bool('RESTRICT_TO_USER_HOME', True), ) logger.info(f"Configuration loaded: custom_sound={bool(config.custom_sound_path)}, " f"volume={config.volume_level}, fallback={config.enable_fallback}") return config @staticmethod def _parse_float(env_var: str, default: float) -> float: """Parse float from environment variable.""" value = os.getenv(env_var) if value is None: return default try: return float(value) except ValueError: raise ConfigurationError(f"{env_var} must be a valid float, got: {value}") @staticmethod def _parse_int(env_var: str, default: int) -> int: """Parse integer from environment variable.""" value = os.getenv(env_var) if value is None: return default try: return int(value) except ValueError: raise ConfigurationError(f"{env_var} must be a valid integer, got: {value}") @staticmethod def _parse_bool(env_var: str, default: bool) -> bool: """Parse boolean from environment variable.""" value = os.getenv(env_var) if value is None: return default value_lower = value.lower() if value_lower in ('true', '1', 'yes', 'on'): return True elif value_lower in ('false', '0', 'no', 'off'): return False else: raise ConfigurationError(f"{env_var} must be a boolean value, got: {value}") @staticmethod def _parse_extensions(env_var: str) -> Optional[Set[str]]: """Parse comma-separated file extensions.""" value = os.getenv(env_var) if value is None: return None extensions = set() for ext in value.split(','): ext = ext.strip() if not ext.startswith('.'): ext = '.' + ext extensions.add(ext.lower()) return extensions def validate(self) -> None: """Validate all configuration options.""" logger.debug("Validating configuration") self._validate_volume_level() self._validate_file_paths() self._validate_numeric_ranges() self._validate_audio_backend() self._validate_security_settings() logger.info("Configuration validation completed successfully") def _validate_volume_level(self) -> None: """Validate volume level is in valid range.""" if not 0.0 <= self.volume_level <= 1.0: raise ConfigurationError( f"VOLUME_LEVEL must be between 0.0 and 1.0, got: {self.volume_level}" ) def _validate_file_paths(self) -> None: """Validate file paths and permissions.""" if self.custom_sound_path: path = Path(self.custom_sound_path) # Check if file exists if not path.exists(): raise ConfigurationError( f"CUSTOM_SOUND_PATH file does not exist: {self.custom_sound_path}" ) # Check if file is readable if not path.is_file(): raise ConfigurationError( f"CUSTOM_SOUND_PATH is not a file: {self.custom_sound_path}" ) # Check file extension if path.suffix.lower() not in self.allowed_audio_extensions: raise ConfigurationError( f"Audio file extension '{path.suffix}' not in ALLOWED_AUDIO_EXTENSIONS: " f"{','.join(sorted(self.allowed_audio_extensions))}" ) # Check file size file_size_mb = path.stat().st_size / (1024 * 1024) if file_size_mb > self.max_file_size_mb: raise ConfigurationError( f"Custom audio file exceeds MAX_FILE_SIZE_MB limit ({self.max_file_size_mb}MB): " f"{path.name} ({file_size_mb:.1f}MB)" ) # Check user home restriction if self.restrict_to_user_home: home_path = Path.home() try: path.resolve().relative_to(home_path.resolve()) except ValueError: raise ConfigurationError( f"CUSTOM_SOUND_PATH must be within user home directory when " f"RESTRICT_TO_USER_HOME is enabled: {self.custom_sound_path}" ) def _validate_numeric_ranges(self) -> None: """Validate numeric configuration ranges.""" if not 1 <= self.max_file_size_mb <= 100: raise ConfigurationError( f"MAX_FILE_SIZE_MB must be between 1 and 100, got: {self.max_file_size_mb}" ) if not 1 <= self.playback_timeout_seconds <= 300: raise ConfigurationError( f"PLAYBACK_TIMEOUT_SECONDS must be between 1 and 300, got: {self.playback_timeout_seconds}" ) if not 1 <= self.cache_size_limit <= 20: raise ConfigurationError( f"CACHE_SIZE_LIMIT must be between 1 and 20, got: {self.cache_size_limit}" ) def _validate_audio_backend(self) -> None: """Validate audio backend option.""" valid_backends = {'auto', 'simpleaudio', 'pydub', 'system'} if self.audio_backend not in valid_backends: raise ConfigurationError( f"AUDIO_BACKEND must be one of {valid_backends}, got: {self.audio_backend}" ) def _validate_security_settings(self) -> None: """Validate security-related settings.""" # Validate allowed extensions for ext in self.allowed_audio_extensions: if not ext.startswith('.'): raise ConfigurationError( f"Audio extensions must start with '.', got: {ext}" ) if len(ext) < 2: raise ConfigurationError( f"Audio extensions must have at least one character after '.', got: {ext}" )

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/davidteren/play-sound-mcp-server'

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