From e05c37645245ecb6b74413c6db43087fa595b00c Mon Sep 17 00:00:00 2001 From: Asger Geel Weirsoee Date: Tue, 29 Sep 2026 17:32:32 +0200 Subject: [PATCH] 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 --- main.py | 29 +------------ mcp_hovedhus.py | 107 ------------------------------------------------ 2 files changed, 1 insertion(+), 135 deletions(-) delete mode 100644 mcp_hovedhus.py diff --git a/main.py b/main.py index c103222..581c102 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,6 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from starlette.middleware.sessions import SessionMiddleware -import contextlib import time import authentik as ak @@ -21,30 +20,11 @@ import ytdl as yt import mcpkeys import mcp_server import mcp_gateway -import mcp_hovedhus # Portalens egen MCP (FastMCP), monteret in-process på /mcp. json_response=True # => almindelig JSON (ingen SSE), så det passerer edge'en utunet. Bearer-gated. mcp_app = mcp_gateway.gateway.http_app(path="/mcp", json_response=True) - -# 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) +app = FastAPI(title="Weircon Portal", lifespan=mcp_app.lifespan) for _mcp_route in mcp_app.routes: app.router.routes.append(_mcp_route) # 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, same_site="lax", max_age=60 * 60 * 12) 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")) oauth = OAuth() diff --git a/mcp_hovedhus.py b/mcp_hovedhus.py deleted file mode 100644 index 6a4a812..0000000 --- a/mcp_hovedhus.py +++ /dev/null @@ -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()