feat(opax-mcp): byt GitHub API → Gitea API, legg til /billing/tokens/by-module + /build-status + /onboard/customers + /run
This commit is contained in:
parent
11720267c2
commit
cabf895b20
|
|
@ -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()
|
||||||
|
|
||||||
|
|
@ -97,15 +119,18 @@ async def _gh_post(path: str, body: dict) -> Any:
|
||||||
# Billing tools
|
# Billing tools
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
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", {
|
||||||
"email": p.get("email"),
|
"company": p.get("company", p.get("name", "")),
|
||||||
"name": p.get("name", ""),
|
"email": p.get("email"),
|
||||||
"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,62 +185,75 @@ 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"),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Platform tools
|
# Platform tools
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -209,34 +262,35 @@ async def push_file(p):
|
||||||
|
|
||||||
TOOLS = {
|
TOOLS = {
|
||||||
# Billing
|
# Billing
|
||||||
"get_billing_summary": get_billing_summary,
|
"get_billing_summary": get_billing_summary,
|
||||||
"get_billing_forecast": get_billing_forecast,
|
"get_billing_forecast": get_billing_forecast,
|
||||||
"get_billing_credits": get_billing_credits,
|
"get_billing_credits": get_billing_credits,
|
||||||
"get_billing_anomalies": get_billing_anomalies,
|
"get_billing_anomalies": get_billing_anomalies,
|
||||||
"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,
|
||||||
# Notify
|
# Notify
|
||||||
"send_webhook": send_webhook,
|
"send_webhook": send_webhook,
|
||||||
"send_email": send_email,
|
"send_email": send_email,
|
||||||
"send_sms": send_sms,
|
"send_sms": send_sms,
|
||||||
"get_notify_channels": get_notify_channels,
|
"get_notify_channels": get_notify_channels,
|
||||||
# Agents
|
# Agents
|
||||||
"run_jason": run_jason,
|
"run_jason": run_jason,
|
||||||
"run_emma": run_emma,
|
"run_emma": run_emma,
|
||||||
# Platform
|
# Platform
|
||||||
"get_health": get_health,
|
"get_health": get_health,
|
||||||
"get_build_status": get_build_status,
|
"get_build_status": get_build_status,
|
||||||
"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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user