fix(mcp): use Cloud Run project URL and Streamable HTTP client
Some checks are pending
Check Python Version Consistency / Check Python Version (push) Waiting to run

This commit is contained in:
Chris Christiansen 2026-09-03 10:44:23 +00:00
parent 6670c2b67b
commit 86551f2def
3 changed files with 95 additions and 222 deletions

View File

@ -6,7 +6,7 @@ Auth-lag:
1. Cloud Run IAM : Authorization: Bearer <identity-token> (automatisk) 1. Cloud Run IAM : Authorization: Bearer <identity-token> (automatisk)
2. Tool-level : X-MCP-Secret: <mcp-server-key> (frå env) 2. Tool-level : X-MCP-Secret: <mcp-server-key> (frå env)
URL: https://opax-mcp-zjbqp3prqq-uc.a.run.app URL: https://opax-mcp-357036551735.us-central1.run.app
""" """
import os import os
@ -14,7 +14,7 @@ import httpx
import google.auth import google.auth
import google.auth.transport.requests import google.auth.transport.requests
OPAX_MCP_URL = os.environ.get("MCP_SERVER_URL", "https://opax-mcp-zjbqp3prqq-uc.a.run.app") OPAX_MCP_URL = os.environ.get("MCP_SERVER_URL", "https://opax-mcp-357036551735.us-central1.run.app")
MCP_SECRET = os.environ.get("MCP_SECRET", "") MCP_SECRET = os.environ.get("MCP_SECRET", "")

View File

@ -23,7 +23,7 @@ from google.adk.tools.mcp_tool.mcp_toolset import (
) )
PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5") PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5")
OPAX_MCP_URL = os.environ.get("MCP_SERVER_URL", "https://opax-mcp-zjbqp3prqq-uc.a.run.app") OPAX_MCP_URL = os.environ.get("MCP_SERVER_URL", "https://opax-mcp-357036551735.us-central1.run.app")
# --- BigQuery MCP (Google managed, OAuth) --- # --- BigQuery MCP (Google managed, OAuth) ---
BIGQUERY_MCP_URL = f"https://bigquery.googleapis.com/mcp/projects/{PROJECT_ID}" BIGQUERY_MCP_URL = f"https://bigquery.googleapis.com/mcp/projects/{PROJECT_ID}"

View File

