Skip to main content
Glama
config.py9.44 kB
"""Configuration management using Pydantic settings with optional file persistence.""" import json import os from pathlib import Path from typing import Any, Literal from urllib.parse import urlparse from pydantic import Field, SecretStr, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict # --- Paths --- APP_NAME = "mcp-server-browser-use" def get_config_dir() -> Path: """Get the configuration directory (e.g. ~/.config/mcp-server-browser-use).""" if os.name == "nt": base = Path(os.environ.get("APPDATA", Path.home() / ".config")).expanduser() else: base = Path("~/.config").expanduser() path = base / APP_NAME path.mkdir(parents=True, exist_ok=True) return path def get_default_results_dir() -> Path: """Get the default directory for saving results.""" base = Path("~/Documents").expanduser() if not base.exists(): base = Path.home() path = base / "mcp-browser-results" return path CONFIG_FILE = get_config_dir() / "config.json" def load_config_file() -> dict[str, Any]: """Load settings from the JSON config file if it exists.""" if not CONFIG_FILE.exists(): return {} try: text = CONFIG_FILE.read_text(encoding="utf-8") if not text.strip(): return {} return json.loads(text) except Exception: return {} def save_config_file(config_data: dict[str, Any]) -> None: """Save settings to the JSON config file.""" CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) CONFIG_FILE.write_text(json.dumps(config_data, indent=2), encoding="utf-8") # Standard environment variable names for API keys (industry convention) # For providers with multiple common env var names, use a list (first match wins) STANDARD_ENV_VAR_NAMES: dict[str, str | list[str]] = { "openai": "OPENAI_API_KEY", "anthropic": "ANTHROPIC_API_KEY", "google": ["GEMINI_API_KEY", "GOOGLE_API_KEY"], # GEMINI_API_KEY takes priority "azure_openai": "AZURE_OPENAI_API_KEY", "groq": "GROQ_API_KEY", "deepseek": "DEEPSEEK_API_KEY", "cerebras": "CEREBRAS_API_KEY", "browser_use": "BROWSER_USE_API_KEY", "openrouter": "OPENROUTER_API_KEY", "vercel": "VERCEL_API_KEY", } # Providers that don't require an API key NO_KEY_PROVIDERS = frozenset({"ollama", "bedrock"}) ProviderType = Literal[ "openai", "anthropic", "google", "azure_openai", "groq", "deepseek", "cerebras", "ollama", "bedrock", "browser_use", "openrouter", "vercel", ] class LLMSettings(BaseSettings): """LLM provider configuration.""" model_config = SettingsConfigDict(env_prefix="MCP_LLM_") provider: ProviderType = Field(default="google") model_name: str = Field(default="gemini-3-flash-preview") api_key: SecretStr | None = Field(default=None, description="Generic API key override (highest priority)") base_url: str | None = Field(default=None, description="Custom base URL for OpenAI-compatible APIs") # Azure OpenAI specific azure_endpoint: str | None = Field(default=None, description="Azure OpenAI endpoint URL") azure_api_version: str | None = Field(default="2024-02-01", description="Azure OpenAI API version") # AWS Bedrock specific aws_region: str | None = Field(default=None, description="AWS region for Bedrock") def get_api_key(self) -> str | None: """Extract API key value from SecretStr (legacy method for backward compat).""" return self.api_key.get_secret_value() if self.api_key else None def get_api_key_for_provider(self) -> str | None: """Resolve API key with priority: generic > standard > MCP-prefixed. Priority order: 1. MCP_LLM_API_KEY (generic override, applies to any provider) 2. <PROVIDER>_API_KEY (standard name, e.g., OPENAI_API_KEY, GEMINI_API_KEY) 3. MCP_LLM_<PROVIDER>_API_KEY (legacy MCP-prefixed, backward compat) Returns: The resolved API key or None if not found. """ # 1. Generic override (highest priority) if self.api_key: return self.api_key.get_secret_value() # 2. Standard env var name(s) (industry convention) standard_vars = STANDARD_ENV_VAR_NAMES.get(self.provider) if standard_vars: # Handle both single string and list of strings if isinstance(standard_vars, str): standard_vars = [standard_vars] for var_name in standard_vars: key = os.environ.get(var_name) if key: return key # 3. MCP-prefixed fallback (backward compatibility) mcp_var = f"MCP_LLM_{self.provider.upper()}_API_KEY" return os.environ.get(mcp_var) def requires_api_key(self) -> bool: """Check if the current provider requires an API key.""" return self.provider not in NO_KEY_PROVIDERS class BrowserSettings(BaseSettings): """Browser configuration.""" model_config = SettingsConfigDict(env_prefix="MCP_BROWSER_") headless: bool = Field(default=True) proxy_server: str | None = Field(default=None, description="Proxy server URL (e.g., http://host:8080)") proxy_bypass: str | None = Field(default=None, description="Comma-separated hosts to bypass proxy") cdp_url: str | None = Field(default=None, description="CDP URL for external browser (e.g., http://localhost:9222)") user_data_dir: str | None = Field(default=None, description="Path to Chrome user data directory for persistent profile") @model_validator(mode="after") def validate_cdp_url(self) -> "BrowserSettings": """Ensure CDP URL is localhost-only for security.""" if self.cdp_url: parsed = urlparse(self.cdp_url) if parsed.hostname not in ("localhost", "127.0.0.1", "::1"): raise ValueError("CDP URL must be localhost for security") return self class AgentSettings(BaseSettings): """Agent behavior configuration.""" model_config = SettingsConfigDict(env_prefix="MCP_AGENT_") max_steps: int = Field(default=20) use_vision: bool = Field(default=True) TransportType = Literal["stdio", "streamable-http", "sse"] class ServerSettings(BaseSettings): """Server configuration.""" model_config = SettingsConfigDict(env_prefix="MCP_SERVER_") logging_level: str = Field(default="INFO") transport: TransportType = Field(default="stdio", description="MCP transport: stdio, streamable-http, or sse") host: str = Field(default="127.0.0.1", description="Host for HTTP transports") port: int = Field(default=8383, description="Port for HTTP transports") results_dir: str | None = Field(default=None, description="Directory to save execution results") auth_token: SecretStr | None = Field(default=None, description="Bearer token for non-localhost access") class ResearchSettings(BaseSettings): """Deep research configuration.""" model_config = SettingsConfigDict(env_prefix="MCP_RESEARCH_") max_searches: int = Field(default=5, description="Maximum number of searches per research task") save_directory: str | None = Field(default=None, description="Directory to save research reports") search_timeout: int = Field(default=120, description="Timeout per search in seconds") class SkillsSettings(BaseSettings): """Browser skills configuration.""" model_config = SettingsConfigDict(env_prefix="MCP_SKILLS_") enabled: bool = Field(default=False, description="Enable skills feature (beta - disabled by default)") directory: str | None = Field(default=None, description="Directory containing skill YAML files (default: ~/.config/browser-skills)") validate_results: bool = Field(default=True, description="Validate execution results against skill success indicators") class AppSettings(BaseSettings): """Root application settings. Priority: Environment Variables > Config File > Defaults """ model_config = SettingsConfigDict(env_prefix="MCP_", extra="ignore") llm: LLMSettings = Field(default_factory=LLMSettings) browser: BrowserSettings = Field(default_factory=BrowserSettings) agent: AgentSettings = Field(default_factory=AgentSettings) server: ServerSettings = Field(default_factory=ServerSettings) research: ResearchSettings = Field(default_factory=ResearchSettings) skills: SkillsSettings = Field(default_factory=SkillsSettings) def save(self) -> Path: """Save current configuration to file (excluding secrets).""" data = self.model_dump(mode="json", exclude_none=True) # Remove secret values from saved config if "llm" in data and "api_key" in data["llm"]: del data["llm"]["api_key"] save_config_file(data) return CONFIG_FILE def get_results_dir(self) -> Path: """Get the results directory, creating if needed.""" if self.server.results_dir: path = Path(self.server.results_dir).expanduser() else: path = get_default_results_dir() path.mkdir(parents=True, exist_ok=True) return path def _load_settings() -> AppSettings: """Load settings with file config as base, env vars overlay.""" file_data = load_config_file() # Pydantic will overlay env vars on top return AppSettings(**file_data) settings = _load_settings()

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/Saik0s/mcp-browser-use'

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