- Connect to MCP servers concurrently instead of sequentially - Reduce retry settings in test mode (IS_TEST=True): - 1 attempt instead of 3 - 100ms retry delay instead of 1s - 2s timeout instead of 30-120s Reduces MCP E2E test time from ~16s to under 1s. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
246 lines
7.1 KiB
Python
246 lines
7.1 KiB
Python
"""
|
|
MCP Configuration System
|
|
|
|
Pydantic models for MCP server configuration with YAML file loading
|
|
and environment variable overrides.
|
|
"""
|
|
|
|
import os
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
|
|
class TransportType(str, Enum):
|
|
"""Supported MCP transport types."""
|
|
|
|
HTTP = "http"
|
|
STDIO = "stdio"
|
|
SSE = "sse"
|
|
|
|
|
|
class MCPServerConfig(BaseModel):
|
|
"""Configuration for a single MCP server."""
|
|
|
|
url: str = Field(..., description="Server URL (supports ${ENV_VAR} syntax)")
|
|
transport: TransportType = Field(
|
|
default=TransportType.HTTP,
|
|
description="Transport protocol to use",
|
|
)
|
|
timeout: int = Field(
|
|
default=30,
|
|
ge=1,
|
|
le=600,
|
|
description="Request timeout in seconds",
|
|
)
|
|
retry_attempts: int = Field(
|
|
default=3,
|
|
ge=0,
|
|
le=10,
|
|
description="Number of retry attempts on failure",
|
|
)
|
|
retry_delay: float = Field(
|
|
default=1.0,
|
|
ge=0.1,
|
|
le=60.0,
|
|
description="Initial delay between retries in seconds",
|
|
)
|
|
retry_max_delay: float = Field(
|
|
default=30.0,
|
|
ge=1.0,
|
|
le=300.0,
|
|
description="Maximum delay between retries in seconds",
|
|
)
|
|
circuit_breaker_threshold: int = Field(
|
|
default=5,
|
|
ge=1,
|
|
le=50,
|
|
description="Number of failures before opening circuit",
|
|
)
|
|
circuit_breaker_timeout: float = Field(
|
|
default=30.0,
|
|
ge=5.0,
|
|
le=300.0,
|
|
description="Seconds to wait before attempting to close circuit",
|
|
)
|
|
enabled: bool = Field(
|
|
default=True,
|
|
description="Whether this server is enabled",
|
|
)
|
|
description: str | None = Field(
|
|
default=None,
|
|
description="Human-readable description of the server",
|
|
)
|
|
|
|
@field_validator("url", mode="before")
|
|
@classmethod
|
|
def expand_env_vars(cls, v: str) -> str:
|
|
"""Expand environment variables in URL using ${VAR:-default} syntax."""
|
|
if not isinstance(v, str):
|
|
return v
|
|
|
|
result = v
|
|
# Find all ${VAR} or ${VAR:-default} patterns
|
|
import re
|
|
|
|
pattern = r"\$\{([^}]+)\}"
|
|
matches = re.findall(pattern, v)
|
|
|
|
for match in matches:
|
|
if ":-" in match:
|
|
var_name, default = match.split(":-", 1)
|
|
else:
|
|
var_name, default = match, ""
|
|
|
|
env_value = os.environ.get(var_name.strip(), default)
|
|
result = result.replace(f"${{{match}}}", env_value)
|
|
|
|
return result
|
|
|
|
|
|
class MCPConfig(BaseModel):
|
|
"""Root configuration for all MCP servers."""
|
|
|
|
mcp_servers: dict[str, MCPServerConfig] = Field(
|
|
default_factory=dict,
|
|
description="Map of server names to their configurations",
|
|
)
|
|
|
|
# Global defaults
|
|
default_timeout: int = Field(
|
|
default=30,
|
|
description="Default timeout for all servers",
|
|
)
|
|
default_retry_attempts: int = Field(
|
|
default=3,
|
|
description="Default retry attempts for all servers",
|
|
)
|
|
connection_pool_size: int = Field(
|
|
default=10,
|
|
ge=1,
|
|
le=100,
|
|
description="Maximum connections per server",
|
|
)
|
|
health_check_interval: int = Field(
|
|
default=30,
|
|
ge=5,
|
|
le=300,
|
|
description="Seconds between health checks",
|
|
)
|
|
|
|
@classmethod
|
|
def from_yaml(cls, path: str | Path) -> "MCPConfig":
|
|
"""Load configuration from a YAML file."""
|
|
path = Path(path)
|
|
if not path.exists():
|
|
raise FileNotFoundError(f"MCP config file not found: {path}")
|
|
|
|
with path.open("r") as f:
|
|
data = yaml.safe_load(f)
|
|
|
|
if data is None:
|
|
data = {}
|
|
|
|
return cls.model_validate(data)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> "MCPConfig":
|
|
"""Load configuration from a dictionary."""
|
|
return cls.model_validate(data)
|
|
|
|
def get_server(self, name: str) -> MCPServerConfig | None:
|
|
"""Get a server configuration by name."""
|
|
return self.mcp_servers.get(name)
|
|
|
|
def get_enabled_servers(self) -> dict[str, MCPServerConfig]:
|
|
"""Get all enabled server configurations."""
|
|
return {
|
|
name: config for name, config in self.mcp_servers.items() if config.enabled
|
|
}
|
|
|
|
def list_server_names(self) -> list[str]:
|
|
"""Get list of all configured server names."""
|
|
return list(self.mcp_servers.keys())
|
|
|
|
|
|
# Default configuration path
|
|
DEFAULT_CONFIG_PATH = Path(__file__).parent.parent.parent.parent / "mcp_servers.yaml"
|
|
|
|
|
|
def load_mcp_config(path: str | Path | None = None) -> MCPConfig:
|
|
"""
|
|
Load MCP configuration from file or environment.
|
|
|
|
Priority:
|
|
1. Explicit path parameter
|
|
2. MCP_CONFIG_PATH environment variable
|
|
3. Default path (backend/mcp_servers.yaml)
|
|
4. Empty config if no file exists
|
|
|
|
In test mode (IS_TEST=True), retry settings are reduced for faster tests.
|
|
"""
|
|
if path is None:
|
|
path = os.environ.get("MCP_CONFIG_PATH", str(DEFAULT_CONFIG_PATH))
|
|
|
|
path = Path(path)
|
|
|
|
if not path.exists():
|
|
# Return empty config if no file exists (allows runtime registration)
|
|
return MCPConfig()
|
|
|
|
config = MCPConfig.from_yaml(path)
|
|
|
|
# In test mode, reduce retry settings to speed up tests
|
|
is_test = os.environ.get("IS_TEST", "").lower() in ("true", "1", "yes")
|
|
if is_test:
|
|
for server_config in config.mcp_servers.values():
|
|
server_config.retry_attempts = 1 # Single attempt
|
|
server_config.retry_delay = 0.1 # 100ms instead of 1s
|
|
server_config.retry_max_delay = 0.5 # 500ms max
|
|
server_config.timeout = 2 # 2s timeout instead of 30-120s
|
|
|
|
return config
|
|
|
|
|
|
def create_default_config() -> MCPConfig:
|
|
"""
|
|
Create a default MCP configuration with standard servers.
|
|
|
|
This is useful for development and as a template.
|
|
"""
|
|
return MCPConfig(
|
|
mcp_servers={
|
|
"llm-gateway": MCPServerConfig(
|
|
url="${LLM_GATEWAY_URL:-http://localhost:8001}",
|
|
transport=TransportType.HTTP,
|
|
timeout=60,
|
|
description="LLM Gateway for multi-provider AI interactions",
|
|
),
|
|
"knowledge-base": MCPServerConfig(
|
|
url="${KNOWLEDGE_BASE_URL:-http://localhost:8002}",
|
|
transport=TransportType.HTTP,
|
|
timeout=30,
|
|
description="Knowledge Base for RAG and document retrieval",
|
|
),
|
|
"git-ops": MCPServerConfig(
|
|
url="${GIT_OPS_URL:-http://localhost:8003}",
|
|
transport=TransportType.HTTP,
|
|
timeout=120,
|
|
description="Git Operations for repository management",
|
|
),
|
|
"issues": MCPServerConfig(
|
|
url="${ISSUES_URL:-http://localhost:8004}",
|
|
transport=TransportType.HTTP,
|
|
timeout=30,
|
|
description="Issue Tracker for Gitea/GitHub/GitLab",
|
|
),
|
|
},
|
|
default_timeout=30,
|
|
default_retry_attempts=3,
|
|
connection_pool_size=10,
|
|
health_check_interval=30,
|
|
)
|