From e4fd0e9412607ad7e66ada59140874e987a9f955 Mon Sep 17 00:00:00 2001 From: Chris Christiansen Date: Wed, 2 Sep 2026 12:05:09 +0000 Subject: [PATCH] fix(mcp): enforce app-level auth, fix trigger_build SDK, and scaffold docs --- docs/osvx/mcp/CONNECTOR_REGISTRY.md | 8 ++++ docs/osvx/mcp/DEPLOYMENT_MATRIX.md | 9 ++++ docs/osvx/mcp/README.md | 10 +++++ docs/osvx/mcp/TOOL_CATALOG.md | 50 +++++++++++++++++++++ docs/osvx/mcp/TOOL_POLICY.md | 24 ++++++++++ opax-mcp/requirements.txt | 1 + opax-mcp/server.py | 68 +++++++++++++++++------------ 7 files changed, 142 insertions(+), 28 deletions(-) create mode 100644 docs/osvx/mcp/CONNECTOR_REGISTRY.md create mode 100644 docs/osvx/mcp/DEPLOYMENT_MATRIX.md create mode 100644 docs/osvx/mcp/README.md create mode 100644 docs/osvx/mcp/TOOL_CATALOG.md create mode 100644 docs/osvx/mcp/TOOL_POLICY.md diff --git a/docs/osvx/mcp/CONNECTOR_REGISTRY.md b/docs/osvx/mcp/CONNECTOR_REGISTRY.md new file mode 100644 index 0000000..1302cda --- /dev/null +++ b/docs/osvx/mcp/CONNECTOR_REGISTRY.md @@ -0,0 +1,8 @@ +# OSVx MCP Connector Registry + +This document is the single source of truth for all clients and connectors that interact with the OSVx MCP. + +| Connector Name | Owner | Target Endpoint URL | Authentication Method | Status | +| :--- | :--- | :--- | :--- | :--- | +| Perplexity | OSVx Team | UNKNOWN | UNKNOWN | Misaligned | + diff --git a/docs/osvx/mcp/DEPLOYMENT_MATRIX.md b/docs/osvx/mcp/DEPLOYMENT_MATRIX.md new file mode 100644 index 0000000..8a8f5bc --- /dev/null +++ b/docs/osvx/mcp/DEPLOYMENT_MATRIX.md @@ -0,0 +1,9 @@ +# OSVx MCP Deployment Matrix + +This document tracks which service revisions and container images are deployed to each environment. + +| Environment | Service Name | Cloud Run Revision | Image Digest | Deployed At | Git Commit | +| :--- | :--- | :--- | :--- | :--- | :--- | +| Production | `opax-mcp` | `opax-mcp-00134-jb7` | `sha256:8adbd3f7...` | 2026-09-01 | UNKNOWN | +| Staging | `osvx-mcp-dev`| `osvx-mcp-dev-00001-n24`| `sha256:b5ac84f4...` | 2026-09-01 | UNKNOWN | + diff --git a/docs/osvx/mcp/README.md b/docs/osvx/mcp/README.md new file mode 100644 index 0000000..768d613 --- /dev/null +++ b/docs/osvx/mcp/README.md @@ -0,0 +1,10 @@ +# OSVx Model Context Protocol (MCP) Architecture + +This directory contains the canonical documentation for the OSVx MCP, which governs how AI agents and connectors interact with the OSVx platform. + +## Key Documents + +- [Connector Registry](./CONNECTOR_REGISTRY.md): A list of all clients that connect to the MCP. +- [Tool Catalog](./TOOL_CATALOG.md): The master list of all available tools. +- [Tool Policy](./TOOL_POLICY.md): Defines the security and exposure policies for tools. +- [Deployment Matrix](./DEPLOYMENT_MATRIX.md): Tracks which service versions are deployed to which environments. diff --git a/docs/osvx/mcp/TOOL_CATALOG.md b/docs/osvx/mcp/TOOL_CATALOG.md new file mode 100644 index 0000000..8176f9a --- /dev/null +++ b/docs/osvx/mcp/TOOL_CATALOG.md @@ -0,0 +1,50 @@ +# OSVx MCP Tool Catalog + +This document lists all 32 tools available in the current production deployment of the OSVx MCP. + +## Billing Tools +- `get_billing_summary` +- `get_billing_forecast` +- `get_billing_credits` +- `get_billing_anomalies` +- `get_billing_history` +- `get_billing_budget` +- `get_telemetry` +- `set_billing_budget` + +## Onboarding Tools +- `create_invite` + +## Notification Tools +- `send_webhook` +- `send_email` +- `send_sms` +- `get_notify_channels` + +## AI Agent Tools +- `run_jason` +- `run_emma` +- `list_emma_models` + +## System & Ops Tools +- `get_health` +- `get_build_status` +- `trigger_build` +- `get_state` +- `list_customers` +- `run_terminal` + +## VCS Tools (Gitea) +- `list_commits` +- `get_file` + +## Workspace Tools (Google Workspace) +- `create_email_alias` +- `list_user_aliases` +- `delete_email_alias` +- `send_email_as` +- `get_workspace_user` +- `list_workspace_users` +- `get_emails` +- `list_calendar_events` + diff --git a/docs/osvx/mcp/TOOL_POLICY.md b/docs/osvx/mcp/TOOL_POLICY.md new file mode 100644 index 0000000..3ce81d3 --- /dev/null +++ b/docs/osvx/mcp/TOOL_POLICY.md @@ -0,0 +1,24 @@ +# OSVx MCP Tool Policy + +This document defines the security classification and exposure tiers for all MCP tools. + +## Tier 1: Safe Reads +- **Description:** Public, cacheable, low-risk data. +- **Tools:** `list_emma_models` + +## Tier 2: Scoped Reads +- **Description:** Requires authentication; reads sensitive but non-modifying data. +- **Tools:** `get_billing_history`, `get_file`, `get_emails` + +## Tier 3: Compute Tools +- **Description:** Requires authentication; triggers non-state-changing computations. +- **Tools:** `run_jason`, `run_emma` + +## Tier 4: Confirmed Actions +- **Description:** State-changing actions that should require user confirmation (HITL). +- **Tools:** `create_issue`, `set_billing_budget` + +## Tier 5: Restricted Admin +- **Description:** High-risk administrative tools that must be protected by strict, server-side authorization. +- **Tools:** `run_terminal`, `trigger_build`, `delete_email_alias` + diff --git a/opax-mcp/requirements.txt b/opax-mcp/requirements.txt index c316f0b..e8bc5f9 100644 --- a/opax-mcp/requirements.txt +++ b/opax-mcp/requirements.txt @@ -5,3 +5,4 @@ google-auth>=2.29.0 google-api-python-client>=2.120.0 google-cloud-firestore>=2.16.0 google-cloud-secret-manager>=2.18.0 +google-cloud-build>=2.0.0 diff --git a/opax-mcp/server.py b/opax-mcp/server.py index acd50a1..7fcb7c4 100644 --- a/opax-mcp/server.py +++ b/opax-mcp/server.py @@ -68,10 +68,20 @@ logger.info(f"OLLAMA_BASE_URL: {OLLAMA_BASE_URL}") async def _verify_auth(request: Request) -> None: secret = os.getenv("MCP_SECRET") if not secret: + logger.error("MCP_SECRET is not configured on the server.") raise HTTPException(status_code=500, detail="MCP_SECRET not configured") - auth = request.headers.get("Authorization", "") - if not auth.startswith("Bearer ") or auth[7:] != secret: - raise HTTPException(status_code=401, detail="Invalid or missing token") + + # Perplexity sends 'api-key', others might send 'X-MCP-Secret' or 'Authorization: Bearer ...' + token = request.headers.get("api-key") + if not token: + token = request.headers.get("X-MCP-Secret") + if not token: + auth_header = request.headers.get("Authorization", "") + if auth_header.startswith("Bearer "): + token = auth_header[7:] + + if not token or token != secret: + raise HTTPException(status_code=401, detail="Invalid or missing API key") # --------------------------------------------------------------------------- @@ -228,31 +238,33 @@ async def list_calendar_events(p: dict) -> dict: return {"events": formatted_events} async def trigger_build(p: dict) -> dict: - """Trigger en Cloud Build manuelt ved å sende kildekode fra Gitea.""" - import subprocess - repo = p.get("repo", "OSVauco") + """Triggers a Cloud Build manually by its trigger ID.""" + from google.cloud.devtools import cloudbuild_v1 + + project_id = GOOGLE_CLOUD_PROJECT + trigger_id = p.get("trigger_id") branch = p.get("branch", "main") - config = p.get("config", "cloudbuild.mcp.yaml") - - result = subprocess.run([ - "gcloud", "builds", "submit", - f"/home/chris_christiansen/OSVauco", - f"--config={config}", - f"--project={GOOGLE_CLOUD_PROJECT}" - ], capture_output=True, text=True) + substitutions = p.get("substitutions", {}) - if result.returncode != 0: - error_message = result.stderr.strip() - logger.error(f"Cloud Build trigger failed: {error_message}") - return {"status": "error", "message": error_message} + if not trigger_id: + raise ValueError("Missing required parameter: 'trigger_id'") - build_id = "Not found in output" - for line in result.stdout.split('\n'): - if "ID:" in line: - build_id = line.split("ID:")[1].strip() - break - - return {"status": "triggered", "build_id": build_id} + try: + client = cloudbuild_v1.CloudBuildClient() + source = cloudbuild_v1.RepoSource(branch_name=branch, substitutions=substitutions) + + response = client.run_build_trigger( + project_id=project_id, + trigger_id=trigger_id, + source=source, + ) + + build_id = response.metadata.build.id + logger.info(f"Successfully triggered Cloud Build. Build ID: {build_id}") + return {"status": "success", "build_id": build_id} + except Exception as e: + logger.error(f"Failed to trigger Cloud Build: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Failed to trigger Cloud Build: {e}") # --------------------------------------------------------------------------- # Ollama helpers — direkte mot emma-gpu-vm @@ -463,9 +475,9 @@ def _jsonrpc_err(req_id, code, message): @app.post("/") async def mcp_handler(request: Request): - # ... (beholdt uendret) - _verify_auth(request) - try: body = await request.json() + await _verify_auth(request) + try: + body = await request.json() except Exception: return JSONResponse(_jsonrpc_err(None, -32700, "Parse error"), status_code=400) method, req_id, params = body.get("method", ""), body.get("id"), body.get("params", {}) if method == "initialize":