Files
leocrm/app/plugins/builtins/mcp_client/routes.py
T

209 lines
7.6 KiB
Python
Raw Normal View History

"""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.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."""
stmt = select(McpServerConfigModel).where(McpServerConfigModel.tenant_id == uuid.UUID(current_user["tenant_id"]))
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"]),
)
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),
)