"""Runtime configuration. Every setting can be given as environment variable (prefix ``MCP_CALC_``) or as CLI flag. CLI flags win over environment variables, which win over defaults. """ from __future__ import annotations import argparse import os from dataclasses import dataclass, field, fields from typing import Literal ENV_PREFIX = "MCP_CALC_" Transport = Literal["stdio", "http"] ExecutorMode = Literal["process", "inline"] def _csv(value: str) -> list[str]: return [item.strip() for item in value.split(",") if item.strip()] def _bool(value: str) -> bool: normalized = value.strip().lower() if normalized in ("1", "true", "yes", "on"): return True if normalized in ("0", "false", "no", "off"): return False raise ValueError("expected true/false") @dataclass(frozen=True) class Limits: """Guards applied to every computation. Shipped to worker processes.""" max_input_length: int = 5_000 max_output_length: int = 20_000 max_precision: int = 1_000 max_matrix_size: int = 30 max_data_points: int = 1_000_000 max_list_items: int = 10_000 @dataclass(frozen=True) class Settings: transport: Transport = "stdio" # HTTP transport host: str = "127.0.0.1" port: int = 8080 path: str = "/mcp" allowed_hosts: list[str] = field(default_factory=list) allowed_origins: list[str] = field(default_factory=list) trusted_proxies: list[str] = field(default_factory=list) max_request_body_bytes: int = 1_048_576 max_connections: int = 200 metrics: bool = True # Authentication (HTTP only; stdio never authenticates) clients_file: str = "" clients_reload_seconds: float = 5.0 auth_tokens: list[str] = field(default_factory=list) audit_file: str = "" # Admin API (separate port, only started when admin tokens are configured) admin_tokens: list[str] = field(default_factory=list) admin_host: str = "127.0.0.1" admin_port: int = 9090 # Execution executor: ExecutorMode = "process" workers: int = 2 max_queue: int = 20 max_queue_wait_seconds: float = 30.0 client_max_concurrent: int = 10 timeout_seconds: float = 10.0 max_tasks_per_worker: int = 200 memory_limit_mb: int = 512 limits: Limits = field(default_factory=Limits) # Logging log_level: str = "INFO" log_format: Literal["text", "json"] = "text" # name -> (parser, help text). Names map to MCP_CALC_ and --. _OPTIONS: dict[str, tuple[type | object, str]] = { "transport": (str, "stdio | http"), "host": (str, "HTTP bind address (use 0.0.0.0 inside containers)"), "port": (int, "HTTP port"), "path": (str, "HTTP path of the MCP endpoint"), "allowed_hosts": (_csv, "comma-separated Host header values; enables DNS-rebinding protection"), "allowed_origins": (_csv, "comma-separated Origin header values (browser clients only)"), "trusted_proxies": (_csv, "comma-separated IPs/CIDRs of proxies whose X-Forwarded-For is trusted (e.g. the OpenShift router)"), "max_request_body_bytes": (int, "maximum HTTP request body size"), "max_connections": (int, "concurrent HTTP connections; a new connection that would reach this number is answered with 503"), "metrics": (_bool, "expose Prometheus metrics at /metrics (HTTP only): true | false"), "clients_file": (str, "JSON file with API clients (tokens, limits, networks); enables authentication"), "clients_reload_seconds": (float, "how often the clients file is checked for changes"), "auth_tokens": (_csv, "static bearer tokens as name:token (comma-separated); simple alternative to clients_file"), "audit_file": (str, "audit log of client changes (default: audit.log next to clients_file)"), "admin_tokens": (_csv, "admin API tokens as name:token; the admin API only starts when set"), "admin_host": (str, "admin API bind address (127.0.0.1 = only via oc port-forward/exec)"), "admin_port": (int, "admin API port"), "executor": (str, "process (isolated worker pool, default) | inline (no isolation, dev/tests only)"), "workers": (int, "number of worker processes"), "max_queue": (int, "computations that may wait for a free worker; beyond that requests are rejected as busy"), "max_queue_wait_seconds": (float, "maximum time a computation waits for a free worker before it is rejected as busy"), "client_max_concurrent": (int, "default limit of concurrent computations per client (0 = unlimited); overridable per client"), "timeout_seconds": (float, "hard timeout per computation"), "max_tasks_per_worker": (int, "recycle a worker after N tasks (0 = never)"), "memory_limit_mb": (int, "extra memory a computation may allocate per worker (Linux only, 0 = off)"), "max_input_length": (int, "maximum characters per expression"), "max_output_length": (int, "results longer than this are truncated"), "max_precision": (int, "maximum significant digits for numeric evaluation"), "max_matrix_size": (int, "maximum rows/columns of a matrix"), "max_data_points": (int, "maximum values per statistics data set"), "max_list_items": (int, "maximum items in other list inputs"), "log_level": (str, "DEBUG | INFO | WARNING | ERROR"), "log_format": (str, "text | json"), } _LIMIT_FIELDS = {f.name for f in fields(Limits)} def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="mcp-calculator", description="MCP server with exact math tools (stdio or Streamable HTTP).", epilog=f"Every option can also be set via environment variable {ENV_PREFIX}