Revert "feat(mcp): hovedhusets egen gateway paa /hovedhus/mcp"

Hovedhuset administreres i stedet som en almindelig backend i /mcp
(registry + oauth_access pr. bruger i /admin/mcp-keys).

Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
This commit is contained in:
agw
2026-09-29 17:32:32 +02:00
parent 0635f4a76c
commit e05c376452
2 changed files with 1 additions and 135 deletions
+1 -28
View File
@@ -7,7 +7,6 @@ from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
import contextlib
import time import time
import authentik as ak import authentik as ak
@@ -21,30 +20,11 @@ import ytdl as yt
import mcpkeys import mcpkeys
import mcp_server import mcp_server
import mcp_gateway import mcp_gateway
import mcp_hovedhus
# Portalens egen MCP (FastMCP), monteret in-process på /mcp. json_response=True # Portalens egen MCP (FastMCP), monteret in-process på /mcp. json_response=True
# => almindelig JSON (ingen SSE), så det passerer edge'en utunet. Bearer-gated. # => almindelig JSON (ingen SSE), så det passerer edge'en utunet. Bearer-gated.
mcp_app = mcp_gateway.gateway.http_app(path="/mcp", json_response=True) mcp_app = mcp_gateway.gateway.http_app(path="/mcp", json_response=True)
app = FastAPI(title="Weircon Portal", lifespan=mcp_app.lifespan)
# Hovedhusets gateway (/hovedhus/mcp) er en selvstændig under-app med egen
# auth, så dens middleware kun gælder under /hovedhus. Begge apps har en
# lifespan (StreamableHTTP-sessionerne), og FastAPI tager kun én.
hh_app = (mcp_hovedhus.gateway.http_app(path="/mcp", json_response=True)
if mcp_hovedhus.gateway else None)
@contextlib.asynccontextmanager
async def _lifespan(a):
async with mcp_app.lifespan(a):
if hh_app is None:
yield
else:
async with hh_app.lifespan(hh_app):
yield
app = FastAPI(title="Weircon Portal", lifespan=_lifespan)
for _mcp_route in mcp_app.routes: for _mcp_route in mcp_app.routes:
app.router.routes.append(_mcp_route) app.router.routes.append(_mcp_route)
# route-extend copies routes but NOT middleware — carry FastMCP's auth/context # route-extend copies routes but NOT middleware — carry FastMCP's auth/context
@@ -54,13 +34,6 @@ for _mcp_mw in reversed(mcp_app.user_middleware):
app.add_middleware(SessionMiddleware, secret_key=config.SECRET_KEY, https_only=True, app.add_middleware(SessionMiddleware, secret_key=config.SECRET_KEY, https_only=True,
same_site="lax", max_age=60 * 60 * 12) same_site="lax", max_age=60 * 60 * 12)
app.mount("/static", StaticFiles(directory=config.BASE / "static"), name="static") app.mount("/static", StaticFiles(directory=config.BASE / "static"), name="static")
if hh_app is not None:
# OAuth-metadata skal ligge i roden (RFC 8414/9728) med stien som suffiks.
# Den bare /.well-known/openid-configuration tilhører hovedgatewayen.
for _r in mcp_hovedhus.gateway.auth.get_well_known_routes(mcp_path="/mcp"):
if _r.path != "/.well-known/openid-configuration":
app.router.routes.append(_r)
app.mount(mcp_hovedhus.PREFIX, hh_app)
templates = Jinja2Templates(directory=str(config.BASE / "templates")) templates = Jinja2Templates(directory=str(config.BASE / "templates"))
oauth = OAuth() oauth = OAuth()
-107
View File
@@ -1,107 +0,0 @@
"""Hovedhusets MCP-gateway på /hovedhus/mcp: Jørn og Lottes egen bro til deres
Claude, helt adskilt fra Asgers gateway på /mcp.
Adskillelsen er hele pointen, så den ligger i konstruktionen og ikke i en
filterregel:
- Egen Authentik-provider (slug ``hovedhus-mcp``, bundet til gruppen Hovedhus)
og egen OIDCProxy med egen nøgle og eget lager. Et token fra /mcp kan ikke
bruges her og omvendt.
- Backends læses fra ``data/hovedhus_gateway.json``, ALDRIG fra
``mcp_registry.json``; ellers ville Asgers gateway montere dem også, og
hovedhusets backends kan ikke se noget der står i registry'et.
- Ingen statiske portal-nøgler: kun OAuth-brugere fra ``allowed_users``.
Monteres som under-app i main.py, så dens auth-middleware kun gælder her.
Mangler konfigurationsfilen, monteres intet.
"""
import json
import logging
from pathlib import Path
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError
from fastmcp.server import create_proxy
from fastmcp.server.auth.oidc_proxy import OIDCProxy
from fastmcp.server.dependencies import get_access_token
from fastmcp.server.middleware import Middleware
from key_value.aio.stores.disk import DiskStore
from mcp_gateway import CLAUDE_REDIRECTS
log = logging.getLogger("portal.hovedhus")
CONF = Path("/opt/portal/data/hovedhus_gateway.json")
PREFIX = "/hovedhus"
BASE_URL = "https://weircon.dk" + PREFIX
def _load() -> dict | None:
try:
return json.loads(CONF.read_text())
except FileNotFoundError:
return None
class UserGate(Middleware):
"""Kun navngivne brugere. Authentik-bindingen til gruppen Hovedhus er den
primære lås; denne er den anden, så en glemt binding eller et refresh-token
fra en der er meldt ud ikke åbner for husstandens økonomi. Fejler lukket."""
def __init__(self, users):
self._users = set(users)
def _ok(self) -> bool:
try:
tok = get_access_token()
except Exception:
return False
if tok is None:
return False
claims = getattr(tok, "claims", None) or {}
sub = claims.get("sub") or getattr(tok, "subject", None)
if sub in self._users:
return True
log.warning("hovedhus-mcp: afvist sub=%r", sub)
return False
async def on_list_tools(self, context, call_next):
if not self._ok():
return []
return await call_next(context)
async def on_call_tool(self, context, call_next):
if not self._ok():
raise ToolError("ingen adgang til hovedhusets økonomi")
return await call_next(context)
def build() -> FastMCP | None:
conf = _load()
if not conf:
return None
o = conf["oidc"]
auth = OIDCProxy(
config_url=o["config_url"], client_id=o["client_id"],
client_secret=o["client_secret"],
base_url=BASE_URL, redirect_path="/oauth/callback",
allowed_client_redirect_uris=CLAUDE_REDIRECTS,
client_storage=DiskStore(directory="/opt/portal/data/mcp_oauth_hovedhus"),
forward_resource=False,
# offline_access giver refresh-token, så planlagte opgaver kan køre
# uden at nogen logger ind igen (se mcp_gateway._build_auth).
required_scopes=["openid", "offline_access"],
require_authorization_consent=False,
)
gw = FastMCP("hovedhus", auth=auth,
instructions="Hovedhusets (Jørn og Lottes) fælles økonomi: "
"bankposteringer, kategorier, budget og rapporter.")
gw.add_middleware(UserGate(conf.get("allowed_users") or []))
for b in conf.get("backends") or []:
proxy = create_proxy({"mcpServers": {b["id"]: {
"transport": "http", "url": b["url"],
"headers": {"Authorization": f"Bearer {b['token']}"}}}})
gw.mount(proxy, namespace=b["id"])
return gw
gateway = build()