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.
140 lines
7.2 KiB
Python
140 lines
7.2 KiB
Python
"""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}
|