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:
139
backend/routes/mcp.py
Normal file
139
backend/routes/mcp.py
Normal 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}
|
||||
Reference in New Issue
Block a user