feat: add MCP tool integration

This commit is contained in:
Xin Wang
2026-07-18 00:00:06 +08:00
parent bdf3d3dd9c
commit e39bb48ba8
21 changed files with 1641 additions and 62 deletions

View File

@@ -7,7 +7,8 @@ from db.session import get_session
from fastapi import APIRouter, Depends, HTTPException
from schemas import ToolOut, ToolUpsert
from services.auth import require_admin
from services.masking import mask_secrets, merge_secrets
from services.masking import merge_secrets
from services.tool_resources import tool_to_out
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
@@ -20,21 +21,10 @@ router = APIRouter(
)
def _to_out(tool: Tool) -> ToolOut:
return ToolOut(
id=tool.id,
name=tool.name,
function_name=tool.function_name,
type=tool.type,
description=tool.description,
definition=tool.definition,
secrets=mask_secrets(tool.secrets or {}),
status=tool.status,
updated_at=tool.updated_at.isoformat() if tool.updated_at else None,
)
def _payload(body: ToolUpsert, stored_secrets: dict | None = None) -> dict:
def _payload(
body: ToolUpsert,
stored_secrets: dict | None = None,
) -> dict:
definition = body.definition.model_dump()
secrets = (
merge_secrets(body.secrets, stored_secrets or {})
@@ -45,6 +35,12 @@ def _payload(body: ToolUpsert, stored_secrets: dict | None = None) -> dict:
"name": body.name.strip(),
"function_name": body.function_name,
"type": definition["type"],
"mcp_server_id": body.mcp_server_id if definition["type"] == "mcp" else None,
"remote_tool_name": (
definition["config"]["remote_tool_name"]
if definition["type"] == "mcp"
else None
),
"description": body.description.strip(),
"definition": definition,
"secrets": secrets,
@@ -66,7 +62,7 @@ async def _commit(session: AsyncSession, tool: Tool) -> ToolOut:
await session.rollback()
raise HTTPException(409, "工具函数名已存在") from exc
await session.refresh(tool)
return _to_out(tool)
return tool_to_out(tool)
@router.get("", response_model=list[ToolOut])
@@ -74,11 +70,13 @@ async def list_tools(session: AsyncSession = Depends(get_session)):
rows = (
await session.execute(select(Tool).order_by(Tool.updated_at.desc()))
).scalars().all()
return [_to_out(tool) for tool in rows]
return [tool_to_out(tool) for tool in rows]
@router.post("", response_model=ToolOut)
async def create_tool(body: ToolUpsert, session: AsyncSession = Depends(get_session)):
if body.definition.type == "mcp":
raise HTTPException(400, "MCP 工具请通过 MCP Server 同步创建")
tool = Tool(id=f"tool_{uuid.uuid4().hex[:12]}", **_payload(body))
session.add(tool)
return await _commit(session, tool)
@@ -89,7 +87,7 @@ async def get_tool(tool_id: str, session: AsyncSession = Depends(get_session)):
tool = await session.get(Tool, tool_id)
if not tool:
raise HTTPException(404, "工具不存在")
return _to_out(tool)
return tool_to_out(tool)
@router.put("/{tool_id}", response_model=ToolOut)
@@ -101,6 +99,14 @@ async def update_tool(
tool = await session.get(Tool, tool_id)
if not tool:
raise HTTPException(404, "工具不存在")
if tool.type == "mcp":
if body.definition.type != "mcp":
raise HTTPException(400, "同步生成的 MCP 工具不能修改类型")
if (
body.mcp_server_id != tool.mcp_server_id
or body.definition.config.remote_tool_name != tool.remote_tool_name
):
raise HTTPException(400, "MCP Server 和远端工具名称不能修改")
for key, value in _payload(body, tool.secrets or {}).items():
setattr(tool, key, value)
return await _commit(session, tool)
@@ -111,6 +117,8 @@ async def duplicate_tool(tool_id: str, session: AsyncSession = Depends(get_sessi
source = await session.get(Tool, tool_id)
if not source:
raise HTTPException(404, "工具不存在")
if source.type == "mcp":
raise HTTPException(400, "MCP 工具由 Server 同步维护,不能单独复制")
function_name = _duplicate_function_name(source.function_name)
existing = (
await session.execute(select(Tool).where(Tool.function_name == function_name))
@@ -124,6 +132,8 @@ async def duplicate_tool(tool_id: str, session: AsyncSession = Depends(get_sessi
name=f"{source.name} 副本",
function_name=function_name,
type=source.type,
mcp_server_id=source.mcp_server_id,
remote_tool_name=source.remote_tool_name,
description=source.description,
definition=dict(source.definition or {}),
secrets=dict(source.secrets or {}),