#!/usr/bin/env python
"""
Configuration Manager
Handles all configuration from MCP environment variables
"""
import os
import json
import subprocess
import platform
import socket
import time
from typing import Optional, Dict, Any, List
from dataclasses import dataclass, field
from loguru import logger
@dataclass
class SearchEngineConfig:
"""Search engine configuration"""
api_key: Optional[str] = None
secret_key: Optional[str] = None
cse_id: Optional[str] = None
enabled: bool = False
@dataclass
class ServerConfig:
"""Main server configuration"""
# Logging configuration
log_level: str = "INFO"
log_file: str = "search_fusion.log"
log_rotation: str = "100 MB"
# Proxy configuration
http_proxy: Optional[str] = None
https_proxy: Optional[str] = None
no_proxy: Optional[str] = None
# Rate limiting configuration
default_cooldown: int = 300
max_error_count: int = 3
# Search engine configurations
google: SearchEngineConfig = field(default_factory=SearchEngineConfig)
serper: SearchEngineConfig = field(default_factory=SearchEngineConfig) # Same priority as Google
jina: SearchEngineConfig = field(default_factory=SearchEngineConfig)
bing: SearchEngineConfig = field(default_factory=SearchEngineConfig)
baidu: SearchEngineConfig = field(default_factory=SearchEngineConfig)
exa: SearchEngineConfig = field(default_factory=SearchEngineConfig)
duckduckgo: SearchEngineConfig = field(default_factory=lambda: SearchEngineConfig(enabled=True))
class ConfigManager:
"""Configuration manager - handles all configuration sources"""
def __init__(self, config_path: Optional[str] = None):
"""Initialize configuration manager
Args:
config_path: Legacy parameter, now deprecated, using pure MCP environment variable configuration
"""
if config_path:
logger.warning("⚠️ config_path parameter is deprecated, now using pure MCP environment variable configuration")
self.config = ServerConfig()
self._load_config()
def _load_config(self):
"""Load configuration from MCP environment variables only"""
# Load from environment variables and MCP configuration only
self._load_from_env()
# Auto-detect and setup proxy (inspired by concurrent-browser-mcp implementation)
self._auto_detect_proxy()
# Setup proxy environment variables
self._setup_proxy()
logger.info("✅ Configuration loaded successfully (MCP environment variable mode)")
self._log_config_summary()
def _load_from_env(self):
"""Load configuration from environment variables and MCP configuration"""
# Basic configuration
self.config.log_level = os.getenv('LOG_LEVEL', self.config.log_level)
self.config.log_file = os.getenv('LOG_FILE', self.config.log_file)
self.config.log_rotation = os.getenv('LOG_ROTATION', self.config.log_rotation)
# Proxy configuration - prioritize environment variables
self.config.http_proxy = os.getenv('HTTP_PROXY') or os.getenv('http_proxy')
self.config.https_proxy = os.getenv('HTTPS_PROXY') or os.getenv('https_proxy')
self.config.no_proxy = os.getenv('NO_PROXY') or os.getenv('no_proxy', self.config.no_proxy)
# MCP format API key configuration
# Supports format: SEARCH_ENGINE_API_KEY, SEARCH_ENGINE_SECRET_KEY etc
# Google configuration
google_api_key = os.getenv('GOOGLE_API_KEY') or os.getenv('GOOGLE_SEARCH_API_KEY')
google_cse_id = os.getenv('GOOGLE_CSE_ID') or os.getenv('GOOGLE_SEARCH_CSE_ID')
if google_api_key:
self.config.google = SearchEngineConfig(
api_key=google_api_key,
cse_id=google_cse_id,
enabled=True
)
# Serper configuration
serper_api_key = os.getenv('SERPER_API_KEY') or os.getenv('SERPER_SEARCH_API_KEY')
if serper_api_key:
self.config.serper = SearchEngineConfig(
api_key=serper_api_key,
enabled=True
)
# Bing configuration
bing_api_key = os.getenv('BING_API_KEY') or os.getenv('BING_SEARCH_API_KEY')
if bing_api_key:
self.config.bing = SearchEngineConfig(
api_key=bing_api_key,
enabled=True
)
# Baidu configuration
baidu_api_key = os.getenv('BAIDU_API_KEY') or os.getenv('BAIDU_SEARCH_API_KEY')
baidu_secret_key = os.getenv('BAIDU_SECRET_KEY') or os.getenv('BAIDU_SEARCH_SECRET_KEY')
if baidu_api_key and baidu_secret_key:
self.config.baidu = SearchEngineConfig(
api_key=baidu_api_key,
secret_key=baidu_secret_key,
enabled=True
)
# Jina configuration
jina_api_key = os.getenv('JINA_API_KEY') or os.getenv('JINA_SEARCH_API_KEY')
if jina_api_key:
self.config.jina = SearchEngineConfig(
api_key=jina_api_key,
enabled=True
)
# Exa configuration
exa_api_key = os.getenv('EXA_API_KEY') or os.getenv('EXA_SEARCH_API_KEY')
if exa_api_key:
self.config.exa = SearchEngineConfig(
api_key=exa_api_key,
enabled=True
)
def _auto_detect_proxy(self):
"""Auto-detect system proxy settings - inspired by concurrent-browser-mcp implementation"""
try:
# If environment variables already have proxy settings, skip auto-detection
if self.config.http_proxy or self.config.https_proxy:
logger.info("🌐 Using proxy from environment variables")
return
# Follow concurrent-browser-mcp detection order
detected_proxy = self._detect_local_proxy()
if detected_proxy:
self.config.http_proxy = detected_proxy
self.config.https_proxy = detected_proxy
logger.info(f"🔍 Auto-detected proxy: {detected_proxy}")
else:
logger.info("🌐 No system proxy detected, using direct connection")
except Exception as e:
logger.warning(f"⚠️ Failed to auto-detect proxy: {e}")
def _detect_local_proxy(self) -> Optional[str]:
"""Detect local proxy - fully inspired by concurrent-browser-mcp implementation"""
# 1. Check environment variables
env_proxy = self._get_proxy_from_env()
if env_proxy:
logger.info(f"Proxy detected from environment variables: {env_proxy}")
return env_proxy
# 2. Check common proxy ports - this is the key feature!
common_ports = [7890, 1087, 8080, 3128, 8888, 10809, 20171]
for port in common_ports:
proxy_url = f"http://127.0.0.1:{port}"
if self._test_proxy_connection(proxy_url):
logger.info(f"Local proxy port detected: {port}")
return proxy_url
# 3. Try to detect system proxy settings (macOS)
if platform.system().lower() == 'darwin':
system_proxy = self._get_macos_system_proxy()
if system_proxy:
logger.info(f"System proxy detected: {system_proxy}")
return system_proxy
return None
def _get_proxy_from_env(self) -> Optional[str]:
"""Get proxy from environment variables"""
http_proxy = os.getenv('HTTP_PROXY') or os.getenv('http_proxy')
https_proxy = os.getenv('HTTPS_PROXY') or os.getenv('https_proxy')
all_proxy = os.getenv('ALL_PROXY') or os.getenv('all_proxy')
return http_proxy or https_proxy or all_proxy
def _test_proxy_connection(self, proxy_url: str) -> bool:
"""Test proxy connection - fully inspired by concurrent-browser-mcp implementation"""
try:
# Simple port detection to avoid complex network requests
from urllib.parse import urlparse
parsed = urlparse(proxy_url)
# Create socket connection test
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(3) # 3-second timeout, consistent with concurrent-browser-mcp
try:
result = sock.connect_ex((parsed.hostname, parsed.port))
return result == 0 # 0 means connection successful
finally:
sock.close()
except Exception:
return False
def _get_macos_system_proxy(self) -> Optional[str]:
"""Get macOS system proxy settings - inspired by concurrent-browser-mcp implementation"""
try:
# Use the same command as concurrent-browser-mcp
result = subprocess.run([
'networksetup', '-getwebproxy', 'Wi-Fi'
], capture_output=True, text=True, timeout=5)
if result.returncode != 0:
# Try Ethernet
result = subprocess.run([
'networksetup', '-getwebproxy', 'Ethernet'
], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
lines = result.stdout.strip().split('\n')
enabled = any('Enabled: Yes' in line for line in lines)
if enabled:
server = None
port = None
for line in lines:
if line.startswith('Server:'):
server = line.split(':', 1)[1].strip()
elif line.startswith('Port:'):
port = line.split(':', 1)[1].strip()
if server and port:
return f"http://{server}:{port}"
except Exception as e:
logger.debug(f"macOS proxy detection failed: {e}")
return None
def _setup_proxy(self):
"""Setup proxy environment variables"""
if self.config.http_proxy:
os.environ['http_proxy'] = self.config.http_proxy
os.environ['HTTP_PROXY'] = self.config.http_proxy
if self.config.https_proxy:
os.environ['https_proxy'] = self.config.https_proxy
os.environ['HTTPS_PROXY'] = self.config.https_proxy
if self.config.no_proxy:
os.environ['no_proxy'] = self.config.no_proxy
os.environ['NO_PROXY'] = self.config.no_proxy
def _log_config_summary(self):
"""Log configuration summary"""
enabled_engines = []
for engine_name in ['google', 'serper', 'jina', 'duckduckgo', 'exa', 'bing', 'baidu']:
if self.is_engine_enabled(engine_name):
config = self.get_engine_config(engine_name)
if config.api_key:
enabled_engines.append(f"{engine_name.title()} (with API key)")
else:
enabled_engines.append(f"{engine_name.title()} (free)")
logger.info(f"📊 Enabled search engines: {', '.join(enabled_engines) if enabled_engines else 'None'}")
if self.config.http_proxy or self.config.https_proxy:
logger.info(f"🌐 Proxy configuration: HTTP={self.config.http_proxy}, HTTPS={self.config.https_proxy}")
logger.info(f"📝 Logging: Level={self.config.log_level}, File={self.config.log_file}")
def get_engine_config(self, engine_name: str) -> SearchEngineConfig:
"""Get configuration for specified search engine"""
return getattr(self.config, engine_name.lower(), SearchEngineConfig(enabled=False))
def is_engine_enabled(self, engine_name: str) -> bool:
"""Check if search engine is enabled and has valid configuration"""
engine_config = self.get_engine_config(engine_name)
# DuckDuckGo doesn't require API key
if engine_name.lower() == 'duckduckgo':
return True
# All other engines (including Jina) require API keys
return engine_config.enabled and bool(engine_config.api_key)
def get_proxy_config(self) -> Dict[str, Optional[str]]:
"""Get proxy configuration"""
return {
"http://": self.config.http_proxy,
"https://": self.config.https_proxy,
} if self.config.http_proxy or self.config.https_proxy else None