fix(CI1): opax-mcp — bytt til uvicorn/FastAPI HTTP server, legg til /health
This commit is contained in:
parent
22f7774809
commit
27290b2ab6
|
|
@ -7,10 +7,8 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# MCP over HTTP (SSE transport) for Cloud Run
|
|
||||||
ENV MCP_TRANSPORT=sse
|
|
||||||
ENV PORT=8080
|
ENV PORT=8080
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
CMD ["python", "-m", "mcp", "run", "server.py", "--transport", "sse", "--port", "8080"]
|
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
mcp[cli]>=1.3.0
|
fastapi>=0.111.0
|
||||||
|
uvicorn>=0.30.0
|
||||||
httpx>=0.27.0
|
httpx>=0.27.0
|
||||||
google-auth>=2.29.0
|
google-auth>=2.29.0
|
||||||
google-cloud-firestore>=2.16.0
|
google-cloud-firestore>=2.16.0
|
||||||
|
|
|
||||||
|
|
@ -1,357 +1,242 @@
|
||||||
#!/usr/bin/env python3
|
"""opax-mcp — MCP tool server for OPAX/Vauco
|
||||||
|
Transport: HTTP (FastAPI + uvicorn) for Cloud Run
|
||||||
|
Alle tools kalles av Jason/Emma internt — ingen 3part.
|
||||||
"""
|
"""
|
||||||
opax-mcp/server.py — OPAX MCP Server
|
|
||||||
|
|
||||||
Eksponerer hele OPAX-plattformen som MCP-tools.
|
|
||||||
Kjører som egen Cloud Run-service: opax-mcp (us-central1).
|
|
||||||
|
|
||||||
Auth: Bearer token via MCP_SECRET env-var (Secret Manager: mcp-server-key).
|
|
||||||
Base: OPAX_BASE_URL (default https://opax.vauco.no)
|
|
||||||
|
|
||||||
Tools:
|
|
||||||
Billing: get_billing_summary, get_billing_forecast, get_billing_credits,
|
|
||||||
get_billing_anomalies, get_billing_history, get_billing_budget, set_billing_budget
|
|
||||||
Onboarding: create_invite, list_customers
|
|
||||||
Notify: send_webhook, send_email, send_sms
|
|
||||||
Agent: run_jason, run_emma
|
|
||||||
Platform: get_health, get_build_status, get_notify_channels,
|
|
||||||
get_state, get_telemetry
|
|
||||||
GitHub: list_commits, get_file, search_code
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import httpx
|
import httpx
|
||||||
from typing import Any
|
from fastapi import FastAPI, HTTPException, Header
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, Any
|
||||||
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
app = FastAPI(title="opax-mcp", version="1.0.0")
|
||||||
|
|
||||||
# ── CONFIG ─────────────────────────────────────────────────────────────────
|
|
||||||
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_PAT = os.environ.get("GITHUB_PAT", "")
|
|
||||||
GH_REPO = os.environ.get("GH_REPO", "vauco-saas/OSVauco")
|
GH_REPO = os.environ.get("GH_REPO", "vauco-saas/OSVauco")
|
||||||
PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5")
|
GITHUB_PAT = os.environ.get("GITHUB_PAT", "")
|
||||||
|
GOOGLE_CLOUD_PROJECT = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5")
|
||||||
mcp = FastMCP("opax-mcp")
|
|
||||||
|
|
||||||
|
|
||||||
# ── HTTP HELPERS ─────────────────────────────────────────────────────────────
|
def _auth_check(authorization: Optional[str]):
|
||||||
def _opax_headers() -> dict:
|
if MCP_SECRET and authorization != f"Bearer {MCP_SECRET}":
|
||||||
headers = {"Content-Type": "application/json"}
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
if MCP_SECRET:
|
|
||||||
headers["Authorization"] = f"Bearer {MCP_SECRET}"
|
|
||||||
# IAP identity token (Cloud Run-to-Cloud Run)
|
|
||||||
try:
|
|
||||||
import google.auth
|
|
||||||
import google.auth.transport.requests
|
|
||||||
creds, _ = google.auth.default()
|
|
||||||
creds.refresh(google.auth.transport.requests.Request())
|
|
||||||
headers["x-goog-authenticated-user-email"] = "serviceAccount:opax-mcp"
|
|
||||||
# For IAP: send identity token
|
|
||||||
import urllib.request
|
|
||||||
meta_url = f"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience={OPAX_BASE_URL}&format=full"
|
|
||||||
req = urllib.request.Request(meta_url, headers={"Metadata-Flavor": "Google"})
|
|
||||||
token = urllib.request.urlopen(req, timeout=2).read().decode()
|
|
||||||
headers["Authorization"] = f"Bearer {token}"
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return headers
|
|
||||||
|
|
||||||
|
|
||||||
def _gh_headers() -> dict:
|
# ---------------------------------------------------------------------------
|
||||||
h = {"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
|
# Health
|
||||||
if GH_PAT:
|
# ---------------------------------------------------------------------------
|
||||||
h["Authorization"] = f"Bearer {GH_PAT}"
|
|
||||||
return h
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
return {"status": "ok", "service": "opax-mcp"}
|
||||||
|
|
||||||
|
|
||||||
async def _get(path: str, params: dict = None) -> Any:
|
# ---------------------------------------------------------------------------
|
||||||
async with httpx.AsyncClient(timeout=20.0) as c:
|
# Tool request/response schema
|
||||||
r = await c.get(f"{OPAX_BASE_URL}{path}", headers=_opax_headers(), params=params)
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ToolRequest(BaseModel):
|
||||||
|
tool: str
|
||||||
|
params: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/tools/call")
|
||||||
|
async def call_tool(
|
||||||
|
req: ToolRequest,
|
||||||
|
authorization: Optional[str] = Header(default=None)
|
||||||
|
):
|
||||||
|
_auth_check(authorization)
|
||||||
|
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(authorization: Optional[str] = Header(default=None)):
|
||||||
|
_auth_check(authorization)
|
||||||
|
return {"tools": list(TOOLS.keys())}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
async def _post(path: str, body: dict = None) -> Any:
|
async def _opax_post(path: str, body: dict) -> Any:
|
||||||
async with httpx.AsyncClient(timeout=30.0) as c:
|
async with httpx.AsyncClient(timeout=30) as client:
|
||||||
r = await c.post(f"{OPAX_BASE_URL}{path}", headers=_opax_headers(), json=body or {})
|
r = await client.post(f"{OPAX_BASE_URL}{path}", json=body)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
async def _gh_get(path: str, params: dict = None) -> Any:
|
async def _gh_get(path: str) -> Any:
|
||||||
async with httpx.AsyncClient(timeout=15.0) as c:
|
headers = {"Authorization": f"token {GITHUB_PAT}", "Accept": "application/vnd.github+json"}
|
||||||
r = await c.get(f"https://api.github.com{path}", headers=_gh_headers(), params=params)
|
async with httpx.AsyncClient(timeout=30) as client:
|
||||||
|
r = await client.get(f"https://api.github.com{path}", headers=headers)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
async def _gh_post(path: str, body: dict) -> Any:
|
||||||
# BILLING TOOLS
|
headers = {"Authorization": f"token {GITHUB_PAT}", "Accept": "application/vnd.github+json"}
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
async with httpx.AsyncClient(timeout=30) as client:
|
||||||
|
r = await client.post(f"https://api.github.com{path}", json=body, headers=headers)
|
||||||
@mcp.tool()
|
r.raise_for_status()
|
||||||
async def get_billing_summary() -> str:
|
return r.json()
|
||||||
"""Hent GCP-kostnadsoversikt per tjeneste for inneværende måned."""
|
|
||||||
data = await _get("/billing/summary")
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
# ---------------------------------------------------------------------------
|
||||||
async def get_billing_forecast() -> str:
|
# Billing tools
|
||||||
"""Hent kostnadsprøve og daglig snitt siste 7 dager."""
|
# ---------------------------------------------------------------------------
|
||||||
data = await _get("/billing/forecast")
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
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 set_billing_budget(p):
|
||||||
|
return await _opax_post("/billing/budget", {"amount": p.get("amount"), "currency": p.get("currency", "USD")})
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
# ---------------------------------------------------------------------------
|
||||||
async def get_billing_credits(days: int = 90) -> str:
|
# Onboarding tools
|
||||||
"""Hent Vertex/GCP-kredittbalanse, burn rate og estimert tom-dato."""
|
# ---------------------------------------------------------------------------
|
||||||
data = await _get("/billing/credits", params={"days": days})
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
async def create_invite(p):
|
||||||
|
return await _opax_post("/onboard/invite", {
|
||||||
|
"email": p.get("email"),
|
||||||
|
"name": p.get("name", ""),
|
||||||
|
"tier": p.get("tier", "starter")
|
||||||
|
})
|
||||||
|
|
||||||
|
async def list_customers(p):
|
||||||
|
return await _opax_get("/onboard/customers")
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
# ---------------------------------------------------------------------------
|
||||||
async def get_billing_anomalies() -> str:
|
# Notify tools
|
||||||
"""Detect kostnadsanomalier i GCP-forbruket."""
|
# ---------------------------------------------------------------------------
|
||||||
data = await _get("/billing/anomalies")
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
async def send_webhook(p):
|
||||||
|
return await _opax_post("/notify/webhook", {"message": p.get("message"), "url": p.get("url", "")})
|
||||||
|
|
||||||
|
async def send_email(p):
|
||||||
|
return await _opax_post("/notify/email", {"to": p.get("to"), "subject": p.get("subject"), "body": p.get("body")})
|
||||||
|
|
||||||
|
async def send_sms(p):
|
||||||
|
return await _opax_post("/notify/sms", {"to": p.get("to"), "message": p.get("message")})
|
||||||
|
|
||||||
|
async def get_notify_channels(p):
|
||||||
|
return await _opax_get("/notify/channels")
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
# ---------------------------------------------------------------------------
|
||||||
async def get_billing_history() -> str:
|
# Agent tools
|
||||||
"""Hent historisk daglig kostnad siste 90 dager."""
|
# ---------------------------------------------------------------------------
|
||||||
data = await _get("/billing/history")
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
async def run_jason(p):
|
||||||
|
return await _opax_post("/run", {"prompt": p.get("prompt"), "agent": "jason"})
|
||||||
|
|
||||||
|
async def run_emma(p):
|
||||||
|
return await _opax_post("/run", {"prompt": p.get("prompt"), "agent": "emma"})
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
# ---------------------------------------------------------------------------
|
||||||
async def get_billing_budget() -> str:
|
# Platform tools
|
||||||
"""Hent nåværende budsjettgrense (NOK)."""
|
# ---------------------------------------------------------------------------
|
||||||
data = await _get("/billing/budget")
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
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 run_terminal(p):
|
||||||
|
return await _opax_post("/terminal/exec", {"command": p.get("command")})
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
# ---------------------------------------------------------------------------
|
||||||
async def set_billing_budget(budget: float) -> str:
|
# GitHub tools
|
||||||
"""Sett ny budsjettgrense i NOK. Eksempel: 1500.0"""
|
# ---------------------------------------------------------------------------
|
||||||
data = await _post("/billing/budget", {"budget": budget})
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
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)}")
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
async def get_file(p):
|
||||||
# ONBOARDING TOOLS
|
repo = p.get("repo", GH_REPO)
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
path = p.get("path", "")
|
||||||
|
return await _gh_get(f"/repos/{repo}/contents/{path}")
|
||||||
|
|
||||||
@mcp.tool()
|
async def list_open_issues(p):
|
||||||
async def create_invite(company: str, email: str, tier: str = "starter") -> str:
|
repo = p.get("repo", GH_REPO)
|
||||||
"""
|
return await _gh_get(f"/repos/{repo}/issues?state=open&per_page=20")
|
||||||
Opprett en CostGuard-invitasjon og send til prospekt.
|
|
||||||
Returnerer ferdig onboarding-URL (72t gyldig).
|
|
||||||
tier: starter | guard | shield | enterprise
|
|
||||||
"""
|
|
||||||
data = await _post("/onboard/invite", {"company": company, "email": email, "tier": tier})
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
async def create_github_issue(p):
|
||||||
|
repo = p.get("repo", GH_REPO)
|
||||||
|
return await _gh_post(f"/repos/{repo}/issues", {
|
||||||
|
"title": p.get("title"),
|
||||||
|
"body": p.get("body", "")
|
||||||
|
})
|
||||||
|
|
||||||
@mcp.tool()
|
async def push_file(p):
|
||||||
async def list_customers() -> str:
|
"""Oppdater en enkelt fil i GitHub via contents API."""
|
||||||
"""List alle onboardede CostGuard-kunder fra Firestore."""
|
repo = p.get("repo", GH_REPO)
|
||||||
try:
|
path = p.get("path")
|
||||||
import google.auth
|
|
||||||
from google.cloud import firestore
|
|
||||||
google.auth.default()
|
|
||||||
db = firestore.Client(project=PROJECT_ID)
|
|
||||||
docs = db.collection("onboarded_customers").stream()
|
|
||||||
customers = []
|
|
||||||
for doc in docs:
|
|
||||||
d = doc.to_dict()
|
|
||||||
d["id"] = doc.id
|
|
||||||
# Fjern sensitive felter
|
|
||||||
d.pop("webhook_url", None)
|
|
||||||
customers.append(d)
|
|
||||||
return json.dumps({"customers": customers, "count": len(customers)}, ensure_ascii=False, indent=2, default=str)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"error": str(e)})
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
# NOTIFY TOOLS
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def send_webhook(url: str, title: str, body: str, event: str = "custom") -> str:
|
|
||||||
"""Send varsel til Slack/Teams/Google Chat webhook."""
|
|
||||||
data = await _post("/notify/webhook", {"url": url, "title": title, "body": body, "event": event})
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def send_email(to: str, subject: str, body_html: str, event: str = "custom") -> str:
|
|
||||||
"""Send e-post via SendGrid. body_html kan være enkel HTML eller ren tekst."""
|
|
||||||
data = await _post("/notify/email", {"to": to, "subject": subject, "body_html": body_html, "event": event})
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def send_sms(to: str, body: str, tier: str, event: str = "spike") -> str:
|
|
||||||
"""
|
|
||||||
Send SMS via Twilio. Krever Guard+-tier.
|
|
||||||
to: mobilnummer i E.164-format (+4712345678)
|
|
||||||
"""
|
|
||||||
data = await _post("/notify/sms", {"to": to, "body": body, "event": event, "tier": tier})
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def get_notify_channels() -> str:
|
|
||||||
"""List alle konfigurerte varslingskanaler (email, sms, webhook)."""
|
|
||||||
data = await _get("/notify/channels")
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
# AGENT TOOLS
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def run_jason(message: str, session_id: str = "mcp", mode: str = "light") -> str:
|
|
||||||
"""
|
|
||||||
Kjør Jason (kundevendt agent, gemini-2.5-flash).
|
|
||||||
mode: light | heavy
|
|
||||||
"""
|
|
||||||
data = await _post("/run", {"message": message, "user_id": "mcp", "session_id": session_id, "mode": mode})
|
|
||||||
return data.get("response", json.dumps(data))
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def run_emma(message: str, session_id: str = "mcp-emma", history: list = None) -> str:
|
|
||||||
"""
|
|
||||||
Kjør Emma (intern agent, Vertex/Gemma).
|
|
||||||
history: [{"role": "user"|"model", "content": str}]
|
|
||||||
"""
|
|
||||||
data = await _post("/emma", {"message": message, "session_id": session_id, "history": history or []})
|
|
||||||
return data.get("response", json.dumps(data))
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
# PLATFORM TOOLS
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def get_health() -> str:
|
|
||||||
"""Sjekk om OPAX Cloud Run-service er oppe."""
|
|
||||||
data = await _get("/health")
|
|
||||||
return json.dumps(data, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def get_build_status() -> str:
|
|
||||||
"""Hent siste 5 Cloud Build-kjøringer og status."""
|
|
||||||
data = await _get("/opax/build-status")
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def get_state() -> str:
|
|
||||||
"""Hent OPAX agent-state snapshot (telemetri, siste kjøringer)."""
|
|
||||||
data = await _get("/state")
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def get_telemetry(limit: int = 20) -> str:
|
|
||||||
"""Hent OPAX agent-telemetri historikk."""
|
|
||||||
data = await _get("/telemetry/history", params={"limit": limit})
|
|
||||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def run_terminal(cmd: str) -> str:
|
|
||||||
"""
|
|
||||||
Kjør whitelisted OPAX terminal-kommando.
|
|
||||||
Gyldige: health | billing | build | logs | help
|
|
||||||
"""
|
|
||||||
data = await _post("/terminal/exec", {"cmd": cmd})
|
|
||||||
return data.get("output", json.dumps(data))
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
# GITHUB TOOLS
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def list_commits(branch: str = "main", per_page: int = 10) -> str:
|
|
||||||
"""List siste commits i OSVauco-repo."""
|
|
||||||
data = await _gh_get(f"/repos/{GH_REPO}/commits", params={"sha": branch, "per_page": per_page})
|
|
||||||
commits = [{"sha": c["sha"][:8], "message": c["commit"]["message"].split("\n")[0], "author": c["commit"]["author"]["name"], "date": c["commit"]["author"]["date"]} for c in data]
|
|
||||||
return json.dumps(commits, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def get_file(path: str, ref: str = "main") -> str:
|
|
||||||
"""Hent innholdet i en fil fra OSVauco-repo. path: f.eks. 'docs/MASTERPLAN.md'"""
|
|
||||||
data = await _gh_get(f"/repos/{GH_REPO}/contents/{path}", params={"ref": ref})
|
|
||||||
import base64
|
import base64
|
||||||
content = base64.b64decode(data["content"]).decode("utf-8")
|
content_b64 = base64.b64encode(p.get("content", "").encode()).decode()
|
||||||
return content
|
body = {
|
||||||
|
"message": p.get("message", f"chore: oppdater {path} via opax-mcp"),
|
||||||
|
"content": content_b64,
|
||||||
|
}
|
||||||
|
if p.get("sha"):
|
||||||
|
body["sha"] = p["sha"]
|
||||||
|
return await _gh_post(f"/repos/{repo}/contents/{path}", body)
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
# ---------------------------------------------------------------------------
|
||||||
async def list_open_issues() -> str:
|
# Tool registry
|
||||||
"""List åpne issues i OSVauco-repo (OQ-lista)."""
|
# ---------------------------------------------------------------------------
|
||||||
data = await _gh_get(f"/repos/{GH_REPO}/issues", params={"state": "open", "per_page": 30})
|
|
||||||
issues = [{"number": i["number"], "title": i["title"], "labels": [l["name"] for l in i["labels"]], "created_at": i["created_at"]} for i in data if "pull_request" not in i]
|
|
||||||
return json.dumps(issues, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
TOOLS = {
|
||||||
@mcp.tool()
|
# Billing
|
||||||
async def create_github_issue(title: str, body: str, labels: list = None) -> str:
|
"get_billing_summary": get_billing_summary,
|
||||||
"""Opprett et nytt GitHub issue i OSVauco-repo."""
|
"get_billing_forecast": get_billing_forecast,
|
||||||
async with httpx.AsyncClient(timeout=15.0) as c:
|
"get_billing_credits": get_billing_credits,
|
||||||
r = await c.post(
|
"get_billing_anomalies": get_billing_anomalies,
|
||||||
f"https://api.github.com/repos/{GH_REPO}/issues",
|
"get_billing_history": get_billing_history,
|
||||||
headers=_gh_headers(),
|
"get_billing_budget": get_billing_budget,
|
||||||
json={"title": title, "body": body, "labels": labels or []}
|
"set_billing_budget": set_billing_budget,
|
||||||
)
|
# Onboarding
|
||||||
r.raise_for_status()
|
"create_invite": create_invite,
|
||||||
data = r.json()
|
"list_customers": list_customers,
|
||||||
return json.dumps({"number": data["number"], "url": data["html_url"], "title": data["title"]}, indent=2)
|
# Notify
|
||||||
|
"send_webhook": send_webhook,
|
||||||
|
"send_email": send_email,
|
||||||
@mcp.tool()
|
"send_sms": send_sms,
|
||||||
async def push_file(path: str, content: str, message: str, branch: str = "main") -> str:
|
"get_notify_channels": get_notify_channels,
|
||||||
"""
|
# Agents
|
||||||
Opprett eller oppdater en fil i OSVauco-repo.
|
"run_jason": run_jason,
|
||||||
Henter nåværende SHA automatisk hvis filen eksisterer.
|
"run_emma": run_emma,
|
||||||
"""
|
# Platform
|
||||||
# Hent nåværende SHA
|
"get_health": get_health,
|
||||||
sha = None
|
"get_build_status": get_build_status,
|
||||||
try:
|
"get_state": get_state,
|
||||||
existing = await _gh_get(f"/repos/{GH_REPO}/contents/{path}", params={"ref": branch})
|
"get_telemetry": get_telemetry,
|
||||||
sha = existing.get("sha")
|
"run_terminal": run_terminal,
|
||||||
except Exception:
|
# GitHub
|
||||||
pass
|
"list_commits": list_commits,
|
||||||
|
"get_file": get_file,
|
||||||
import base64
|
"list_open_issues": list_open_issues,
|
||||||
encoded = base64.b64encode(content.encode("utf-8")).decode("ascii")
|
"create_github_issue": create_github_issue,
|
||||||
payload = {"message": message, "content": encoded, "branch": branch}
|
"push_file": push_file,
|
||||||
if sha:
|
}
|
||||||
payload["sha"] = sha
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=15.0) as c:
|
|
||||||
r = await c.put(
|
|
||||||
f"https://api.github.com/repos/{GH_REPO}/contents/{path}",
|
|
||||||
headers=_gh_headers(),
|
|
||||||
json=payload
|
|
||||||
)
|
|
||||||
r.raise_for_status()
|
|
||||||
data = r.json()
|
|
||||||
return json.dumps({"path": path, "sha": data["content"]["sha"][:8], "url": data["content"]["html_url"]}, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
# ── ENTRYPOINT ─────────────────────────────────────────────────────────────────
|
|
||||||
if __name__ == "__main__":
|
|
||||||
mcp.run(transport="stdio")
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user