refactor: decompose monolith — app.py 10,600→143 lines, 30+ modules

Fase 0 complete: extract all business logic, auth, database, and 176
endpoints from monolithic app.py into dedicated modules.

Structure:
- app.py: FastAPI hub (CORS, routers, startup/shutdown)
- models.py: 13 Pydantic request models
- utils.py: shared utilities (embed status, upload validation, process registries)
- config.py: constants, env vars, model catalogs
- auth/: crypto, jwt_auth, oidc, rate_limit
- database/: db(), init_db(), schema DDL
- services/: genai, compliance, chat, terraform, embeddings, cis_reports, mcp
- routes/: 13 APIRouter modules (auth, users, oci_config, oci_explorer,
  genai, mcp, adb, embeddings, reports, chat, terraform, settings, cis_engine)

Also: README updated with OCIR auth instructions for manual docker run.

86 tests passing.
This commit is contained in:
nogueiraguh
2026-04-06 15:20:10 -03:00
parent 426bde563e
commit 1135e9d6a9
36 changed files with 11179 additions and 10514 deletions

139
backend/routes/mcp.py Normal file
View File

@@ -0,0 +1,139 @@
"""MCP server routes — CRUD, toggle, upload, link-adb, discover-tools, update tools."""
import json
import uuid
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Query
from database import db
from auth.jwt_auth import current_user, require, _audit, _config_log, _verify_config_access
from config import MCP_DIR, log
from models import MCPServerReq
from utils import validate_upload
from services.mcp_client import _discover_mcp_tools
router = APIRouter()
@router.post("/api/mcp/servers")
async def register_mcp(req: MCPServerReq, u=Depends(require("admin","user"))):
mid = str(uuid.uuid4())
is_global_val = 1 if (req.is_global and u["role"] == "admin") else 0
with db() as c:
c.execute(
"INSERT INTO mcp_servers (id,user_id,name,description,server_type,command,args,env_vars,url,module_path,tools,linked_adb_id,is_global) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
(mid, u["id"], req.name, req.description, req.server_type,
req.command, json.dumps(req.args) if req.args else None,
json.dumps(req.env_vars) if req.env_vars else None, req.url, req.module_path,
json.dumps(req.tools) if req.tools else None, req.linked_adb_id, is_global_val))
_audit(u["id"], u["username"], "register_mcp", mid, req.name)
_config_log("mcp", mid, req.name, "success", "save", f"MCP registrado: {req.name} ({req.server_type})", u["id"], u["username"])
return {"id": mid, "name": req.name, "server_type": req.server_type}
@router.get("/api/mcp/servers")
async def list_mcp(u=Depends(current_user)):
with db() as c:
if u["role"]=="admin": rows=c.execute("SELECT * FROM mcp_servers ORDER BY created_at DESC").fetchall()
else: rows=c.execute("SELECT * FROM mcp_servers WHERE user_id=? OR is_global=1 ORDER BY created_at DESC",(u["id"],)).fetchall()
res = []
for r in rows:
d = dict(r)
for k in ("args","env_vars","tools"):
if d.get(k):
try: d[k] = json.loads(d[k])
except Exception as e: log.warning(f"Failed to parse MCP server field '{k}': {e}")
res.append(d)
return res
@router.delete("/api/mcp/servers/{mid}")
async def del_mcp(mid: str, u=Depends(require("admin","user"))):
_verify_config_access("mcp", mid, u, require_owner=True)
with db() as c: c.execute("DELETE FROM mcp_servers WHERE id=?", (mid,))
return {"ok": True}
@router.put("/api/mcp/servers/{mid}")
async def update_mcp(mid: str, req: MCPServerReq, u=Depends(require("admin","user"))):
with db() as c:
existing = c.execute("SELECT * FROM mcp_servers WHERE id=?", (mid,)).fetchone()
if not existing: raise HTTPException(404)
if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
is_global_val = 1 if (req.is_global and u["role"] == "admin") else (existing["is_global"] if u["role"] != "admin" else (1 if req.is_global else 0))
with db() as c:
c.execute(
"UPDATE mcp_servers SET name=?,description=?,server_type=?,command=?,args=?,env_vars=?,url=?,tools=?,linked_adb_id=?,is_global=? WHERE id=?",
(req.name, req.description, req.server_type, req.command,
json.dumps(req.args) if req.args else None,
json.dumps(req.env_vars) if req.env_vars else None, req.url,
json.dumps(req.tools) if req.tools else None, req.linked_adb_id, is_global_val, mid))
_audit(u["id"], u["username"], "update_mcp", mid, req.name)
_config_log("mcp", mid, req.name, "success", "save", f"MCP atualizado: {req.name} ({req.server_type})", u["id"], u["username"])
return {"id": mid, "name": req.name, "server_type": req.server_type}
@router.put("/api/mcp/servers/{mid}/toggle")
async def toggle_mcp(mid: str, u=Depends(require("admin","user"))):
_verify_config_access("mcp", mid, u, require_owner=True)
with db() as c:
cur = c.execute("SELECT is_active FROM mcp_servers WHERE id=?",(mid,)).fetchone()
if not cur: raise HTTPException(404)
c.execute("UPDATE mcp_servers SET is_active=? WHERE id=?",(0 if cur["is_active"] else 1, mid))
return {"ok": True, "is_active": not cur["is_active"]}
@router.post("/api/mcp/servers/{mid}/upload")
async def upload_mcp_file(mid: str, file: UploadFile = File(...), u=Depends(require("admin","user"))):
await validate_upload(file, {".py", ".js", ".ts", ".json", ".yaml", ".yml", ".toml", ".sh"})
file.file.seek(0)
sdir = MCP_DIR / mid; sdir.mkdir(parents=True, exist_ok=True)
fp = sdir / file.filename
fp.write_bytes(await file.read())
with db() as c: c.execute("UPDATE mcp_servers SET module_path=? WHERE id=?", (str(fp), mid))
return {"ok": True, "path": str(fp)}
@router.put("/api/mcp/servers/{mid}/link-adb")
async def link_mcp_adb(mid: str, adb_id: str = Query(...), u=Depends(require("admin","user"))):
with db() as c: c.execute("UPDATE mcp_servers SET linked_adb_id=? WHERE id=?", (adb_id, mid))
return {"ok": True, "mcp_id": mid, "linked_adb_id": adb_id}
@router.post("/api/mcp/servers/{mid}/discover-tools")
async def discover_mcp_tools(mid: str, u=Depends(require("admin","user"))):
"""Connect to MCP server and discover available tools."""
with db() as c:
row = c.execute("SELECT * FROM mcp_servers WHERE id=?", (mid,)).fetchone()
if not row: raise HTTPException(404)
mcp_srv = dict(row)
try:
discovered = await _discover_mcp_tools(mcp_srv)
except Exception as e:
raise HTTPException(500, f"Erro ao descobrir tools: {str(e)[:500]}")
# Merge with existing tools (keep manually added ones)
existing = []
if mcp_srv.get("tools"):
try: existing = json.loads(mcp_srv["tools"]) if isinstance(mcp_srv["tools"], str) else mcp_srv["tools"]
except Exception as e: log.warning(f"Failed to parse existing MCP tools JSON: {e}")
existing_names = {t["name"] for t in existing if isinstance(t, dict)}
for dt in discovered:
if dt["name"] not in existing_names:
existing.append(dt)
else:
# Update existing tool with discovered schema
for i, et in enumerate(existing):
if isinstance(et, dict) and et["name"] == dt["name"]:
existing[i] = dt
break
with db() as c:
c.execute("UPDATE mcp_servers SET tools=? WHERE id=?", (json.dumps(existing), mid))
_audit(u["id"], u["username"], "discover_mcp_tools", mid, f"{len(discovered)} tools found")
return {"ok": True, "discovered": len(discovered), "total": len(existing), "tools": existing}
@router.put("/api/mcp/servers/{mid}/tools")
async def update_mcp_tools(mid: str, req: dict, u=Depends(require("admin","user"))):
"""Manually update tools for an MCP server."""
with db() as c:
row = c.execute("SELECT id FROM mcp_servers WHERE id=?", (mid,)).fetchone()
if not row: raise HTTPException(404)
tools = req.get("tools", [])
if not isinstance(tools, list): raise HTTPException(400, "tools must be a list")
for t in tools:
if not isinstance(t, dict) or "name" not in t:
raise HTTPException(400, "Each tool must have a 'name' field")
with db() as c:
c.execute("UPDATE mcp_servers SET tools=? WHERE id=?", (json.dumps(tools), mid))
return {"ok": True, "tools": tools}