fix(mcp): enforce app-level auth, fix trigger_build SDK, and scaffold docs

This commit is contained in:
Chris Christiansen 2026-09-02 12:05:09 +00:00
parent 2a6890bb22
commit 4f2cf3e28b
7 changed files with 142 additions and 28 deletions

View File

@ -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 |

View File

@ -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 |

10
docs/osvx/mcp/README.md Normal file
View File

@ -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.

View File

@ -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`

View File

@ -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`

View File

@ -5,3 +5,4 @@ google-auth>=2.29.0
google-api-python-client>=2.120.0 google-api-python-client>=2.120.0
google-cloud-firestore>=2.16.0 google-cloud-firestore>=2.16.0
google-cloud-secret-manager>=2.18.0 google-cloud-secret-manager>=2.18.0
google-cloud-build>=2.0.0

View File

@ -68,10 +68,20 @@ logger.info(f"OLLAMA_BASE_URL: {OLLAMA_BASE_URL}")
async def _verify_auth(request: Request) -> None: async def _verify_auth(request: Request) -> None:
secret = os.getenv("MCP_SECRET") secret = os.getenv("MCP_SECRET")
if not secret: if not secret:
logger.error("MCP_SECRET is not configured on the server.")
raise HTTPException(status_code=500, detail="MCP_SECRET not configured") raise HTTPException(status_code=500, detail="MCP_SECRET not configured")
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer ") or auth[7:] != secret: # Perplexity sends 'api-key', others might send 'X-MCP-Secret' or 'Authorization: Bearer ...'
raise HTTPException(status_code=401, detail="Invalid or missing token") 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} return {"events": formatted_events}
async def trigger_build(p: dict) -> dict: async def trigger_build(p: dict) -> dict:
"""Trigger en Cloud Build manuelt ved å sende kildekode fra Gitea.""" """Triggers a Cloud Build manually by its trigger ID."""
import subprocess from google.cloud.devtools import cloudbuild_v1
repo = p.get("repo", "OSVauco")
project_id = GOOGLE_CLOUD_PROJECT
trigger_id = p.get("trigger_id")
branch = p.get("branch", "main") branch = p.get("branch", "main")
config = p.get("config", "cloudbuild.mcp.yaml") substitutions = p.get("substitutions", {})
result = subprocess.run([
"gcloud", "builds", "submit",
f"/home/chris_christiansen/OSVauco",
f"--config={config}",
f"--project={GOOGLE_CLOUD_PROJECT}"
], capture_output=True, text=True)
if result.returncode != 0: if not trigger_id:
error_message = result.stderr.strip() raise ValueError("Missing required parameter: 'trigger_id'")
logger.error(f"Cloud Build trigger failed: {error_message}")
return {"status": "error", "message": error_message}
build_id = "Not found in output" try:
for line in result.stdout.split('\n'): client = cloudbuild_v1.CloudBuildClient()
if "ID:" in line: source = cloudbuild_v1.RepoSource(branch_name=branch, substitutions=substitutions)
build_id = line.split("ID:")[1].strip()
break response = client.run_build_trigger(
project_id=project_id,
return {"status": "triggered", "build_id": build_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 # Ollama helpers — direkte mot emma-gpu-vm
@ -463,9 +475,9 @@ def _jsonrpc_err(req_id, code, message):
@app.post("/") @app.post("/")
async def mcp_handler(request: Request): async def mcp_handler(request: Request):
# ... (beholdt uendret) await _verify_auth(request)
_verify_auth(request) try:
try: body = await request.json() body = await request.json()
except Exception: return JSONResponse(_jsonrpc_err(None, -32700, "Parse error"), status_code=400) 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", {}) method, req_id, params = body.get("method", ""), body.get("id"), body.get("params", {})
if method == "initialize": if method == "initialize":