@ -1,234 +1,107 @@
""" """Bakoverkompatibel MCP Streamable HTTP-klient for opax-mcp."""
opax_mcp_client.py REST-klient for opax-mcp Cloud Run service. import json
Bruk denne for å kalle alle 25 tools frå Jason/Emma eller anna Python-kode.
Auth-lag:
1. Cloud Run IAM : Authorization: Bearer <identity-token> (automatisk)
2. Tool-level : X-MCP-Secret: <mcp-server-key> (frå Secret Manager)
URL: https://opax-mcp-zjbqp3prqq-uc.a.run.app
"""
import os import os
import httpx import httpx
import google.auth
import google.auth.transport.requests
OPAX_MCP_URL = os.environ.get("MCP_SERVER_URL", "https://opax-mcp-zjbqp3prqq-uc.a.run.app") OPAX_MCP_URL = os.environ.get(
"MCP_SERVER_URL",
"https://opax-mcp-357036551735.us-central1.run.app",
).rstrip("/")
MCP_SECRET = os.environ.get("MCP_SECRET", "") MCP_SECRET = os.environ.get("MCP_SECRET", "")
def _headers(session_id=None):
def _identity_token() -> str: h = {
"""Hent Cloud Run identity token (GCE metadata) eller ADC access token (lokal dev).""" "Authorization": f"Bearer {MCP_SECRET}",
metadata_url = (
"http://metadata.google.internal/computeMetadata/v1/instance"
f"/service-accounts/default/identity?audience={OPAX_MCP_URL}&format=full"
)
try:
resp = httpx.get(metadata_url, headers={"Metadata-Flavor": "Google"}, timeout=5)
if resp.status_code == 200 and resp.text.strip():
return resp.text.strip()
except Exception:
pass
credentials, _ = google.auth.default()
credentials.refresh(google.auth.transport.requests.Request())
return credentials.token
def _headers() -> dict:
return {
"Authorization": f"Bearer {_identity_token()}",
"X-MCP-Secret": MCP_SECRET,
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2025-06-18",
} }
if session_id:
h["Mcp-Session-Id"] = session_id
return h
def _decode(response):
raw = response.text
rows = [line[5:].strip() for line in raw.splitlines() if line.startswith("data:")]
return json.loads(rows[-1] if rows else raw)
def _post(payload, session_id=None):
with httpx.Client(timeout=60) as client:
r = client.post(OPAX_MCP_URL, headers=_headers(session_id), json=payload)
r.raise_for_status()
return _decode(r), r.headers.get("Mcp-Session-Id")
def _session():
_, sid = _post({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {"name": "osvxcc", "version": "1.0"},
},
})
_post({"jsonrpc": "2.0", "method": "notifications/initialized"}, sid)
return sid
def call_tool(tool: str, params: dict = None) -> dict: def call_tool(tool: str, params: dict = None) -> dict:
"""Kall eit opax-mcp tool. Returnerer {tool, result}.""" sid = _session()
resp = httpx.post( reply, _ = _post({
f"{OPAX_MCP_URL}/tools/call", "jsonrpc": "2.0",
headers=_headers(), "id": 3,
json={"tool": tool, "params": params or {}}, "method": "tools/call",
timeout=30, "params": {"name": tool, "arguments": params or {}},
) }, sid)
resp.raise_for_status() return reply.get("result", reply)
return resp.json()
def list_tools() -> list: def list_tools() -> list:
"""List alle tilgjengelige tools i opax-mcp.""" sid = _session()
resp = httpx.get(f"{OPAX_MCP_URL}/tools", headers=_headers(), timeout=10) reply, _ = _post({
resp.raise_for_status() "jsonrpc": "2.0",
return resp.json().get("tools", []) "id": 2,
"method": "tools/list",
"params": {},
}, sid)
return reply.get("result", {}).get("tools", [])
def get_billing_summary() -> dict: return call_tool("get_billing_summary")
# --------------------------------------------------------------------------- def get_billing_forecast() -> dict: return call_tool("get_billing_forecast")
# Billing def get_billing_credits() -> dict: return call_tool("get_billing_credits")
# --------------------------------------------------------------------------- def get_billing_anomalies() -> dict: return call_tool("get_billing_anomalies")
def get_billing_history() -> dict: return call_tool("get_billing_history")
def get_billing_summary() -> dict: def get_billing_budget() -> dict: return call_tool("get_billing_budget")
"""Hent billing-oversikt for OSVauco (GCP-kostnader, token-forbruk).""" def set_billing_budget(amount: float, currency: str = "USD") -> dict: return call_tool("set_billing_budget", {"amount": amount, "currency": currency})
return call_tool("get_billing_summary") def create_invite(email: str, name: str = "", tier: str = "starter") -> dict: return call_tool("create_invite", {"email": email, "name": name, "tier": tier})
def list_customers() -> dict: return call_tool("list_customers")
def get_billing_forecast() -> dict: def send_webhook(message: str, url: str = "") -> dict: return call_tool("send_webhook", {"message": message, "url": url})
"""Hent token-estimat og kostnadsframskriving.""" def send_email(to: str, subject: str, body: str) -> dict: return call_tool("send_email", {"to": to, "subject": subject, "body": body})
return call_tool("get_billing_forecast") def send_sms(to: str, message: str) -> dict: return call_tool("send_sms", {"to": to, "message": message})
def get_notify_channels() -> dict: return call_tool("get_notify_channels")
def get_billing_credits() -> dict: def run_jason(prompt: str) -> dict: return call_tool("run_jason", {"prompt": prompt})
"""Hent gjenværende GCP-kreditter.""" def run_emma(prompt: str) -> dict: return call_tool("run_emma", {"prompt": prompt})
return call_tool("get_billing_credits") def get_health() -> dict: return call_tool("get_health")
def get_build_status() -> dict: return call_tool("get_build_status")
def get_billing_anomalies() -> dict: def get_state() -> dict: return call_tool("get_state")
"""Sjekk for kostnadanomalier og uventede spiker.""" def get_telemetry() -> dict: return call_tool("get_telemetry")
return call_tool("get_billing_anomalies") def run_terminal(command: str) -> dict: return call_tool("run_terminal", {"command": command})
def list_commits(repo: str = "", limit: int = 10) -> dict: return call_tool("list_commits", {"repo": repo, "limit": limit})
def get_billing_history() -> dict: def get_file(path: str, repo: str = "") -> dict: return call_tool("get_file", {"path": path, "repo": repo})
"""Hent historisk kostnadsdata.""" def list_open_issues(repo: str = "") -> dict: return call_tool("list_open_issues", {"repo": repo})
return call_tool("get_billing_history") def create_github_issue(title: str, body: str = "", repo: str = "") -> dict: return call_tool("create_github_issue", {"title": title, "body": body, "repo": repo})
def push_file(path: str, content: str, message: str = "", sha: str = "", repo: str = "") -> dict: return call_tool("push_file", {"path": path, "content": content, "message": message, "sha": sha, "repo": repo})
def get_billing_budget() -> dict:
"""Hent gjeldande budsjett for OSVauco."""
return call_tool("get_billing_budget")
def set_billing_budget(amount: float, currency: str = "USD") -> dict:
"""Sett nytt budsjett. amount er beløp i angitt valuta."""
return call_tool("set_billing_budget", {"amount": amount, "currency": currency})
# ---------------------------------------------------------------------------
# Onboarding
# ---------------------------------------------------------------------------
def create_invite(email: str, name: str = "", tier: str = "starter") -> dict:
"""Opprett invite for ny bruker. tier: starter | pro | enterprise."""
return call_tool("create_invite", {"email": email, "name": name, "tier": tier})
def list_customers() -> dict:
"""List alle onboardede kunder."""
return call_tool("list_customers")
# ---------------------------------------------------------------------------
# Notify
# ---------------------------------------------------------------------------
def send_webhook(message: str, url: str = "") -> dict:
"""Send webhook-varsling. url er valgfri override."""
return call_tool("send_webhook", {"message": message, "url": url})
def send_email(to: str, subject: str, body: str) -> dict:
"""Send e-post via OPAX notify-modul."""
return call_tool("send_email", {"to": to, "subject": subject, "body": body})
def send_sms(to: str, message: str) -> dict:
"""Send SMS via OPAX notify-modul."""
return call_tool("send_sms", {"to": to, "message": message})
def get_notify_channels() -> dict:
"""List konfigurerte varslingskanalar."""
return call_tool("get_notify_channels")
# ---------------------------------------------------------------------------
# Agents
# ---------------------------------------------------------------------------
def run_jason(prompt: str) -> dict:
"""Kall Jason-agenten med ein prompt via OPAX."""
return call_tool("run_jason", {"prompt": prompt})
def run_emma(prompt: str) -> dict:
"""Kall Emma-agenten med ein prompt via OPAX."""
return call_tool("run_emma", {"prompt": prompt})
# ---------------------------------------------------------------------------
# Platform
# ---------------------------------------------------------------------------
def get_health() -> dict:
"""Sjekk helsestatus for OPAX-plattformen."""
return call_tool("get_health")
def get_build_status() -> dict:
"""Hent status på siste Cloud Build-kjøring."""
return call_tool("get_build_status")
def get_state() -> dict:
"""Hent gjeldande systemtilstand for OSVauco."""
return call_tool("get_state")
def get_telemetry() -> dict:
"""Hent token-forbruk per modul."""
return call_tool("get_telemetry")
def run_terminal(command: str) -> dict:
"""Kjør kommando i OPAX terminal-exec. Krever HITL-godkjenning."""
return call_tool("run_terminal", {"command": command})
# ---------------------------------------------------------------------------
# GitHub
# ---------------------------------------------------------------------------
def list_commits(repo: str = "", limit: int = 10) -> dict:
"""List siste commits i repoet."""
return call_tool("list_commits", {"repo": repo, "limit": limit})
def get_file(path: str, repo: str = "") -> dict:
"""Hent innhald i ein fil frå GitHub."""
return call_tool("get_file", {"path": path, "repo": repo})
def list_open_issues(repo: str = "") -> dict:
"""List alle opne issues i repoet."""
return call_tool("list_open_issues", {"repo": repo})
def create_github_issue(title: str, body: str = "", repo: str = "") -> dict:
"""Opprett nytt GitHub issue."""
return call_tool("create_github_issue", {"title": title, "body": body, "repo": repo})
def push_file(path: str, content: str, message: str = "", sha: str = "", repo: str = "") -> dict:
"""Push/oppdater ein fil i GitHub. sha krevst ved oppdatering av eksisterande fil."""
return call_tool("push_file", {"path": path, "content": content, "message": message, "sha": sha, "repo": repo})
# ---------------------------------------------------------------------------
# ADK FunctionTools — for bruk i agent.py
# ---------------------------------------------------------------------------
def get_all_function_tools() -> list: def get_all_function_tools() -> list:
"""Returner alle 25 tools som ADK FunctionTool-liste for Jason/Emma.""" try:
from google.adk.tools import FunctionTool from google.adk.tools import FunctionTool
return [ return [FunctionTool(func=f) for f in [
# Billing get_billing_summary, get_billing_forecast, get_billing_credits,
FunctionTool(func=get_billing_summary), get_billing_anomalies, get_billing_history, get_billing_budget,
FunctionTool(func=get_billing_forecast), set_billing_budget, create_invite, list_customers, send_webhook,
FunctionTool(func=get_billing_credits), send_email, send_sms, get_notify_channels, run_jason, run_emma,
FunctionTool(func=get_billing_anomalies), get_health, get_build_status, get_state, get_telemetry,
FunctionTool(func=get_billing_history), run_terminal, list_commits, get_file, list_open_issues,
FunctionTool(func=get_billing_budget), create_github_issue, push_file,
FunctionTool(func=set_billing_budget), ]]
# Onboarding except ImportError:
FunctionTool(func=create_invite), return []
FunctionTool(func=list_customers),
# Notify
FunctionTool(func=send_webhook),
FunctionTool(func=send_email),
FunctionTool(func=send_sms),
FunctionTool(func=get_notify_channels),
# Agents
FunctionTool(func=run_jason),
FunctionTool(func=run_emma),
# Platform
FunctionTool(func=get_health),
FunctionTool(func=get_build_status),
FunctionTool(func=get_state),
FunctionTool(func=get_telemetry),
FunctionTool(func=run_terminal),
# GitHub
FunctionTool(func=list_commits),
FunctionTool(func=get_file),
FunctionTool(func=list_open_issues),
FunctionTool(func=create_github_issue),
FunctionTool(func=push_file),
]