update
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"""Configuration, loaded from environment variables."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
def _bool(name: str, default: bool = False) -> bool:
|
||||
raw = os.environ.get(name)
|
||||
if raw is None:
|
||||
return default
|
||||
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _list(name: str) -> list[str]:
|
||||
raw = os.environ.get(name, "")
|
||||
return [item.strip() for item in raw.split(",") if item.strip()]
|
||||
|
||||
|
||||
def _require(name: str) -> str:
|
||||
value = os.environ.get(name, "").strip()
|
||||
if not value:
|
||||
raise RuntimeError(f"Missing required environment variable: {name}")
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Settings:
|
||||
# --- BookStack ---
|
||||
bookstack_url: str
|
||||
bookstack_token_id: str
|
||||
bookstack_token_secret: str
|
||||
bookstack_verify_tls: bool = True
|
||||
request_timeout: float = 30.0
|
||||
|
||||
# --- Behaviour ---
|
||||
read_only: bool = True
|
||||
allow_delete: bool = False
|
||||
max_content_chars: int = 40_000
|
||||
|
||||
# --- Transport ---
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8080
|
||||
mcp_path: str = "/mcp"
|
||||
|
||||
# --- Auth (client -> this MCP server) ---
|
||||
auth_mode: str = "none" # none | token | github | github+token
|
||||
static_tokens: list[str] = field(default_factory=list)
|
||||
public_url: str = ""
|
||||
github_client_id: str = ""
|
||||
github_client_secret: str = ""
|
||||
allowed_hosts: list[str] = field(default_factory=list)
|
||||
|
||||
# --- OIDC (your own IdP: Authentik, Keycloak, Authelia, Zitadel, ...) ---
|
||||
oidc_config_url: str = ""
|
||||
oidc_client_id: str = ""
|
||||
oidc_client_secret: str = ""
|
||||
oidc_verify_id_token: bool = False
|
||||
oidc_groups_claim: str = "groups"
|
||||
oidc_scopes: list[str] = field(default_factory=list)
|
||||
|
||||
# --- Who may use the server (applies to both GitHub and OIDC) ---
|
||||
allowed_users: list[str] = field(default_factory=list)
|
||||
required_groups: list[str] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "Settings":
|
||||
auth_mode = os.environ.get("MCP_AUTH_MODE", "none").strip().lower()
|
||||
valid_modes = {
|
||||
"none", "token",
|
||||
"github", "github+token",
|
||||
"oidc", "oidc+token",
|
||||
}
|
||||
if auth_mode not in valid_modes:
|
||||
raise RuntimeError(
|
||||
f"MCP_AUTH_MODE must be one of {'|'.join(sorted(valid_modes))}, got {auth_mode!r}"
|
||||
)
|
||||
|
||||
settings = cls(
|
||||
bookstack_url=_require("BOOKSTACK_URL").rstrip("/"),
|
||||
bookstack_token_id=_require("BOOKSTACK_TOKEN_ID"),
|
||||
bookstack_token_secret=_require("BOOKSTACK_TOKEN_SECRET"),
|
||||
bookstack_verify_tls=_bool("BOOKSTACK_VERIFY_TLS", True),
|
||||
request_timeout=float(os.environ.get("BOOKSTACK_TIMEOUT", "30")),
|
||||
read_only=_bool("BOOKSTACK_READ_ONLY", True),
|
||||
allow_delete=_bool("BOOKSTACK_ALLOW_DELETE", False),
|
||||
max_content_chars=int(os.environ.get("MCP_MAX_CONTENT_CHARS", "40000")),
|
||||
host=os.environ.get("MCP_HOST", "0.0.0.0"),
|
||||
port=int(os.environ.get("MCP_PORT", "8080")),
|
||||
mcp_path=os.environ.get("MCP_PATH", "/mcp"),
|
||||
auth_mode=auth_mode,
|
||||
static_tokens=_list("MCP_STATIC_TOKENS"),
|
||||
public_url=os.environ.get("MCP_PUBLIC_URL", "").rstrip("/"),
|
||||
github_client_id=os.environ.get("GITHUB_CLIENT_ID", ""),
|
||||
github_client_secret=os.environ.get("GITHUB_CLIENT_SECRET", ""),
|
||||
allowed_hosts=_list("MCP_ALLOWED_HOSTS"),
|
||||
oidc_config_url=os.environ.get("OIDC_CONFIG_URL", "").strip(),
|
||||
oidc_client_id=os.environ.get("OIDC_CLIENT_ID", ""),
|
||||
oidc_client_secret=os.environ.get("OIDC_CLIENT_SECRET", ""),
|
||||
oidc_verify_id_token=_bool("OIDC_VERIFY_ID_TOKEN", False),
|
||||
oidc_groups_claim=os.environ.get("OIDC_GROUPS_CLAIM", "groups"),
|
||||
oidc_scopes=_list("OIDC_SCOPES"),
|
||||
# GITHUB_ALLOWED_USERS is the old name, still honoured.
|
||||
allowed_users=[
|
||||
u.lower() for u in (_list("MCP_ALLOWED_USERS") or _list("GITHUB_ALLOWED_USERS"))
|
||||
],
|
||||
required_groups=[g.lower() for g in _list("MCP_REQUIRED_GROUPS")],
|
||||
)
|
||||
settings.validate()
|
||||
return settings
|
||||
|
||||
def validate(self) -> None:
|
||||
if "token" in self.auth_mode and not self.static_tokens:
|
||||
raise RuntimeError("MCP_AUTH_MODE includes 'token' but MCP_STATIC_TOKENS is empty")
|
||||
if "github" in self.auth_mode:
|
||||
if not (self.github_client_id and self.github_client_secret):
|
||||
raise RuntimeError(
|
||||
"MCP_AUTH_MODE includes 'github' but GITHUB_CLIENT_ID / "
|
||||
"GITHUB_CLIENT_SECRET are not set"
|
||||
)
|
||||
if "oidc" in self.auth_mode:
|
||||
if not self.oidc_config_url:
|
||||
raise RuntimeError(
|
||||
"MCP_AUTH_MODE includes 'oidc' but OIDC_CONFIG_URL is not set. "
|
||||
"This is your provider's discovery document, e.g. "
|
||||
"https://auth.example.com/application/o/bookstack-mcp/.well-known/openid-configuration"
|
||||
)
|
||||
if not (self.oidc_client_id and self.oidc_client_secret):
|
||||
raise RuntimeError(
|
||||
"MCP_AUTH_MODE includes 'oidc' but OIDC_CLIENT_ID / "
|
||||
"OIDC_CLIENT_SECRET are not set"
|
||||
)
|
||||
if ("github" in self.auth_mode or "oidc" in self.auth_mode) and not self.public_url:
|
||||
raise RuntimeError(
|
||||
"MCP_PUBLIC_URL is required for OAuth (e.g. https://mcp.example.com)"
|
||||
)
|
||||
if not self.mcp_path.startswith("/"):
|
||||
raise RuntimeError("MCP_PATH must start with '/'")
|
||||
|
||||
@property
|
||||
def api_base(self) -> str:
|
||||
return f"{self.bookstack_url}/api"
|
||||
Reference in New Issue
Block a user