Skip to main content
Glama
quellant

OpenSCAD MCP Server

by quellant
config.py9.36 kB
""" Configuration management for OpenSCAD MCP Server. This module provides centralized configuration with environment variable support, type safety, and sensible defaults. """ import os from pathlib import Path from typing import Optional import yaml from dotenv import load_dotenv from pydantic import BaseModel, Field, field_validator from ..types import TransportType class RenderingConfig(BaseModel): """Rendering-specific configuration.""" max_concurrent: int = Field( 5, ge=1, le=20, description="Maximum concurrent rendering operations", ) queue_size: int = Field( 100, ge=10, le=1000, description="Maximum queue size", ) timeout_seconds: int = Field( 300, ge=30, le=3600, description="Render timeout in seconds", ) max_image_width: int = Field( 4096, ge=100, le=8192, description="Maximum image width", ) max_image_height: int = Field( 4096, ge=100, le=8192, description="Maximum image height", ) max_animation_frames: int = Field( 360, ge=2, le=1000, description="Maximum animation frames", ) default_color_scheme: str = Field( "Cornfield", description="Default OpenSCAD color scheme", ) class CacheConfig(BaseModel): """Cache configuration.""" enabled: bool = Field(True, description="Enable caching") directory: Path = Field( Path.home() / ".cache" / "openscad-mcp", description="Cache directory", ) max_size_mb: int = Field( 500, ge=100, le=10000, description="Maximum cache size in MB", ) ttl_hours: int = Field( 24, ge=1, le=168, description="Cache time-to-live in hours", ) @field_validator("directory") @classmethod def ensure_directory(cls, v: Path) -> Path: """Ensure cache directory exists.""" v.mkdir(parents=True, exist_ok=True) return v class SecurityConfig(BaseModel): """Security configuration.""" rate_limit: int = Field( 60, ge=0, le=1000, description="Max requests per minute (0 to disable)", ) max_file_size_mb: int = Field( 10, ge=1, le=100, description="Maximum SCAD file size in MB", ) allowed_paths: Optional[list[str]] = Field( None, description="Allowed paths for file access", ) class LoggingConfig(BaseModel): """Logging configuration.""" level: str = Field( "INFO", description="Logging level", pattern="^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$", ) file: Optional[Path] = Field( None, description="Log file path", ) max_size_mb: int = Field( 100, ge=10, le=1000, description="Maximum log file size in MB", ) rotate_count: int = Field( 5, ge=1, le=10, description="Number of rotated log files to keep", ) class ServerConfig(BaseModel): """Server configuration.""" name: str = Field( "OpenSCAD MCP Server", description="Server name", ) version: str = Field( "0.1.0", description="Server version", ) transport: TransportType = Field( TransportType.STDIO, description="Transport type", ) host: str = Field( "localhost", description="Host for HTTP/SSE transport", ) port: int = Field( 8000, ge=1024, le=65535, description="Port for HTTP/SSE transport", ) class Config(BaseModel): """Main configuration class.""" # Paths openscad_path: Optional[str] = Field( None, description="Path to OpenSCAD executable", ) imagemagick_path: Optional[str] = Field( None, description="Path to ImageMagick convert command", ) temp_dir: Path = Field( Path.cwd() / ".openscad-mcp" / "tmp", description="Temporary file directory", ) # Sub-configurations server: ServerConfig = Field(default_factory=ServerConfig) rendering: RenderingConfig = Field(default_factory=RenderingConfig) cache: CacheConfig = Field(default_factory=CacheConfig) security: SecurityConfig = Field(default_factory=SecurityConfig) logging: LoggingConfig = Field(default_factory=LoggingConfig) @field_validator("temp_dir") @classmethod def ensure_temp_dir(cls, v: Path) -> Path: """Ensure temp directory exists.""" v.mkdir(parents=True, exist_ok=True) return v @classmethod def from_env(cls, env_file: Optional[str] = None) -> "Config": """ Load configuration from environment variables. Args: env_file: Optional path to .env file Returns: Configured Config instance """ if env_file: load_dotenv(env_file) else: load_dotenv() # Load from default .env # Build configuration from environment config_dict = {} # Paths if openscad := os.getenv("OPENSCAD_PATH"): config_dict["openscad_path"] = openscad if imagemagick := os.getenv("IMAGEMAGICK_PATH"): config_dict["imagemagick_path"] = imagemagick if temp_dir := os.getenv("MCP_TEMP_DIR"): config_dict["temp_dir"] = Path(temp_dir) # Server configuration server_config = {} if transport := os.getenv("MCP_TRANSPORT"): server_config["transport"] = transport if host := os.getenv("MCP_HOST"): server_config["host"] = host if port := os.getenv("MCP_PORT"): server_config["port"] = int(port) if server_config: config_dict["server"] = ServerConfig(**server_config) # Rendering configuration rendering_config = {} if max_concurrent := os.getenv("MCP_MAX_CONCURRENT_RENDERS"): rendering_config["max_concurrent"] = int(max_concurrent) if queue_size := os.getenv("MCP_QUEUE_SIZE"): rendering_config["queue_size"] = int(queue_size) if timeout := os.getenv("MCP_RENDER_TIMEOUT"): rendering_config["timeout_seconds"] = int(timeout) if max_width := os.getenv("MCP_MAX_IMAGE_WIDTH"): rendering_config["max_image_width"] = int(max_width) if max_height := os.getenv("MCP_MAX_IMAGE_HEIGHT"): rendering_config["max_image_height"] = int(max_height) if max_frames := os.getenv("MCP_MAX_ANIMATION_FRAMES"): rendering_config["max_animation_frames"] = int(max_frames) if rendering_config: config_dict["rendering"] = RenderingConfig(**rendering_config) # Cache configuration cache_config = {} if cache_enabled := os.getenv("MCP_CACHE_ENABLED"): cache_config["enabled"] = cache_enabled.lower() == "true" if cache_size := os.getenv("MCP_CACHE_SIZE_MB"): cache_config["max_size_mb"] = int(cache_size) if cache_ttl := os.getenv("MCP_CACHE_TTL_HOURS"): cache_config["ttl_hours"] = int(cache_ttl) if cache_config: config_dict["cache"] = CacheConfig(**cache_config) # Security configuration security_config = {} if rate_limit := os.getenv("MCP_RATE_LIMIT"): security_config["rate_limit"] = int(rate_limit) if max_file_size := os.getenv("MCP_MAX_FILE_SIZE_MB"): security_config["max_file_size_mb"] = int(max_file_size) if security_config: config_dict["security"] = SecurityConfig(**security_config) # Logging configuration logging_config = {} if log_level := os.getenv("MCP_LOG_LEVEL"): logging_config["level"] = log_level if log_file := os.getenv("MCP_LOG_FILE"): logging_config["file"] = Path(log_file) if logging_config: config_dict["logging"] = LoggingConfig(**logging_config) return cls(**config_dict) @classmethod def from_yaml(cls, yaml_file: str) -> "Config": """ Load configuration from YAML file. Args: yaml_file: Path to YAML configuration file Returns: Configured Config instance """ with open(yaml_file, "r") as f: config_dict = yaml.safe_load(f) return cls(**config_dict) def to_yaml(self, yaml_file: str) -> None: """ Save configuration to YAML file. Args: yaml_file: Path to save YAML configuration """ with open(yaml_file, "w") as f: yaml.dump(self.model_dump(), f, default_flow_style=False) # Global configuration instance _config: Optional[Config] = None def get_config() -> Config: """ Get the global configuration instance. Returns: The global Config instance """ global _config if _config is None: _config = Config.from_env() return _config def set_config(config: Config) -> None: """ Set the global configuration instance. Args: config: Config instance to set globally """ global _config _config = config

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/quellant/openscad-mcp'

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