feat(mcp): HTTP+Bearer CRA-MCP-Server für den Repo-Scanner + Finding-Adapter

Register-Flow für compliance-scanner-agent (anderes Team, Rust): deren MCP-Client
(McpServerConfig) erwartet Streamable HTTP + Bearer — unser MCP war stdio/ohne Auth.
- server.py auf FastMCP umgestellt: Tools cra_assess_findings + cra_list_requirements,
  Dual-Transport (stdio default; Streamable HTTP wenn MCP_PORT gesetzt), Bearer-Gate
  via CRA_MCP_TOKEN.
- ScannerFinding.from_dict tolerant für ihr Finding-Schema (_id/fingerprint,
  scan_type→category, cvss_score→cvss, file_path→location, severity info→low).
- Eigenständiger docker-compose-Dienst bp-compliance-mcp (Port 8099, pure/kein DB,
  isoliert von der Haupt-API) + Hetzner-amd64-Override.
- Tests: test_cra_scanner_adapter, test_mcp_server (Bearer-Gate + Tool-Registry).

Pull-Flow (wir holen ihre Findings über ihren MCP) + öffentliches nginx-Routing
folgen separat (brauchen ihren Endpoint/Token).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-15 18:30:47 +02:00
parent 7aaa7e083b
commit 414496c31a
6 changed files with 222 additions and 75 deletions
@@ -73,15 +73,23 @@ class ScannerFinding:
@classmethod
def from_dict(cls, d: dict) -> "ScannerFinding":
# Tolerant to the compliance-scanner-agent Finding shape (Rust/Mongo):
# _id/fingerprint, scan_type, cvss_score, file_path, severity incl. "info".
raw_id = d.get("id") or d.get("finding_id") or d.get("_id") or d.get("fingerprint") or ""
if isinstance(raw_id, dict): # Mongo extended JSON {"$oid": "..."}
raw_id = raw_id.get("$oid") or ""
sev = (d.get("severity") or "").lower()
if sev == "info": # scanner has 5 levels; we use 4
sev = "low"
return cls(
id=str(d.get("id") or d.get("finding_id") or ""),
id=str(raw_id),
title=d.get("title", "") or d.get("name", ""),
description=d.get("description", "") or d.get("detail", ""),
category=d.get("category", "") or d.get("type", ""),
category=d.get("category", "") or d.get("type", "") or d.get("scan_type", "") or d.get("scanner", ""),
cwe=str(d.get("cwe", "") or ""),
severity=d.get("severity", "") or "",
cvss=d.get("cvss"),
location=d.get("location", "") or d.get("path", ""),
severity=sev,
cvss=d.get("cvss") if d.get("cvss") is not None else d.get("cvss_score"),
location=d.get("location", "") or d.get("path", "") or d.get("file_path", ""),
safety_impact=bool(d.get("safety_impact", False)),
exploited=bool(d.get("exploited", False)),
)