Files
fast-next-template/backend/app/services/mcp/config.py
Felipe Cardoso 35aea2d73a perf(mcp): optimize test performance with parallel connections and reduced retries
- 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>
2026-01-05 18:33:38 +01:00

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,
)