Skip to main content
Glama
johnoconnor0

Google Ads MCP Server

by johnoconnor0
config_manager.py15.3 kB
""" Google Ads MCP Configuration Manager Centralized configuration management with: - Environment variable support - Configuration file loading (YAML/JSON) - Per-customer configuration profiles - Feature flags - Validation """ import os import json import yaml import logging from typing import Optional, Dict, Any, List from pathlib import Path from pydantic import BaseModel, Field, field_validator from enum import Enum logger = logging.getLogger(__name__) class CacheBackend(str, Enum): """Cache backend options.""" MEMORY = "memory" REDIS = "redis" NONE = "none" class LogLevel(str, Enum): """Log level options.""" DEBUG = "DEBUG" INFO = "INFO" WARNING = "WARNING" ERROR = "ERROR" CRITICAL = "CRITICAL" class LogFormat(str, Enum): """Log format options.""" JSON = "json" TEXT = "text" class AuthConfig(BaseModel): """Authentication configuration.""" method: str = Field(default="oauth2", description="Authentication method (oauth2 or service_account)") developer_token: Optional[str] = Field(default=None, description="Google Ads developer token") client_id: Optional[str] = Field(default=None, description="OAuth2 client ID") client_secret: Optional[str] = Field(default=None, description="OAuth2 client secret") refresh_token: Optional[str] = Field(default=None, description="OAuth2 refresh token") service_account_key_file: Optional[str] = Field(default=None, description="Service account JSON key file path") login_customer_id: Optional[str] = Field(default=None, description="MCC account ID") @field_validator('login_customer_id') @classmethod def validate_customer_id(cls, v): """Validate customer ID format.""" if v and '-' in v: raise ValueError("Customer ID must not contain hyphens") return v class CacheConfig(BaseModel): """Cache configuration.""" enabled: bool = Field(default=True, description="Enable caching") backend: CacheBackend = Field(default=CacheBackend.MEMORY, description="Cache backend") ttl: int = Field(default=3600, description="Cache TTL in seconds") redis_url: Optional[str] = Field(default=None, description="Redis connection URL") max_size: int = Field(default=1000, description="Maximum cache size (for memory backend)") class ConnectionPoolConfig(BaseModel): """Connection pool configuration.""" size: int = Field(default=10, description="Connection pool size") timeout: int = Field(default=30, description="Connection timeout in seconds") class RateLimitConfig(BaseModel): """Rate limiting configuration.""" enabled: bool = Field(default=True, description="Enable rate limiting") requests_per_minute: int = Field(default=100, description="Maximum requests per minute") burst_size: int = Field(default=10, description="Burst size for rate limiter") class PerformanceConfig(BaseModel): """Performance configuration.""" cache: CacheConfig = Field(default_factory=CacheConfig) connection_pool: ConnectionPoolConfig = Field(default_factory=ConnectionPoolConfig) rate_limiting: RateLimitConfig = Field(default_factory=RateLimitConfig) class RetryConfig(BaseModel): """Retry configuration.""" enabled: bool = Field(default=True, description="Enable automatic retries") max_attempts: int = Field(default=3, description="Maximum retry attempts") backoff: str = Field(default="exponential", description="Backoff strategy (exponential or linear)") initial_delay: float = Field(default=1.0, description="Initial delay in seconds") max_delay: float = Field(default=60.0, description="Maximum delay in seconds") class AlertsConfig(BaseModel): """Alerts configuration.""" enabled: bool = Field(default=False, description="Enable alerts") webhook_url: Optional[str] = Field(default=None, description="Webhook URL for alerts") email: Optional[str] = Field(default=None, description="Email for alerts") class ErrorHandlingConfig(BaseModel): """Error handling configuration.""" retry: RetryConfig = Field(default_factory=RetryConfig) alerts: AlertsConfig = Field(default_factory=AlertsConfig) log_errors: bool = Field(default=True, description="Log errors") class LoggingConfig(BaseModel): """Logging configuration.""" level: LogLevel = Field(default=LogLevel.INFO, description="Log level") format: LogFormat = Field(default=LogFormat.TEXT, description="Log format") file: Optional[str] = Field(default=None, description="Log file path") console: bool = Field(default=True, description="Log to console") class FeaturesConfig(BaseModel): """Feature flags configuration.""" batch_operations: bool = Field(default=True, description="Enable batch operations") auto_recommendations: bool = Field(default=False, description="Auto-apply recommendations") advanced_reporting: bool = Field(default=True, description="Enable advanced reporting") conversion_tracking: bool = Field(default=True, description="Enable conversion tracking") audience_management: bool = Field(default=True, description="Enable audience management") class GoogleAdsMCPConfig(BaseModel): """Main configuration model.""" authentication: AuthConfig = Field(default_factory=AuthConfig) performance: PerformanceConfig = Field(default_factory=PerformanceConfig) error_handling: ErrorHandlingConfig = Field(default_factory=ErrorHandlingConfig) logging: LoggingConfig = Field(default_factory=LoggingConfig) features: FeaturesConfig = Field(default_factory=FeaturesConfig) # API settings api_version: str = Field(default="v22", description="Google Ads API version") use_proto_plus: bool = Field(default=True, description="Use proto-plus for API responses") # Response settings default_page_size: int = Field(default=50, description="Default page size for queries") max_page_size: int = Field(default=100, description="Maximum page size for queries") character_limit: int = Field(default=25000, description="Response character limit") class ConfigManager: """Manages application configuration.""" def __init__(self, config_file: Optional[str] = None): """ Initialize configuration manager. Args: config_file: Optional path to configuration file (YAML or JSON) """ self.config: GoogleAdsMCPConfig = self._load_config(config_file) self._configure_logging() def _load_config(self, config_file: Optional[str]) -> GoogleAdsMCPConfig: """ Load configuration from file and environment variables. Args: config_file: Path to configuration file Returns: Loaded configuration """ config_dict = {} # Load from file if provided if config_file: config_path = Path(config_file) if config_path.exists(): try: with open(config_path, 'r') as f: if config_path.suffix in ['.yml', '.yaml']: config_dict = yaml.safe_load(f) or {} elif config_path.suffix == '.json': config_dict = json.load(f) logger.info(f"Loaded configuration from {config_file}") except Exception as e: logger.warning(f"Failed to load config file {config_file}: {e}") else: logger.warning(f"Config file not found: {config_file}") # Override with environment variables env_config = self._load_from_env() config_dict = self._merge_dicts(config_dict, env_config) # Create and validate configuration try: return GoogleAdsMCPConfig(**config_dict) except Exception as e: logger.error(f"Configuration validation failed: {e}") # Return default configuration return GoogleAdsMCPConfig() def _load_from_env(self) -> Dict[str, Any]: """ Load configuration from environment variables. Returns: Configuration dictionary from environment """ config = { "authentication": {} } # Authentication settings if os.getenv("GOOGLE_ADS_DEVELOPER_TOKEN"): config["authentication"]["developer_token"] = os.getenv("GOOGLE_ADS_DEVELOPER_TOKEN") if os.getenv("GOOGLE_ADS_CLIENT_ID"): config["authentication"]["client_id"] = os.getenv("GOOGLE_ADS_CLIENT_ID") if os.getenv("GOOGLE_ADS_CLIENT_SECRET"): config["authentication"]["client_secret"] = os.getenv("GOOGLE_ADS_CLIENT_SECRET") if os.getenv("GOOGLE_ADS_REFRESH_TOKEN"): config["authentication"]["refresh_token"] = os.getenv("GOOGLE_ADS_REFRESH_TOKEN") if os.getenv("GOOGLE_ADS_LOGIN_CUSTOMER_ID"): config["authentication"]["login_customer_id"] = os.getenv("GOOGLE_ADS_LOGIN_CUSTOMER_ID") if os.getenv("GOOGLE_ADS_SERVICE_ACCOUNT_KEY_FILE"): config["authentication"]["service_account_key_file"] = os.getenv("GOOGLE_ADS_SERVICE_ACCOUNT_KEY_FILE") config["authentication"]["method"] = "service_account" # Logging settings if os.getenv("LOG_LEVEL"): config["logging"] = {"level": os.getenv("LOG_LEVEL")} # Cache settings if os.getenv("CACHE_ENABLED"): config["performance"] = { "cache": {"enabled": os.getenv("CACHE_ENABLED").lower() == "true"} } if os.getenv("CACHE_BACKEND"): if "performance" not in config: config["performance"] = {} if "cache" not in config["performance"]: config["performance"]["cache"] = {} config["performance"]["cache"]["backend"] = os.getenv("CACHE_BACKEND") if os.getenv("REDIS_URL"): if "performance" not in config: config["performance"] = {} if "cache" not in config["performance"]: config["performance"]["cache"] = {} config["performance"]["cache"]["redis_url"] = os.getenv("REDIS_URL") return config def _merge_dicts(self, base: Dict, override: Dict) -> Dict: """ Recursively merge two dictionaries. Args: base: Base dictionary override: Override dictionary Returns: Merged dictionary """ result = base.copy() for key, value in override.items(): if key in result and isinstance(result[key], dict) and isinstance(value, dict): result[key] = self._merge_dicts(result[key], value) else: result[key] = value return result def _configure_logging(self): """Configure logging based on config.""" log_config = self.config.logging # Set log level numeric_level = getattr(logging, log_config.level.value) logging.basicConfig(level=numeric_level) # Configure format if log_config.format == LogFormat.JSON: formatter = logging.Formatter( '{"time": "%(asctime)s", "level": "%(levelname)s", "message": "%(message)s"}' ) else: formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) # Configure handlers handlers: List[logging.Handler] = [] if log_config.console: console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) handlers.append(console_handler) if log_config.file: file_handler = logging.FileHandler(log_config.file) file_handler.setFormatter(formatter) handlers.append(file_handler) # Apply to root logger root_logger = logging.getLogger() root_logger.handlers = handlers def get_auth_config(self) -> AuthConfig: """Get authentication configuration.""" return self.config.authentication def get_cache_config(self) -> CacheConfig: """Get cache configuration.""" return self.config.performance.cache def get_retry_config(self) -> RetryConfig: """Get retry configuration.""" return self.config.error_handling.retry def is_feature_enabled(self, feature: str) -> bool: """ Check if a feature is enabled. Args: feature: Feature name Returns: True if feature is enabled """ return getattr(self.config.features, feature, False) def save_config(self, file_path: str, format: str = "yaml"): """ Save current configuration to file. Args: file_path: Path to save configuration format: File format (yaml or json) """ config_dict = self.config.model_dump() try: with open(file_path, 'w') as f: if format == "yaml": yaml.dump(config_dict, f, default_flow_style=False, sort_keys=False) else: json.dump(config_dict, f, indent=2) logger.info(f"Configuration saved to {file_path}") except Exception as e: logger.error(f"Failed to save configuration: {e}") raise def validate(self) -> tuple[bool, List[str]]: """ Validate current configuration. Returns: Tuple of (is_valid, error_messages) """ errors = [] # Validate authentication auth = self.config.authentication if auth.method == "oauth2": required_fields = ["developer_token", "client_id", "client_secret", "refresh_token"] for field in required_fields: if not getattr(auth, field): errors.append(f"Missing required OAuth2 field: {field}") elif auth.method == "service_account": if not auth.service_account_key_file: errors.append("Missing service_account_key_file for service account authentication") elif not Path(auth.service_account_key_file).exists(): errors.append(f"Service account key file not found: {auth.service_account_key_file}") # Validate cache backend if self.config.performance.cache.enabled: if self.config.performance.cache.backend == CacheBackend.REDIS: if not self.config.performance.cache.redis_url: errors.append("Redis backend enabled but redis_url not provided") return len(errors) == 0, errors # Global configuration manager instance _config_manager: Optional[ConfigManager] = None def get_config_manager(config_file: Optional[str] = None) -> ConfigManager: """ Get or create the global configuration manager. Args: config_file: Optional configuration file path Returns: Configuration manager instance """ global _config_manager if _config_manager is None: _config_manager = ConfigManager(config_file) return _config_manager def get_config() -> GoogleAdsMCPConfig: """Get current configuration.""" return get_config_manager().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/johnoconnor0/google-ads-mcp'

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