feat(opax-mcp): byt GitHub API → Gitea API, legg til /billing/tokens/by-module + /build-status + /onboard/customers + /run

This commit is contained in:
chrischristiansen-glitch 2026-06-15 19:58:03 +02:00
parent 11720267c2
commit cabf895b20

View File

@ -4,16 +4,21 @@ Auth: Cloud Run IAM (Authorization header) + X-MCP-Secret header for tool-level
""" """
import os import os
import httpx import httpx
import base64
from fastapi import FastAPI, HTTPException, Header from fastapi import FastAPI, HTTPException, Header
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, Any 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") OPAX_BASE_URL = os.environ.get("OPAX_BASE_URL", "https://opax.vauco.no")
MCP_SECRET = os.environ.get("MCP_SECRET", "") MCP_SECRET = os.environ.get("MCP_SECRET", "")
GH_REPO = os.environ.get("GH_REPO", "vauco-saas/OSVauco")
GITHUB_PAT = os.environ.get("GITHUB_PAT", "") # 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") 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") @app.get("/health")
async def 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: async def _opax_get(path: str) -> Any:
@ -77,18 +82,35 @@ async def _opax_post(path: str, body: dict) -> Any:
return r.json() 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: 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() r.raise_for_status()
return r.json() return r.json()
async def _gh_post(path: str, body: dict) -> Any: async def _gitea_post(path: str, body: dict) -> Any:
headers = {"Authorization": f"token {GITHUB_PAT}", "Accept": "application/vnd.github+json"}
async with httpx.AsyncClient(timeout=30) as client: 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() r.raise_for_status()
return r.json() return r.json()
@ -98,14 +120,17 @@ async def _gh_post(path: str, body: dict) -> Any:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
async def get_billing_summary(p): return await _opax_get("/billing/summary") 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_forecast(p): return await _opax_get("/billing/forecast")
async def get_billing_credits(p): return await _opax_get("/billing/summary") 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_anomalies(p): return await _opax_get("/billing/anomalies")
async def get_billing_history(p): return await _opax_get("/billing/history") 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_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): 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): async def create_invite(p):
return await _opax_post("/onboard/invite", { return await _opax_post("/onboard/invite", {
"company": p.get("company", p.get("name", "")),
"email": p.get("email"), "email": p.get("email"),
"name": p.get("name", ""),
"tier": p.get("tier", "starter") "tier": p.get("tier", "starter")
}) })
async def list_customers(p): 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): 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): 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): 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): async def get_notify_channels(p):
return await _opax_get("/notify/channels") return await _opax_get("/notify/channels")
@ -145,10 +185,18 @@ async def get_notify_channels(p):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
async def run_jason(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): 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"),
})
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -156,51 +204,56 @@ async def run_emma(p):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
async def get_health(p): return await _opax_get("/health") 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_build_status(p): return await _opax_get("/opax/build-status")
async def get_state(p): return await _opax_get("/state") 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_telemetry(p): return await _opax_get("/telemetry/history")
async def run_terminal(p): 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): async def list_commits(p):
repo = p.get("repo", GH_REPO) repo = p.get("repo", GITEA_REPO)
return await _gh_get(f"/repos/{repo}/commits?per_page={p.get('limit', 10)}") limit = p.get("limit", 10)
return await _gitea_get(f"/repos/{repo}/commits?limit={limit}")
async def get_file(p): async def get_file(p):
repo = p.get("repo", GH_REPO) repo = p.get("repo", GITEA_REPO)
path = p.get("path", "") 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): async def list_open_issues(p):
repo = p.get("repo", GH_REPO) repo = p.get("repo", GITEA_REPO)
return await _gh_get(f"/repos/{repo}/issues?state=open&per_page=20") return await _gitea_get(f"/repos/{repo}/issues?state=open&limit=20")
async def create_github_issue(p): async def create_issue(p):
repo = p.get("repo", GH_REPO) repo = p.get("repo", GITEA_REPO)
return await _gh_post(f"/repos/{repo}/issues", { return await _gitea_post(f"/repos/{repo}/issues", {
"title": p.get("title"), "title": p.get("title"),
"body": p.get("body", "") "body": p.get("body", ""),
}) })
async def push_file(p): async def push_file(p):
"""Oppdater en enkelt fil i GitHub via contents API.""" """Opprett eller oppdater ein fil i Gitea."""
repo = p.get("repo", GH_REPO) repo = p.get("repo", GITEA_REPO)
path = p.get("path") path = p.get("path")
import base64 content = base64.b64encode(p.get("content", "").encode()).decode()
content_b64 = base64.b64encode(p.get("content", "").encode()).decode()
body = { body = {
"message": p.get("message", f"chore: oppdater {path} via opax-mcp"), "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"): if p.get("sha"):
body["sha"] = p["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)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -216,6 +269,7 @@ TOOLS = {
"get_billing_history": get_billing_history, "get_billing_history": get_billing_history,
"get_billing_budget": get_billing_budget, "get_billing_budget": get_billing_budget,
"set_billing_budget": set_billing_budget, "set_billing_budget": set_billing_budget,
"get_billing_tokens_by_module": get_billing_tokens_by_module,
# Onboarding # Onboarding
"create_invite": create_invite, "create_invite": create_invite,
"list_customers": list_customers, "list_customers": list_customers,
@ -233,10 +287,10 @@ TOOLS = {
"get_state": get_state, "get_state": get_state,
"get_telemetry": get_telemetry, "get_telemetry": get_telemetry,
"run_terminal": run_terminal, "run_terminal": run_terminal,
# GitHub # Gitea (erstatter GitHub)
"list_commits": list_commits, "list_commits": list_commits,
"get_file": get_file, "get_file": get_file,
"list_open_issues": list_open_issues, "list_open_issues": list_open_issues,
"create_github_issue": create_github_issue, "create_issue": create_issue,
"push_file": push_file, "push_file": push_file,
} }