"""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}