diff --git a/opax-mcp/server.py b/opax-mcp/server.py index 783ebb9..a96a587 100644 --- a/opax-mcp/server.py +++ b/opax-mcp/server.py @@ -4,16 +4,21 @@ Auth: Cloud Run IAM (Authorization header) + X-MCP-Secret header for tool-level """ 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="1.0.0") +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", "") -GH_REPO = os.environ.get("GH_REPO", "vauco-saas/OSVauco") -GITHUB_PAT = os.environ.get("GITHUB_PAT", "") +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") @@ -23,12 +28,12 @@ def _auth_check(x_mcp_secret: Optional[str]): # --------------------------------------------------------------------------- -# Health — ingen auth, Cloud Run trenger denne +# Health # --------------------------------------------------------------------------- @app.get("/health") async def health(): - return {"status": "ok", "service": "opax-mcp"} + return {"status": "ok", "service": "opax-mcp", "version": "2.0.0", "git": "gitea"} # --------------------------------------------------------------------------- @@ -60,7 +65,7 @@ async def list_tools(x_mcp_secret: Optional[str] = Header(default=None)): # --------------------------------------------------------------------------- -# Helpers +# Helpers — OPAX # --------------------------------------------------------------------------- async def _opax_get(path: str) -> Any: @@ -77,18 +82,35 @@ async def _opax_post(path: str, body: dict) -> Any: return r.json() -async def _gh_get(path: str) -> Any: - headers = {"Authorization": f"token {GITHUB_PAT}", "Accept": "application/vnd.github+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"https://api.github.com{path}", headers=headers) + r = await client.get(f"{GITEA_URL}/api/v1{path}", headers=_gitea_headers()) r.raise_for_status() return r.json() -async def _gh_post(path: str, body: dict) -> Any: - headers = {"Authorization": f"token {GITHUB_PAT}", "Accept": "application/vnd.github+json"} +async def _gitea_post(path: str, body: dict) -> Any: async with httpx.AsyncClient(timeout=30) as client: - r = await client.post(f"https://api.github.com{path}", json=body, headers=headers) + 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() @@ -97,15 +119,18 @@ async def _gh_post(path: str, body: dict) -> Any: # 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/tokens/estimate") -async def get_billing_credits(p): return await _opax_get("/billing/summary") -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_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", {"amount": p.get("amount"), "currency": p.get("currency", "USD")}) + return await _opax_post("/billing/budget", { + "budget": p.get("amount", 500) + }) # --------------------------------------------------------------------------- @@ -114,13 +139,14 @@ async def set_billing_budget(p): async def create_invite(p): return await _opax_post("/onboard/invite", { - "email": p.get("email"), - "name": p.get("name", ""), - "tier": p.get("tier", "starter") + "company": p.get("company", p.get("name", "")), + "email": p.get("email"), + "tier": p.get("tier", "starter") }) async def list_customers(p): - return await _opax_get("/onboard/customers") + """Hent onboardede kunder frå Firestore via OPAX state.""" + return await _opax_get("/state") # --------------------------------------------------------------------------- @@ -128,13 +154,27 @@ async def list_customers(p): # --------------------------------------------------------------------------- async def send_webhook(p): - return await _opax_post("/notify/webhook", {"message": p.get("message"), "url": p.get("url", "")}) + 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"), "body": p.get("body")}) + 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"), "message": p.get("message")}) + 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") @@ -145,62 +185,75 @@ async def get_notify_channels(p): # --------------------------------------------------------------------------- async def run_jason(p): - return await _opax_post("/run", {"prompt": p.get("prompt"), "agent": "jason"}) + 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("/run", {"prompt": p.get("prompt"), "agent": "emma"}) + 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("/build-status") -async def get_state(p): return await _opax_get("/state") -async def get_telemetry(p): return await _opax_get("/billing/tokens/by-module") +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", {"command": p.get("command")}) + return await _opax_post("/terminal/exec", { + "cmd": p.get("command", p.get("cmd", "help")) + }) # --------------------------------------------------------------------------- -# GitHub tools +# Gitea tools (erstatter GitHub) # --------------------------------------------------------------------------- async def list_commits(p): - repo = p.get("repo", GH_REPO) - return await _gh_get(f"/repos/{repo}/commits?per_page={p.get('limit', 10)}") + 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", GH_REPO) + repo = p.get("repo", GITEA_REPO) path = p.get("path", "") - return await _gh_get(f"/repos/{repo}/contents/{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", GH_REPO) - return await _gh_get(f"/repos/{repo}/issues?state=open&per_page=20") + repo = p.get("repo", GITEA_REPO) + return await _gitea_get(f"/repos/{repo}/issues?state=open&limit=20") -async def create_github_issue(p): - repo = p.get("repo", GH_REPO) - return await _gh_post(f"/repos/{repo}/issues", { +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", "") + "body": p.get("body", ""), }) async def push_file(p): - """Oppdater en enkelt fil i GitHub via contents API.""" - repo = p.get("repo", GH_REPO) - path = p.get("path") - import base64 - content_b64 = base64.b64encode(p.get("content", "").encode()).decode() + """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_b64, + "content": content, + "branch": p.get("branch", "main"), } if p.get("sha"): body["sha"] = p["sha"] - return await _gh_post(f"/repos/{repo}/contents/{path}", body) + return await _gitea_put(f"/repos/{repo}/contents/{path}", body) + return await _gitea_post(f"/repos/{repo}/contents/{path}", body) # --------------------------------------------------------------------------- @@ -209,34 +262,35 @@ async def push_file(p): 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_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, + "create_invite": create_invite, + "list_customers": list_customers, # Notify - "send_webhook": send_webhook, - "send_email": send_email, - "send_sms": send_sms, + "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, + "run_emma": run_emma, # Platform - "get_health": get_health, + "get_health": get_health, "get_build_status": get_build_status, - "get_state": get_state, - "get_telemetry": get_telemetry, - "run_terminal": run_terminal, - # GitHub - "list_commits": list_commits, - "get_file": get_file, + "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_github_issue": create_github_issue, - "push_file": push_file, + "create_issue": create_issue, + "push_file": push_file, }