| """ |
| Environment configuration management for the MCP server. |
| """ |
|
|
| import ipaddress |
| import json |
| import os |
| from dataclasses import dataclass, field |
| from typing import Any |
| from urllib.parse import urlparse |
|
|
| from jose import jwt |
|
|
|
|
| class ConfigurationError(Exception): |
| """Raised when there's an error in configuration.""" |
|
|
| pass |
|
|
|
|
| @dataclass |
| class EnvironmentConfig: |
| """Configuration loaded from environment variables.""" |
|
|
| supabase_url: str |
| supabase_service_key: str |
| port: int |
| openai_api_key: str | None = None |
| gemini_api_key: str | None = None |
| host: str = "0.0.0.0" |
| transport: str = "sse" |
| token_pricing: dict[str, Any] = field(default_factory=dict) |
|
|
|
|
| @dataclass |
| class RAGStrategyConfig: |
| """Configuration for RAG strategies.""" |
|
|
| use_contextual_embeddings: bool = False |
| use_hybrid_search: bool = True |
| use_agentic_rag: bool = True |
| use_reranking: bool = True |
|
|
|
|
| def validate_openai_api_key(api_key: str) -> bool: |
| """Validate OpenAI API key format, allowing test keys in test environments.""" |
| if not api_key: |
| raise ConfigurationError("OpenAI API key cannot be empty") |
|
|
| |
| if os.getenv("PYTEST_CURRENT_TEST") and api_key.startswith("test_"): |
| return True |
|
|
| if not api_key.startswith("sk-"): |
| raise ConfigurationError("OpenAI API key must start with 'sk-'") |
|
|
| return True |
|
|
|
|
| def validate_supabase_key(supabase_key: str) -> tuple[bool, str]: |
| """Validate Supabase key type and return validation result. |
| |
| Returns: |
| tuple[bool, str]: (is_valid, message) |
| - (False, "ANON_KEY_DETECTED") if anon key detected |
| - (True, "VALID_SERVICE_KEY") if service key detected |
| - (False, "UNKNOWN_KEY_TYPE:{role}") for unknown roles |
| - (True, "UNABLE_TO_VALIDATE") if JWT cannot be decoded |
| """ |
| if not supabase_key: |
| return False, "EMPTY_KEY" |
|
|
| try: |
| |
| |
| |
| decoded = jwt.decode( |
| supabase_key, |
| "", |
| options={ |
| "verify_signature": False, |
| "verify_aud": False, |
| "verify_exp": False, |
| "verify_nbf": False, |
| "verify_iat": False, |
| }, |
| ) |
| role = decoded.get("role") |
|
|
| if role == "anon": |
| return False, "ANON_KEY_DETECTED" |
| elif role == "service_role": |
| return True, "VALID_SERVICE_KEY" |
| else: |
| return False, f"UNKNOWN_KEY_TYPE:{role}" |
|
|
| except Exception: |
| |
| |
| return True, "UNABLE_TO_VALIDATE" |
|
|
|
|
| def validate_supabase_url(url: str) -> bool: |
| """Validate Supabase URL format.""" |
| if not url: |
| raise ConfigurationError("Supabase URL cannot be empty") |
|
|
| parsed = urlparse(url) |
| |
| if parsed.scheme not in ("http", "https"): |
| raise ConfigurationError("Supabase URL must use HTTP or HTTPS") |
|
|
| |
| if parsed.scheme == "http": |
| hostname = parsed.hostname or "" |
|
|
| |
| local_hosts = ["localhost", "127.0.0.1", "host.docker.internal"] |
| if hostname in local_hosts or hostname.endswith(".localhost"): |
| return True |
|
|
| |
| try: |
| ip = ipaddress.ip_address(hostname) |
| |
| |
| |
| |
| |
| |
| if (ip.is_private or ip.is_loopback or ip.is_link_local) and not ip.is_unspecified: |
| return True |
| except ValueError: |
| |
| pass |
|
|
| |
| raise ConfigurationError(f"Supabase URL must use HTTPS for non-local environments (hostname: {hostname})") |
|
|
| if not parsed.netloc: |
| raise ConfigurationError("Invalid Supabase URL format") |
|
|
| return True |
|
|
|
|
| def load_environment_config() -> EnvironmentConfig: |
| """Load and validate environment configuration.""" |
| |
| openai_api_key = os.getenv("OPENAI_API_KEY") |
|
|
| |
| supabase_url = os.getenv("SUPABASE_URL") |
| if not supabase_url: |
| raise ConfigurationError("SUPABASE_URL environment variable is required") |
|
|
| supabase_service_key = os.getenv("SUPABASE_SERVICE_KEY") |
| if not supabase_service_key: |
| raise ConfigurationError("SUPABASE_SERVICE_KEY environment variable is required") |
|
|
| |
| if openai_api_key: |
| validate_openai_api_key(openai_api_key) |
| validate_supabase_url(supabase_url) |
|
|
| |
| is_valid_key, key_message = validate_supabase_key(supabase_service_key) |
| if not is_valid_key: |
| if key_message == "ANON_KEY_DETECTED": |
| raise ConfigurationError( |
| "CRITICAL: You are using a Supabase ANON key instead of a SERVICE key.\n\n" |
| "The ANON key is a public key with read-only permissions that cannot write to the database.\n" |
| "This will cause all database operations to fail with 'permission denied' errors.\n\n" |
| "To fix this:\n" |
| "1. Go to your Supabase project dashboard\n" |
| "2. Navigate to Settings > API keys\n" |
| "3. Find the 'service_role' key (NOT the 'anon' key)\n" |
| "4. Update your SUPABASE_SERVICE_KEY environment variable\n\n" |
| "Key characteristics:\n" |
| "- ANON key: Starts with 'eyJ...' and has role='anon' (public, read-only)\n" |
| "- SERVICE key: Starts with 'eyJ...' and has role='service_role' (private, full access)\n\n" |
| "Current key role detected: anon" |
| ) |
| elif key_message.startswith("UNKNOWN_KEY_TYPE:"): |
| role = key_message.split(":", 1)[1] |
| raise ConfigurationError( |
| f"CRITICAL: Unknown Supabase key role '{role}'.\n\n" |
| f"Expected 'service_role' but found '{role}'.\n" |
| f"This key type is not supported and will likely cause failures.\n\n" |
| f"Please use a valid service_role key from your Supabase dashboard." |
| ) |
| |
|
|
| |
| host = os.getenv("HOST", "0.0.0.0") |
| port_str = os.getenv("PORT") |
| if not port_str: |
| |
| port_str = os.getenv("ARCHON_MCP_PORT") |
| if not port_str: |
| raise ConfigurationError( |
| "PORT or ARCHON_MCP_PORT environment variable is required. " |
| "Please set it in your .env file or environment. " |
| "Default value: 8051" |
| ) |
| transport = os.getenv("TRANSPORT", "sse") |
|
|
| |
| try: |
| port = int(port_str) |
| except ValueError as e: |
| raise ConfigurationError(f"PORT must be a valid integer, got: {port_str}") from e |
|
|
| |
| |
| |
| token_pricing_json = os.getenv("TOKEN_PRICING_JSON") |
| try: |
| from ..utils import get_supabase_client |
|
|
| db_res = ( |
| get_supabase_client().table("archon_settings").select("value").eq("key", "TOKEN_PRICING_JSON").execute() |
| ) |
| if db_res.data: |
| token_pricing_json = db_res.data[0]["value"] |
| except Exception: |
| pass |
| default_pricing = { |
| "gpt-4o": {"input": 2.50, "output": 10.00}, |
| "gpt-4o-mini": {"input": 0.15, "output": 0.60}, |
| "gemini-3.1-pro-preview": {"input": 1.25, "output": 5.00}, |
| "gemini-3.1-flash": {"input": 0.10, "output": 0.40}, |
| "gemini-3.1-flash-lite": {"input": 0.05, "output": 0.20}, |
| "text-embedding-004": {"input": 0.02, "output": 0.00}, |
| "ollama": {"input": 0.00, "output": 0.00}, |
| } |
|
|
| token_pricing = default_pricing |
| if token_pricing_json: |
| try: |
| custom_pricing = json.loads(token_pricing_json) |
| |
| token_pricing.update(custom_pricing) |
| except json.JSONDecodeError: |
| |
| pass |
|
|
| return EnvironmentConfig( |
| openai_api_key=openai_api_key, |
| supabase_url=supabase_url, |
| supabase_service_key=supabase_service_key, |
| host=host, |
| port=port, |
| transport=transport, |
| token_pricing=token_pricing, |
| ) |
|
|
|
|
| def get_config() -> EnvironmentConfig: |
| """Get environment configuration with validation.""" |
| return load_environment_config() |
|
|
|
|
| def get_rag_strategy_config() -> RAGStrategyConfig: |
| """Load RAG strategy configuration from environment variables.""" |
|
|
| def str_to_bool(value: str | None) -> bool: |
| """Convert string environment variable to boolean.""" |
| if value is None: |
| return False |
| return value.lower() in ("true", "1", "yes", "on") |
|
|
| return RAGStrategyConfig( |
| use_contextual_embeddings=str_to_bool(os.getenv("USE_CONTEXTUAL_EMBEDDINGS")), |
| use_hybrid_search=str_to_bool(os.getenv("USE_HYBRID_SEARCH")), |
| use_agentic_rag=str_to_bool(os.getenv("USE_AGENTIC_RAG")), |
| use_reranking=str_to_bool(os.getenv("USE_RERANKING")), |
| ) |
|
|