You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

303 lines
9.7 KiB

"""MCP 服务注册模块路由。
提供 Model Context Protocol (MCP) 服务的注册、管理、测试和工具发现功能。
支持 HTTP 传输方式的 MCP 服务接入。
"""
import uuid
import httpx
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_db
from models import MCPService, AuditLog
from schemas import MCPServiceCreate, MCPServiceUpdate, MCPServiceOut
from dependencies import get_current_user
router = APIRouter(prefix="/api/mcp", tags=["mcp"])
@router.get("/servers", response_model=list[MCPServiceOut])
async def list_servers(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
"""列出所有已注册的 MCP 服务。
Args:
request: HTTP 请求对象。
db: 异步数据库会话。
user: 当前登录用户信息。
Returns:
list[MCPServiceOut]: MCP 服务列表。
"""
result = await db.execute(
select(MCPService).order_by(MCPService.updated_at.desc())
)
return result.scalars().all()
@router.get("/servers/{server_id}", response_model=MCPServiceOut)
async def get_server(server_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
"""获取指定 MCP 服务的详细信息。
Args:
server_id: MCP 服务唯一标识 ID。
request: HTTP 请求对象。
db: 异步数据库会话。
user: 当前登录用户信息。
Returns:
MCPServiceOut: MCP 服务详细信息。
Raises:
HTTPException: 服务不存在时抛出异常。
"""
result = await db.execute(select(MCPService).where(MCPService.id == server_id))
server = result.scalar_one_or_none()
if not server:
raise HTTPException(404, "MCP服务不存在")
return server
@router.post("/servers", response_model=MCPServiceOut)
async def register_server(
req: MCPServiceCreate,
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
"""注册新的 MCP 服务。
Args:
req: MCP 服务创建请求体。
request: HTTP 请求对象。
db: 异步数据库会话。
user: 当前登录用户信息。
Returns:
MCPServiceOut: 注册后的 MCP 服务响应。
Raises:
HTTPException: 服务名称已存在时抛出异常。
"""
existing = await db.execute(select(MCPService).where(MCPService.name == req.name))
if existing.scalar_one_or_none():
raise HTTPException(400, "服务名称已存在")
server = MCPService(
name=req.name,
transport=req.transport,
url=req.url,
command=req.command,
args=req.args,
env=req.env,
creator_id=uuid.UUID(user["id"]),
)
db.add(server)
# 记录审计日志
audit = AuditLog(
operator_id=uuid.UUID(user["id"]),
action="mcp.register",
resource="mcp_service",
resource_id=req.name,
detail={"name": req.name, "transport": req.transport},
ip_address=request.client.host if request.client else None,
)
db.add(audit)
await db.flush()
return server
@router.put("/servers/{server_id}", response_model=MCPServiceOut)
async def update_server(
server_id: uuid.UUID, req: MCPServiceUpdate,
request: Request, db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
"""更新 MCP 服务的配置信息。
Args:
server_id: MCP 服务唯一标识 ID。
req: MCP 服务更新请求体。
request: HTTP 请求对象。
db: 异步数据库会话。
user: 当前登录用户信息。
Returns:
MCPServiceOut: 更新后的 MCP 服务响应。
Raises:
HTTPException: 服务不存在时抛出异常。
"""
result = await db.execute(select(MCPService).where(MCPService.id == server_id))
server = result.scalar_one_or_none()
if not server:
raise HTTPException(404, "MCP服务不存在")
if req.transport is not None:
server.transport = req.transport
if req.url is not None:
server.url = req.url
if req.command is not None:
server.command = req.command
if req.args is not None:
server.args = req.args
if req.env is not None:
server.env = req.env
# 记录审计日志
audit = AuditLog(
operator_id=uuid.UUID(user["id"]),
action="mcp.update",
resource="mcp_service",
resource_id=str(server_id),
ip_address=request.client.host if request.client else None,
)
db.add(audit)
await db.flush()
return server
@router.delete("/servers/{server_id}")
async def delete_server(
server_id: uuid.UUID, request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
"""注销指定的 MCP 服务。
Args:
server_id: MCP 服务唯一标识 ID。
request: HTTP 请求对象。
db: 异步数据库会话。
user: 当前登录用户信息。
Returns:
dict: 操作结果响应。
Raises:
HTTPException: 服务不存在时抛出异常。
"""
result = await db.execute(select(MCPService).where(MCPService.id == server_id))
server = result.scalar_one_or_none()
if not server:
raise HTTPException(404, "MCP服务不存在")
await db.delete(server)
# 记录审计日志
audit = AuditLog(
operator_id=uuid.UUID(user["id"]),
action="mcp.delete",
resource="mcp_service",
resource_id=str(server_id),
detail={"name": server.name},
ip_address=request.client.host if request.client else None,
)
db.add(audit)
await db.flush()
return {"code": 200, "message": "已注销"}
@router.post("/servers/{server_id}/test")
async def test_connection(
server_id: uuid.UUID, request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
"""测试 MCP 服务的连接状态并发现可用工具。
对于 HTTP 传输的服务,访问 /.well-known/mcp 端点进行连接测试。
Args:
server_id: MCP 服务唯一标识 ID。
request: HTTP 请求对象。
db: 异步数据库会话。
user: 当前登录用户信息。
Returns:
dict: 包含连接测试结果和工具列表的响应数据。
Raises:
HTTPException: 服务不存在时抛出异常。
"""
result = await db.execute(select(MCPService).where(MCPService.id == server_id))
server = result.scalar_one_or_none()
if not server:
raise HTTPException(404, "MCP服务不存在")
test_results = {"connectivity": False, "tools_discovered": 0, "tools": [], "error": None}
if server.transport == "http" and server.url:
try:
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get(server.url.rstrip("/") + "/.well-known/mcp")
if resp.status_code == 200:
test_results["connectivity"] = True # 连接成功
data = resp.json()
tools = data.get("tools", [])
test_results["tools_discovered"] = len(tools)
test_results["tools"] = [{"name": t.get("name", ""), "description": t.get("description", "")} for t in tools]
server.tools = test_results["tools"]
server.status = "connected"
else:
test_results["error"] = f"HTTP {resp.status_code}"
server.status = "error"
except Exception as e:
test_results["error"] = str(e)
server.status = "error"
# 记录审计日志
audit = AuditLog(
operator_id=uuid.UUID(user["id"]),
action="mcp.test",
resource="mcp_service",
resource_id=str(server_id),
detail={"name": server.name, "result": "connected" if test_results["connectivity"] else "failed"},
ip_address=request.client.host if request.client else None,
)
db.add(audit)
await db.flush()
return {"code": 200, "data": test_results}
@router.post("/servers/{server_id}/discover-tools")
async def discover_tools(
server_id: uuid.UUID, request: Request,
db: AsyncSession = Depends(get_db),
):
"""从 MCP 服务中发现并注册可用工具。
对于 HTTP 传输的服务,调用 /tools/list 端点获取工具列表。
Args:
server_id: MCP 服务唯一标识 ID。
request: HTTP 请求对象。
db: 异步数据库会话。
Returns:
dict: 包含发现的工具列表和数量的响应数据。
Raises:
HTTPException: 服务不存在或工具发现失败时抛出异常。
"""
result = await db.execute(select(MCPService).where(MCPService.id == server_id))
server = result.scalar_one_or_none()
if not server:
raise HTTPException(404, "MCP服务不存在")
tools = []
if server.transport == "http" and server.url:
try:
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.post(server.url.rstrip("/") + "/tools/list", json={})
if resp.status_code == 200:
data = resp.json()
tools = data.get("tools", [])
server.tools = [{"name": t.get("name", ""), "description": t.get("description", ""), "inputSchema": t.get("inputSchema", {})} for t in tools]
server.status = "connected"
except Exception as e:
raise HTTPException(500, f"工具发现失败: {str(e)}")
await db.flush()
return {"code": 200, "data": {"tools": server.tools, "count": len(server.tools)}}