"""Configuration models and loader for Sentry MCP server."""
from enum import Enum
from pathlib import Path
from typing import Any
import yaml
from pydantic import BaseModel, Field
class SentryRegion(str, Enum):
"""Sentry deployment regions."""
US = "us" # sentry.io
EU = "eu" # de.sentry.io (GDPR)
CUSTOM = "custom" # Self-hosted
# Region to base URL mapping
REGION_BASE_URLS: dict[SentryRegion, str] = {
SentryRegion.US: "https://sentry.io",
SentryRegion.EU: "https://de.sentry.io",
}
class SentryInstanceLabels(BaseModel):
"""Customer-provided labels for a Sentry instance."""
data_residency: str | None = Field(None, alias="dataResidency")
description: str | None = None
team: str | None = None
environment: str | None = None
class Config:
populate_by_name = True
class SentryInstanceConfig(BaseModel):
"""Configuration for a single Sentry instance."""
name: str # Instance name (e.g., "sentryUS", "sentryEU")
region: SentryRegion
org_slug: str = Field(..., alias="orgSlug")
base_url: str | None = Field(None, alias="baseUrl") # Required for CUSTOM region
secret_name: str | None = Field(None, alias="secretName")
labels: SentryInstanceLabels = Field(default_factory=SentryInstanceLabels)
# Runtime values (populated after loading secrets)
auth_token: str | None = None
class Config:
populate_by_name = True
@property
def api_base_url(self) -> str:
"""Get the API base URL for this instance."""
if self.base_url:
return self.base_url.rstrip("/")
if self.region == SentryRegion.CUSTOM:
raise ValueError(
f"Instance '{self.name}' has region 'custom' but no baseUrl specified"
)
return REGION_BASE_URLS[self.region]
class SentryMCPConfig(BaseModel):
"""Root configuration for Sentry MCP server."""
instances: dict[str, SentryInstanceConfig] = Field(default_factory=dict)
@classmethod
def from_yaml_file(cls, path: str | Path) -> "SentryMCPConfig":
"""Load configuration from a YAML file."""
path = Path(path)
if not path.exists():
return cls()
with open(path) as f:
data = yaml.safe_load(f) or {}
return cls.from_dict(data)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "SentryMCPConfig":
"""Load configuration from a dictionary."""
instances_data = data.get("instances", {})
instances: dict[str, SentryInstanceConfig] = {}
for name, config in instances_data.items():
# Skip disabled instances
if not config.get("enabled", True):
continue
# Add name to config
config["name"] = name
instances[name] = SentryInstanceConfig(**config)
return cls(instances=instances)
def load_auth_token(instance_name: str, secrets_base_path: str = "/etc/secrets") -> str | None:
"""Load auth token for an instance from mounted secret.
Secrets are expected at: {secrets_base_path}/{instance_name}/auth_token
Args:
instance_name: Name of the Sentry instance
secrets_base_path: Base path where secrets are mounted
Returns:
Auth token string or None if not found
"""
token_path = Path(secrets_base_path) / instance_name / "auth_token"
if token_path.exists():
return token_path.read_text().strip()
return None
def load_config(
config_path: str = "/etc/sentry-mcp/config.yaml",
secrets_base_path: str = "/etc/secrets",
) -> SentryMCPConfig:
"""Load configuration and populate auth tokens from secrets.
Args:
config_path: Path to the config.yaml file
secrets_base_path: Base path where secrets are mounted
Returns:
Fully populated SentryMCPConfig
"""
config = SentryMCPConfig.from_yaml_file(config_path)
# Load auth tokens for each instance
for name, instance in config.instances.items():
token = load_auth_token(name, secrets_base_path)
if token:
instance.auth_token = token
elif instance.secret_name:
# Try loading from secret_name path as fallback
alt_token = load_auth_token(instance.secret_name, secrets_base_path)
if alt_token:
instance.auth_token = alt_token
return config