"""Configuration management for Dolibarr MCP Server."""
import os
import sys
from pydantic import AliasChoices, Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
class Config(BaseSettings):
"""Configuration for Dolibarr MCP Server."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
validate_assignment=True,
extra="ignore",
)
dolibarr_url: str = Field(
description="Dolibarr API URL",
default="",
)
dolibarr_api_key: str = Field(
description="Dolibarr API key",
default="",
validation_alias=AliasChoices("dolibarr_api_key", "api_key"),
)
log_level: str = Field(
description="Logging level",
default="INFO",
)
mcp_transport: str = Field(
description="Transport for MCP server (stdio or http)",
default="stdio",
)
mcp_http_host: str = Field(
description="HTTP host/interface for MCP server",
default="0.0.0.0",
)
mcp_http_port: int = Field(
description="HTTP port for MCP server",
default=8080,
)
allow_ref_autogen: bool = Field(
description="Allow automatic generation of reference fields when missing",
default=False,
)
ref_autogen_prefix: str = Field(
description="Prefix to use when auto-generating references",
default="AUTO",
)
debug_mode: bool = Field(
description="Enable verbose debug logging without exposing secrets",
default=False,
)
max_retries: int = Field(
description="Maximum retries for transient HTTP errors (502, 503, 504)",
default=3,
)
retry_backoff_seconds: float = Field(
description="Base backoff (seconds) for retry strategy with exponential backoff",
default=1.0,
)
request_timeout: int = Field(
description="Request timeout in seconds",
default=60,
)
@field_validator("dolibarr_url")
@classmethod
def validate_dolibarr_url(cls, v: str) -> str:
"""Validate Dolibarr URL."""
if not v:
v = (
os.getenv("DOLIBARR_URL")
or os.getenv("DOLIBARR_BASE_URL")
or os.getenv("DOLIBARR_SHOP_URL")
or ""
)
if not v:
# Print warning but don't fail
print(
"⚠️ DOLIBARR_URL/DOLIBARR_BASE_URL not configured - API calls will fail",
file=sys.stderr,
)
return "https://your-dolibarr-instance.com/api/index.php"
if not v.startswith(("http://", "https://")):
raise ValueError("DOLIBARR_URL must start with http:// or https://")
# Remove trailing slash if present
v = v.rstrip("/")
# Ensure it ends with the proper API path
if not v.endswith("/api/index.php"):
if "/api" in v:
if not v.endswith("/index.php"):
if v.endswith("/index.php/"):
v = v[:-1]
elif not v.endswith("/index.php"):
v = v + "/index.php"
else:
v = v + "/api/index.php"
return v
@field_validator("dolibarr_api_key")
@classmethod
def validate_api_key(cls, v: str) -> str:
"""Validate API key."""
if not v:
v = os.getenv("DOLIBARR_API_KEY", "")
if not v:
print(
"⚠️ DOLIBARR_API_KEY not configured - API authentication will fail",
file=sys.stderr,
)
print(
"📝 Please set DOLIBARR_API_KEY in your .env file or Claude configuration",
file=sys.stderr,
)
return "placeholder_api_key"
if v == "your_dolibarr_api_key_here":
print(
"⚠️ Using placeholder API key - please configure a real API key",
file=sys.stderr,
)
return v
@field_validator("log_level")
@classmethod
def validate_log_level(cls, v: str) -> str:
"""Validate log level."""
if not v:
v = os.getenv("LOG_LEVEL", "INFO")
valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
if v.upper() not in valid_levels:
print(f"⚠️ Invalid LOG_LEVEL '{v}', using INFO", file=sys.stderr)
return "INFO"
return v.upper()
@field_validator("mcp_transport")
@classmethod
def validate_transport(cls, v: str) -> str:
"""Validate MCP transport selection."""
if not v:
v = os.getenv("MCP_TRANSPORT", "stdio")
normalized = v.lower()
if normalized not in {"stdio", "http"}:
print(f"⚠️ Invalid MCP_TRANSPORT '{v}', defaulting to stdio", file=sys.stderr)
return "stdio"
return normalized
@field_validator("mcp_http_host")
@classmethod
def validate_http_host(cls, v: str) -> str:
"""Validate HTTP host."""
return v or os.getenv("MCP_HTTP_HOST", "0.0.0.0")
@field_validator("mcp_http_port")
@classmethod
def validate_http_port(cls, v: int) -> int:
"""Validate HTTP port."""
try:
port = int(v)
except Exception as exc:
raise ValueError(f"Invalid MCP_HTTP_PORT '{v}': {exc}") from exc
if not 1 <= port <= 65535:
raise ValueError("MCP_HTTP_PORT must be between 1 and 65535")
return port
@classmethod
def from_env(cls) -> "Config":
"""Create configuration from environment variables with validation."""
try:
config = cls(
dolibarr_url=(
os.getenv("DOLIBARR_URL")
or os.getenv("DOLIBARR_BASE_URL")
or os.getenv("DOLIBARR_SHOP_URL")
or ""
),
dolibarr_api_key=os.getenv("DOLIBARR_API_KEY", ""),
log_level=os.getenv("LOG_LEVEL", "INFO"),
)
if os.getenv("DEBUG_CONFIG"):
print(f"✅ Config loaded:", file=sys.stderr)
print(f" URL: {config.dolibarr_url}", file=sys.stderr)
print(
f" API Key: {'*' * 10 if config.dolibarr_api_key else 'NOT SET'}",
file=sys.stderr,
)
return config
except Exception as e:
print(f"❌ Configuration Error: {e}", file=sys.stderr)
print(file=sys.stderr)
print("💡 Quick Setup Guide:", file=sys.stderr)
print("1. Copy .env.example to .env", file=sys.stderr)
print("2. Edit .env with your Dolibarr details:", file=sys.stderr)
print(
" DOLIBARR_URL=https://your-dolibarr-instance.com",
file=sys.stderr,
)
print(
" (or DOLIBARR_BASE_URL=https://your-dolibarr-instance.com/api/index.php/)",
file=sys.stderr,
)
print(" DOLIBARR_API_KEY=your_api_key_here", file=sys.stderr)
print(file=sys.stderr)
print("🔧 Dolibarr API Key Setup:", file=sys.stderr)
print(" 1. Login to Dolibarr as admin", file=sys.stderr)
print(" 2. Go to: Home → Setup → Modules", file=sys.stderr)
print(" 3. Enable: 'Web Services API REST (developer)'", file=sys.stderr)
print(" 4. Go to: Home → Setup → API/Web services", file=sys.stderr)
print(" 5. Create a new API key", file=sys.stderr)
print(file=sys.stderr)
raise
def validate_config(self) -> None:
"""Validate current configuration values."""
self.dolibarr_url = type(self).validate_dolibarr_url(self.dolibarr_url)
self.dolibarr_api_key = type(self).validate_api_key(self.dolibarr_api_key)
self.log_level = type(self).validate_log_level(self.log_level)
if self.dolibarr_url.endswith('your-dolibarr-instance.com/api/index.php') or self.dolibarr_api_key in {'', 'placeholder_api_key', 'your_dolibarr_api_key_here'}:
raise ValueError('Dolibarr configuration is incomplete')
@property
def api_key(self) -> str:
"""Backward compatibility for api_key property."""
return self.dolibarr_api_key
@api_key.setter
def api_key(self, value: str) -> None:
"""Allow updating the API key via legacy attribute."""
self.dolibarr_api_key = value