diff --git a/agents/core-logic/opax_mcp_client.py b/agents/core-logic/opax_mcp_client.py index c174d99..93515fc 100644 --- a/agents/core-logic/opax_mcp_client.py +++ b/agents/core-logic/opax_mcp_client.py @@ -6,7 +6,7 @@ Auth-lag: 1. Cloud Run IAM : Authorization: Bearer (automatisk) 2. Tool-level : X-MCP-Secret: (frå env) -URL: https://opax-mcp-zjbqp3prqq-uc.a.run.app +URL: https://opax-mcp-357036551735.us-central1.run.app """ import os @@ -14,7 +14,7 @@ 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") MCP_SECRET = os.environ.get("MCP_SECRET", "") diff --git a/agents/tools/mcp_tools.py b/agents/tools/mcp_tools.py index 1050eef..83548b2 100644 --- a/agents/tools/mcp_tools.py +++ b/agents/tools/mcp_tools.py @@ -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") -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_URL = f"https://bigquery.googleapis.com/mcp/projects/{PROJECT_ID}" diff --git a/agents/tools/opax_mcp_client.py b/agents/tools/opax_mcp_client.py index 0342921..1abe051 100644 --- a/agents/tools/opax_mcp_client.py +++ b/agents/tools/opax_mcp_client.py @@ -1,234 +1,107 @@ -""" -opax_mcp_client.py — REST-klient for opax-mcp Cloud Run service. -Bruk denne for å kalle alle 25 tools frå Jason/Emma eller anna Python-kode. - -Auth-lag: - 1. Cloud Run IAM : Authorization: Bearer (automatisk) - 2. Tool-level : X-MCP-Secret: (frå Secret Manager) - -URL: https://opax-mcp-zjbqp3prqq-uc.a.run.app -""" - +"""Bakoverkompatibel MCP Streamable HTTP-klient for opax-mcp.""" +import json import os 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", "") - -def _identity_token() -> str: - """Hent Cloud Run identity token (GCE metadata) eller ADC access token (lokal dev).""" - 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, +def _headers(session_id=None): + h = { + "Authorization": f"Bearer {MCP_SECRET}", "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: - """Kall eit opax-mcp tool. Returnerer {tool, result}.""" - resp = httpx.post( - f"{OPAX_MCP_URL}/tools/call", - headers=_headers(), - json={"tool": tool, "params": params or {}}, - timeout=30, - ) - resp.raise_for_status() - return resp.json() - + sid = _session() + reply, _ = _post({ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": {"name": tool, "arguments": params or {}}, + }, sid) + return reply.get("result", reply) def list_tools() -> list: - """List alle tilgjengelige tools i opax-mcp.""" - resp = httpx.get(f"{OPAX_MCP_URL}/tools", headers=_headers(), timeout=10) - resp.raise_for_status() - return resp.json().get("tools", []) + sid = _session() + reply, _ = _post({ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list", + "params": {}, + }, sid) + return reply.get("result", {}).get("tools", []) - -# --------------------------------------------------------------------------- -# Billing -# --------------------------------------------------------------------------- - -def get_billing_summary() -> dict: - """Hent billing-oversikt for OSVauco (GCP-kostnader, token-forbruk).""" - return call_tool("get_billing_summary") - -def get_billing_forecast() -> dict: - """Hent token-estimat og kostnadsframskriving.""" - return call_tool("get_billing_forecast") - -def get_billing_credits() -> dict: - """Hent gjenværende GCP-kreditter.""" - return call_tool("get_billing_credits") - -def get_billing_anomalies() -> dict: - """Sjekk for kostnadanomalier og uventede spiker.""" - return call_tool("get_billing_anomalies") - -def get_billing_history() -> dict: - """Hent historisk kostnadsdata.""" - return call_tool("get_billing_history") - -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_billing_summary() -> dict: return call_tool("get_billing_summary") +def get_billing_forecast() -> dict: return call_tool("get_billing_forecast") +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_budget() -> dict: return call_tool("get_billing_budget") +def set_billing_budget(amount: float, currency: str = "USD") -> dict: return call_tool("set_billing_budget", {"amount": amount, "currency": currency}) +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 send_webhook(message: str, url: str = "") -> dict: return call_tool("send_webhook", {"message": message, "url": url}) +def send_email(to: str, subject: str, body: str) -> dict: return call_tool("send_email", {"to": to, "subject": subject, "body": body}) +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 run_jason(prompt: str) -> dict: return call_tool("run_jason", {"prompt": prompt}) +def run_emma(prompt: str) -> dict: return call_tool("run_emma", {"prompt": prompt}) +def get_health() -> dict: return call_tool("get_health") +def get_build_status() -> dict: return call_tool("get_build_status") +def get_state() -> dict: return call_tool("get_state") +def get_telemetry() -> dict: return call_tool("get_telemetry") +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_file(path: str, repo: str = "") -> dict: return call_tool("get_file", {"path": path, "repo": repo}) +def list_open_issues(repo: str = "") -> dict: return call_tool("list_open_issues", {"repo": repo}) +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_all_function_tools() -> list: - """Returner alle 25 tools som ADK FunctionTool-liste for Jason/Emma.""" - from google.adk.tools import FunctionTool - return [ - # Billing - FunctionTool(func=get_billing_summary), - FunctionTool(func=get_billing_forecast), - FunctionTool(func=get_billing_credits), - FunctionTool(func=get_billing_anomalies), - FunctionTool(func=get_billing_history), - FunctionTool(func=get_billing_budget), - FunctionTool(func=set_billing_budget), - # Onboarding - FunctionTool(func=create_invite), - 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), - ] + try: + from google.adk.tools import FunctionTool + return [FunctionTool(func=f) for f in [ + get_billing_summary, get_billing_forecast, get_billing_credits, + get_billing_anomalies, get_billing_history, get_billing_budget, + set_billing_budget, create_invite, list_customers, send_webhook, + send_email, send_sms, get_notify_channels, run_jason, run_emma, + get_health, get_build_status, get_state, get_telemetry, + run_terminal, list_commits, get_file, list_open_issues, + create_github_issue, push_file, + ]] + except ImportError: + return []