Files
leocrm/app/plugins/builtins/mcp_client/routes.py
T
Agent Zero e17b9c9e56
Check Cross-Plugin Imports / check (push) Has been cancelled
Phase 2: Visibility Filter & Owner ID
- Add OwnedMixin to 15 models (contact_folder, user_preference, workspace,
  mcp_server_config, agent_definition, automation_definition, report_template,
  report_instance, entity_link, comm_conversation, proactive_suggestion,
  ai_agent, ai_chat_session, tag, share_link)
- Migration 0102: Add owner_id column to 15 tables with backfill from user_id
- Fix EntityPermission Registry: remove notification, add entity_attachment,
  entity_history, subtask, calendar, folder; fix wrong class names
  (DmsFile→File, CalendarEvent→CalendarEntry, Mailbox→MailAccount)
- Add apply_visibility_filter to list endpoints in tags, tasks, mcp_client,
  automation, report_generator, ai_assistant routes
- Add owner_id to create handlers for all new OwnedMixin models
- Patch tasks/services.py and automation/services.py list methods with
  user_id and is_system_admin parameters
2026-08-04 00:03:29 +02:00

217 lines
8.0 KiB
Python

"""MCP Client plugin routes — CRUD for server configs, tool listing, execution."""
from __future__ import annotations
import logging
import uuid
from datetime import datetime
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
from app.core.visibility import apply_visibility_filter
from app.deps import get_current_user, require_permission
from app.plugins.builtins.mcp_client.client import McpClient
from app.plugins.builtins.mcp_client.models import McpServerConfig as McpServerConfigModel
from app.plugins.builtins.mcp_client.schemas import (
McpServerConfigCreate,
McpServerConfigResponse,
McpServerConfigUpdate,
McpServerExecuteRequest,
McpServerExecuteResponse,
McpServerToolsResponse,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/mcp-client", tags=["mcp-client"])
def _config_to_response(cfg: McpServerConfigModel) -> McpServerConfigResponse:
return McpServerConfigResponse(
id=str(cfg.id),
name=cfg.name,
url=cfg.url,
api_token=cfg.api_token,
enabled=cfg.enabled,
description=cfg.description,
last_connected_at=cfg.last_connected_at,
created_by=str(cfg.created_by) if cfg.created_by else None,
created_at=cfg.created_at,
updated_at=cfg.updated_at,
)
@router.get("/servers", response_model=list[McpServerConfigResponse])
async def list_mcp_servers(
db: AsyncSession = Depends(get_db),
current_user: dict[str, Any] = Depends(require_permission("mcp-client:read")),
) -> list[McpServerConfigResponse]:
"""List all configured MCP servers for the current tenant."""
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
stmt = select(McpServerConfigModel).where(McpServerConfigModel.tenant_id == tenant_id)
stmt = await apply_visibility_filter(
db, stmt, "mcp_server_config", McpServerConfigModel, user_id, tenant_id, is_system_admin
)
result = await db.execute(stmt)
configs = result.scalars().all()
return [_config_to_response(c) for c in configs]
@router.post("/servers", response_model=McpServerConfigResponse, status_code=201)
async def create_mcp_server(
body: McpServerConfigCreate,
db: AsyncSession = Depends(get_db),
current_user: dict[str, Any] = Depends(require_permission("mcp-client:write")),
) -> McpServerConfigResponse:
"""Create a new MCP server configuration."""
cfg = McpServerConfigModel(
tenant_id=uuid.UUID(current_user["tenant_id"]),
name=body.name,
url=body.url,
api_token=body.api_token,
enabled=body.enabled,
description=body.description,
created_by=uuid.UUID(current_user["user_id"]),
owner_id=uuid.UUID(current_user["user_id"]),
)
db.add(cfg)
await db.commit()
await db.refresh(cfg)
return _config_to_response(cfg)
@router.patch("/servers/{server_id}", response_model=McpServerConfigResponse)
async def update_mcp_server(
server_id: str,
body: McpServerConfigUpdate,
db: AsyncSession = Depends(get_db),
current_user: dict[str, Any] = Depends(require_permission("mcp-client:write")),
) -> McpServerConfigResponse:
"""Update an existing MCP server configuration."""
try:
sid = uuid.UUID(server_id)
except (ValueError, TypeError):
raise HTTPException(400, detail={"detail": "Invalid server_id", "code": "invalid_id"})
stmt = select(McpServerConfigModel).where(
McpServerConfigModel.id == sid,
McpServerConfigModel.tenant_id == uuid.UUID(current_user["tenant_id"]),
)
result = await db.execute(stmt)
cfg = result.scalar_one_or_none()
if not cfg:
raise HTTPException(404, detail={"detail": "MCP server not found", "code": "not_found"})
update_data = body.model_dump(exclude_unset=True)
for key, val in update_data.items():
setattr(cfg, key, val)
await db.commit()
await db.refresh(cfg)
return _config_to_response(cfg)
@router.delete("/servers/{server_id}", status_code=204)
async def delete_mcp_server(
server_id: str,
db: AsyncSession = Depends(get_db),
current_user: dict[str, Any] = Depends(require_permission("mcp-client:write")),
):
"""Delete an MCP server configuration."""
try:
sid = uuid.UUID(server_id)
except (ValueError, TypeError):
raise HTTPException(400, detail={"detail": "Invalid server_id", "code": "invalid_id"})
stmt = select(McpServerConfigModel).where(
McpServerConfigModel.id == sid,
McpServerConfigModel.tenant_id == uuid.UUID(current_user["tenant_id"]),
)
result = await db.execute(stmt)
cfg = result.scalar_one_or_none()
if not cfg:
raise HTTPException(404, detail={"detail": "MCP server not found", "code": "not_found"})
await db.delete(cfg)
await db.commit()
@router.get("/servers/{server_id}/tools", response_model=McpServerToolsResponse)
async def list_server_tools(
server_id: str,
db: AsyncSession = Depends(get_db),
current_user: dict[str, Any] = Depends(require_permission("mcp-client:read")),
) -> McpServerToolsResponse:
"""List tools available on a specific external MCP server."""
try:
sid = uuid.UUID(server_id)
except (ValueError, TypeError):
raise HTTPException(400, detail={"detail": "Invalid server_id", "code": "invalid_id"})
stmt = select(McpServerConfigModel).where(
McpServerConfigModel.id == sid,
McpServerConfigModel.tenant_id == uuid.UUID(current_user["tenant_id"]),
)
result = await db.execute(stmt)
cfg = result.scalar_one_or_none()
if not cfg:
raise HTTPException(404, detail={"detail": "MCP server not found", "code": "not_found"})
if not cfg.enabled:
raise HTTPException(400, detail={"detail": "MCP server is disabled", "code": "server_disabled"})
client = McpClient(base_url=cfg.url, api_token=cfg.api_token)
try:
tools_resp = await client.list_tools()
# Update last_connected_at
await db.execute(
update(McpServerConfigModel).where(McpServerConfigModel.id == sid).values(last_connected_at=datetime.utcnow())
)
await db.commit()
return tools_resp
except Exception as exc:
logger.exception("Failed to list tools from MCP server %s", cfg.name)
raise HTTPException(502, detail={"detail": f"Failed to connect: {exc}", "code": "connection_failed"})
@router.post("/servers/{server_id}/execute", response_model=McpServerExecuteResponse)
async def execute_server_tool(
server_id: str,
body: McpServerExecuteRequest,
db: AsyncSession = Depends(get_db),
current_user: dict[str, Any] = Depends(require_permission("mcp-client:write")),
) -> McpServerExecuteResponse:
"""Execute a tool on a specific external MCP server."""
try:
sid = uuid.UUID(server_id)
except (ValueError, TypeError):
raise HTTPException(400, detail={"detail": "Invalid server_id", "code": "invalid_id"})
stmt = select(McpServerConfigModel).where(
McpServerConfigModel.id == sid,
McpServerConfigModel.tenant_id == uuid.UUID(current_user["tenant_id"]),
)
result = await db.execute(stmt)
cfg = result.scalar_one_or_none()
if not cfg:
raise HTTPException(404, detail={"detail": "MCP server not found", "code": "not_found"})
if not cfg.enabled:
raise HTTPException(400, detail={"detail": "MCP server is disabled", "code": "server_disabled"})
client = McpClient(base_url=cfg.url, api_token=cfg.api_token)
try:
return await client.execute_tool(body.tool_name, body.arguments)
except Exception as exc:
logger.exception("Failed to execute tool on MCP server %s", cfg.name)
return McpServerExecuteResponse(
server_name=cfg.name,
tool=body.tool_name,
success=False,
error=str(exc),
)