7935b671d3
Exposes homelab topology to MCP-compatible AI clients (Claude Code, etc.) over LAN via HTTP/SSE on port 8001. - New mcp/ service: FastAPI + mcp SDK, SSE transport - Auth: X-API-Key for AI clients, X-MCP-Service-Key for backend (Docker-internal) - Resources: canvas, nodes, edges, scan/pending, scan/runs - Tools: create/update/delete nodes+edges, trigger scan, approve/hide devices - Backend deps.py: accepts JWT or MCP service key (no plain-text password) - 40 tests (auth, resources, tools) across asyncio + trio - docker-compose.yml: mcp service on port 8001 - README: MCP setup section with Claude Code/Desktop config examples
57 lines
1.9 KiB
Python
57 lines
1.9 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
|
|
|
secret_key: str # Required — set SECRET_KEY in .env
|
|
sqlite_path: str = "./data/homelab.db"
|
|
cors_origins: list[str] = ["http://localhost:5173", "http://localhost:3000"]
|
|
|
|
# JWT
|
|
algorithm: str = "HS256"
|
|
access_token_expire_minutes: int = 1440 # 24h
|
|
|
|
# Auth — set AUTH_USERNAME and AUTH_PASSWORD_HASH in .env
|
|
auth_username: str = "admin"
|
|
auth_password_hash: str = ""
|
|
|
|
# Scanner
|
|
scanner_ranges: list[str] = ["192.168.1.0/24"]
|
|
|
|
# Status checker
|
|
status_checker_interval: int = 60
|
|
|
|
# MCP service key — set MCP_SERVICE_KEY in .env
|
|
# Used by the MCP server to authenticate against the backend without a user password.
|
|
# Leave empty to disable MCP service key auth.
|
|
mcp_service_key: str = ""
|
|
|
|
def _override_path(self) -> Path:
|
|
return Path(self.sqlite_path).parent / "scan_config.json"
|
|
|
|
def load_overrides(self) -> None:
|
|
"""Load runtime scan config overrides written by the API."""
|
|
try:
|
|
data = json.loads(self._override_path().read_text())
|
|
if "scanner_ranges" in data:
|
|
self.scanner_ranges = data["scanner_ranges"]
|
|
if "status_checker_interval" in data:
|
|
self.status_checker_interval = int(data["status_checker_interval"])
|
|
except Exception:
|
|
pass
|
|
|
|
def save_overrides(self) -> None:
|
|
"""Persist scan config so it survives container restarts."""
|
|
self._override_path().parent.mkdir(parents=True, exist_ok=True)
|
|
self._override_path().write_text(json.dumps({
|
|
"scanner_ranges": self.scanner_ranges,
|
|
"status_checker_interval": self.status_checker_interval,
|
|
}))
|
|
|
|
|
|
settings = Settings() # type: ignore[call-arg]
|