"""opax-mcp — MCP tool server for OPAX/Vauco Transport: HTTP (FastAPI + uvicorn) for Cloud Run Auth: Cloud Run IAM (Authorization header) + X-MCP-Secret header for tool-level auth """ import os import httpx import base64 from fastapi import FastAPI, HTTPException, Header from pydantic import BaseModel from typing import Optional, Any app = FastAPI(title="opax-mcp", version="2.0.0") OPAX_BASE_URL = os.environ.get("OPAX_BASE_URL", "https://opax.vauco.no") MCP_SECRET = os.environ.get("MCP_SECRET", "") # Gitea GITEA_URL = os.environ.get("GITEA_URL", "http://34.59.131.162:3000") GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "") GITEA_REPO = os.environ.get("GITEA_REPO", "chris/OSVauco") GOOGLE_CLOUD_PROJECT = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5") def _auth_check(x_mcp_secret: Optional[str]): if MCP_SECRET and x_mcp_secret != MCP_SECRET: raise HTTPException(status_code=401, detail="Unauthorized") # --------------------------------------------------------------------------- # Health # --------------------------------------------------------------------------- @app.get("/health") async def health(): return {"status": "ok", "service": "opax-mcp", "version": "2.0.0", "git": "gitea"} # --------------------------------------------------------------------------- # Tool request/response schema # --------------------------------------------------------------------------- class ToolRequest(BaseModel): tool: str params: dict = {} @app.post("/tools/call") async def call_tool( req: ToolRequest, x_mcp_secret: Optional[str] = Header(default=None) ): _auth_check(x_mcp_secret) handler = TOOLS.get(req.tool) if not handler: raise HTTPException(status_code=404, detail=f"Unknown tool: {req.tool}") result = await handler(req.params) return {"tool": req.tool, "result": result} @app.get("/tools") async def list_tools(x_mcp_secret: Optional[str] = Header(default=None)): _auth_check(x_mcp_secret) return {"tools": list(TOOLS.keys())} # --------------------------------------------------------------------------- # Helpers — OPAX # --------------------------------------------------------------------------- async def _opax_get(path: str) -> Any: async with httpx.AsyncClient(timeout=30) as client: r = await client.get(f"{OPAX_BASE_URL}{path}") r.raise_for_status() return r.json() async def _opax_post(path: str, body: dict) -> Any: async with httpx.AsyncClient(timeout=30) as client: r = await client.post(f"{OPAX_BASE_URL}{path}", json=body) r.raise_for_status() return r.json() # --------------------------------------------------------------------------- # Helpers — Gitea # --------------------------------------------------------------------------- def _gitea_headers() -> dict: return { "Authorization": f"token {GITEA_TOKEN}", "Content-Type": "application/json", "Accept": "application/json", } async def _gitea_get(path: str) -> Any: async with httpx.AsyncClient(timeout=30) as client: r = await client.get(f"{GITEA_URL}/api/v1{path}", headers=_gitea_headers()) r.raise_for_status() return r.json() async def _gitea_post(path: str, body: dict) -> Any: async with httpx.AsyncClient(timeout=30) as client: r = await client.post(f"{GITEA_URL}/api/v1{path}", json=body, headers=_gitea_headers()) r.raise_for_status() return r.json() async def _gitea_put(path: str, body: dict) -> Any: async with httpx.AsyncClient(timeout=30) as client: r = await client.put(f"{GITEA_URL}/api/v1{path}", json=body, headers=_gitea_headers()) r.raise_for_status() return r.json() # --------------------------------------------------------------------------- # Billing tools # --------------------------------------------------------------------------- async def get_billing_summary(p): return await _opax_get("/billing/summary") async def get_billing_forecast(p): return await _opax_get("/billing/forecast") async def get_billing_credits(p): return await _opax_get("/billing/credits") async def get_billing_anomalies(p): return await _opax_get("/billing/anomalies") async def get_billing_history(p): return await _opax_get("/billing/history") async def get_billing_budget(p): return await _opax_get("/billing/budget") async def get_billing_tokens_by_module(p): return await _opax_get("/telemetry/history") async def set_billing_budget(p): return await _opax_post("/billing/budget", { "budget": p.get("amount", 500) }) # --------------------------------------------------------------------------- # Onboarding tools # --------------------------------------------------------------------------- async def create_invite(p): return await _opax_post("/onboard/invite", { "company": p.get("company", p.get("name", "")), "email": p.get("email"), "tier": p.get("tier", "starter") }) async def list_customers(p): """Hent onboardede kunder frå Firestore via OPAX state.""" return await _opax_get("/state") # --------------------------------------------------------------------------- # Notify tools # --------------------------------------------------------------------------- async def send_webhook(p): return await _opax_post("/notify/webhook", { "url": p.get("url", ""), "event": p.get("event", "custom"), "title": p.get("title", "OPAX varsel"), "body": p.get("message", p.get("body", "")), }) async def send_email(p): return await _opax_post("/notify/email", { "to": p.get("to"), "subject": p.get("subject"), "event": p.get("event", "digest"), }) async def send_sms(p): return await _opax_post("/notify/sms", { "to": p.get("to"), "body": p.get("message", p.get("body", "")), "event": p.get("event", "spike"), "tier": p.get("tier", "guard"), }) async def get_notify_channels(p): return await _opax_get("/notify/channels") # --------------------------------------------------------------------------- # Agent tools # --------------------------------------------------------------------------- async def run_jason(p): return await _opax_post("/run", { "message": p.get("prompt", p.get("message", "")), "user_id": p.get("user_id", "opax"), "session_id": p.get("session_id", "mcp"), "mode": p.get("mode", "light"), }) async def run_emma(p): return await _opax_post("/emma", { "message": p.get("prompt", p.get("message", "")), "session_id": p.get("session_id", "mcp"), }) # --------------------------------------------------------------------------- # Platform tools # --------------------------------------------------------------------------- async def get_health(p): return await _opax_get("/health") async def get_build_status(p): return await _opax_get("/opax/build-status") async def get_state(p): return await _opax_get("/state") async def get_telemetry(p): return await _opax_get("/telemetry/history") async def run_terminal(p): return await _opax_post("/terminal/exec", { "cmd": p.get("command", p.get("cmd", "help")) }) # --------------------------------------------------------------------------- # Gitea tools (erstatter GitHub) # --------------------------------------------------------------------------- async def list_commits(p): repo = p.get("repo", GITEA_REPO) limit = p.get("limit", 10) return await _gitea_get(f"/repos/{repo}/commits?limit={limit}") async def get_file(p): repo = p.get("repo", GITEA_REPO) path = p.get("path", "") ref = p.get("ref", "main") return await _gitea_get(f"/repos/{repo}/contents/{path}?ref={ref}") async def list_open_issues(p): repo = p.get("repo", GITEA_REPO) return await _gitea_get(f"/repos/{repo}/issues?state=open&limit=20") async def create_issue(p): repo = p.get("repo", GITEA_REPO) return await _gitea_post(f"/repos/{repo}/issues", { "title": p.get("title"), "body": p.get("body", ""), }) async def push_file(p): """Opprett eller oppdater ein fil i Gitea.""" repo = p.get("repo", GITEA_REPO) path = p.get("path") content = base64.b64encode(p.get("content", "").encode()).decode() body = { "message": p.get("message", f"chore: oppdater {path} via opax-mcp"), "content": content, "branch": p.get("branch", "main"), } if p.get("sha"): body["sha"] = p["sha"] return await _gitea_put(f"/repos/{repo}/contents/{path}", body) return await _gitea_post(f"/repos/{repo}/contents/{path}", body) # --------------------------------------------------------------------------- # Tool registry # --------------------------------------------------------------------------- TOOLS = { # Billing "get_billing_summary": get_billing_summary, "get_billing_forecast": get_billing_forecast, "get_billing_credits": get_billing_credits, "get_billing_anomalies": get_billing_anomalies, "get_billing_history": get_billing_history, "get_billing_budget": get_billing_budget, "set_billing_budget": set_billing_budget, "get_billing_tokens_by_module": get_billing_tokens_by_module, # Onboarding "create_invite": create_invite, "list_customers": list_customers, # Notify "send_webhook": send_webhook, "send_email": send_email, "send_sms": send_sms, "get_notify_channels": get_notify_channels, # Agents "run_jason": run_jason, "run_emma": run_emma, # Platform "get_health": get_health, "get_build_status": get_build_status, "get_state": get_state, "get_telemetry": get_telemetry, "run_terminal": run_terminal, # Gitea (erstatter GitHub) "list_commits": list_commits, "get_file": get_file, "list_open_issues": list_open_issues, "create_issue": create_issue, "push_file": push_file, }