862 lines
29 KiB
Python
862 lines
29 KiB
Python
"""MCP插件管理API"""
|
|
from fastapi import APIRouter, HTTPException, Depends, Query, Request
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from typing import List, Optional
|
|
from datetime import datetime
|
|
|
|
from app.database import get_db
|
|
from app.models.mcp_plugin import MCPPlugin
|
|
from app.schemas.mcp_plugin import (
|
|
MCPPluginCreate,
|
|
MCPPluginSimpleCreate,
|
|
MCPPluginUpdate,
|
|
MCPPluginResponse,
|
|
MCPToolCall,
|
|
MCPTestResult
|
|
)
|
|
import json
|
|
from app.user_manager import User
|
|
from app.mcp.registry import mcp_registry
|
|
from app.logger import get_logger
|
|
from app.services.ai_service import create_user_ai_service
|
|
from app.models.settings import Settings as UserSettings
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
router = APIRouter(prefix="/mcp/plugins", tags=["MCP插件管理"])
|
|
|
|
|
|
def require_login(request: Request) -> User:
|
|
"""依赖:要求用户已登录"""
|
|
if not hasattr(request.state, "user") or not request.state.user:
|
|
raise HTTPException(status_code=401, detail="需要登录")
|
|
return request.state.user
|
|
|
|
|
|
@router.get("", response_model=List[MCPPluginResponse])
|
|
async def list_plugins(
|
|
enabled_only: bool = Query(False, description="只返回启用的插件"),
|
|
category: Optional[str] = Query(None, description="按分类筛选"),
|
|
user: User = Depends(require_login),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
获取用户的所有MCP插件
|
|
"""
|
|
query = select(MCPPlugin).where(MCPPlugin.user_id == user.user_id)
|
|
|
|
if enabled_only:
|
|
query = query.where(MCPPlugin.enabled == True)
|
|
|
|
if category:
|
|
query = query.where(MCPPlugin.category == category)
|
|
|
|
query = query.order_by(MCPPlugin.sort_order, MCPPlugin.created_at)
|
|
|
|
result = await db.execute(query)
|
|
plugins = result.scalars().all()
|
|
|
|
logger.info(f"用户 {user.user_id} 查询插件列表,共 {len(plugins)} 个")
|
|
return plugins
|
|
|
|
|
|
@router.post("", response_model=MCPPluginResponse)
|
|
async def create_plugin(
|
|
data: MCPPluginCreate,
|
|
user: User = Depends(require_login),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
创建新的MCP插件
|
|
"""
|
|
# 检查插件名是否已存在
|
|
result = await db.execute(
|
|
select(MCPPlugin).where(
|
|
MCPPlugin.user_id == user.user_id,
|
|
MCPPlugin.plugin_name == data.plugin_name
|
|
)
|
|
)
|
|
existing = result.scalar_one_or_none()
|
|
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail=f"插件名已存在: {data.plugin_name}")
|
|
|
|
# 创建插件数据
|
|
plugin_data = data.model_dump()
|
|
|
|
# 如果没有提供display_name,使用plugin_name作为默认值
|
|
if not plugin_data.get("display_name"):
|
|
plugin_data["display_name"] = plugin_data["plugin_name"]
|
|
|
|
# 创建插件
|
|
plugin = MCPPlugin(
|
|
user_id=user.user_id,
|
|
**plugin_data
|
|
)
|
|
|
|
db.add(plugin)
|
|
await db.commit()
|
|
await db.refresh(plugin)
|
|
|
|
# 如果启用,加载到注册表
|
|
if plugin.enabled:
|
|
success = await mcp_registry.load_plugin(plugin)
|
|
if success:
|
|
plugin.status = "active"
|
|
else:
|
|
plugin.status = "error"
|
|
plugin.last_error = "加载失败"
|
|
await db.commit()
|
|
await db.refresh(plugin)
|
|
|
|
logger.info(f"用户 {user.user_id} 创建插件: {plugin.plugin_name}")
|
|
return plugin
|
|
|
|
|
|
@router.post("/simple", response_model=MCPPluginResponse)
|
|
async def create_plugin_simple(
|
|
data: MCPPluginSimpleCreate,
|
|
user: User = Depends(require_login),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
通过标准MCP配置JSON创建或更新插件(简化版)
|
|
|
|
接受格式:
|
|
{
|
|
"config_json": '{"mcpServers": {"exa": {"type": "http", "url": "...", "headers": {}}}}',
|
|
"category": "search"
|
|
}
|
|
|
|
自动从mcpServers中提取插件名称(取第一个键)
|
|
如果插件已存在,则更新;否则创建新插件
|
|
"""
|
|
try:
|
|
# 解析配置JSON
|
|
config = json.loads(data.config_json)
|
|
|
|
# 验证格式
|
|
if "mcpServers" not in config:
|
|
raise HTTPException(status_code=400, detail="配置JSON必须包含mcpServers字段")
|
|
|
|
servers = config["mcpServers"]
|
|
if not servers or len(servers) == 0:
|
|
raise HTTPException(status_code=400, detail="mcpServers不能为空")
|
|
|
|
# 自动提取第一个插件名称
|
|
plugin_name = list(servers.keys())[0]
|
|
server_config = servers[plugin_name]
|
|
|
|
logger.info(f"从配置中提取插件名称: {plugin_name}")
|
|
|
|
# 提取配置
|
|
server_type = server_config.get("type", "http")
|
|
|
|
if server_type not in ["http", "stdio"]:
|
|
raise HTTPException(status_code=400, detail=f"不支持的服务器类型: {server_type}")
|
|
|
|
# 检查插件名是否已存在
|
|
result = await db.execute(
|
|
select(MCPPlugin).where(
|
|
MCPPlugin.user_id == user.user_id,
|
|
MCPPlugin.plugin_name == plugin_name
|
|
)
|
|
)
|
|
existing = result.scalar_one_or_none()
|
|
|
|
# 构建插件数据
|
|
plugin_data = {
|
|
"plugin_name": plugin_name,
|
|
"display_name": plugin_name,
|
|
"plugin_type": server_type,
|
|
"enabled": data.enabled,
|
|
"category": data.category,
|
|
"sort_order": 0
|
|
}
|
|
|
|
if server_type == "http":
|
|
plugin_data["server_url"] = server_config.get("url")
|
|
plugin_data["headers"] = server_config.get("headers", {})
|
|
|
|
if not plugin_data["server_url"]:
|
|
raise HTTPException(status_code=400, detail="HTTP类型插件必须提供url字段")
|
|
|
|
elif server_type == "stdio":
|
|
plugin_data["command"] = server_config.get("command")
|
|
plugin_data["args"] = server_config.get("args", [])
|
|
plugin_data["env"] = server_config.get("env", {})
|
|
|
|
if not plugin_data["command"]:
|
|
raise HTTPException(status_code=400, detail="Stdio类型插件必须提供command字段")
|
|
|
|
if existing:
|
|
# 更新现有插件
|
|
logger.info(f"插件 {plugin_name} 已存在,执行更新操作")
|
|
|
|
# 先卸载旧插件
|
|
if existing.enabled:
|
|
await mcp_registry.unload_plugin(user.user_id, existing.plugin_name)
|
|
|
|
# 更新字段
|
|
for key, value in plugin_data.items():
|
|
setattr(existing, key, value)
|
|
|
|
plugin = existing
|
|
await db.commit()
|
|
await db.refresh(plugin)
|
|
|
|
# 如果启用,重新加载
|
|
if plugin.enabled:
|
|
success = await mcp_registry.load_plugin(plugin)
|
|
if success:
|
|
plugin.status = "active"
|
|
plugin.last_error = None
|
|
else:
|
|
plugin.status = "error"
|
|
plugin.last_error = "加载失败"
|
|
await db.commit()
|
|
await db.refresh(plugin)
|
|
|
|
logger.info(f"用户 {user.user_id} 更新插件: {plugin_name}")
|
|
else:
|
|
# 创建新插件
|
|
plugin = MCPPlugin(
|
|
user_id=user.user_id,
|
|
**plugin_data
|
|
)
|
|
|
|
db.add(plugin)
|
|
await db.commit()
|
|
await db.refresh(plugin)
|
|
|
|
# 如果启用,加载到注册表
|
|
if plugin.enabled:
|
|
success = await mcp_registry.load_plugin(plugin)
|
|
if success:
|
|
plugin.status = "active"
|
|
else:
|
|
plugin.status = "error"
|
|
plugin.last_error = "加载失败"
|
|
await db.commit()
|
|
await db.refresh(plugin)
|
|
|
|
logger.info(f"用户 {user.user_id} 通过简化配置创建插件: {plugin_name}")
|
|
|
|
return plugin
|
|
|
|
except json.JSONDecodeError as e:
|
|
raise HTTPException(status_code=400, detail=f"配置JSON格式错误: {str(e)}")
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"创建插件失败: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=f"创建插件失败: {str(e)}")
|
|
|
|
|
|
@router.get("/{plugin_id}", response_model=MCPPluginResponse)
|
|
async def get_plugin(
|
|
plugin_id: str,
|
|
user: User = Depends(require_login),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
获取插件详情
|
|
"""
|
|
result = await db.execute(
|
|
select(MCPPlugin).where(
|
|
MCPPlugin.id == plugin_id,
|
|
MCPPlugin.user_id == user.user_id
|
|
)
|
|
)
|
|
plugin = result.scalar_one_or_none()
|
|
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail="插件不存在")
|
|
|
|
return plugin
|
|
|
|
|
|
@router.put("/{plugin_id}", response_model=MCPPluginResponse)
|
|
async def update_plugin(
|
|
plugin_id: str,
|
|
data: MCPPluginUpdate,
|
|
user: User = Depends(require_login),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
更新插件配置
|
|
"""
|
|
result = await db.execute(
|
|
select(MCPPlugin).where(
|
|
MCPPlugin.id == plugin_id,
|
|
MCPPlugin.user_id == user.user_id
|
|
)
|
|
)
|
|
plugin = result.scalar_one_or_none()
|
|
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail="插件不存在")
|
|
|
|
# 更新字段
|
|
update_data = data.model_dump(exclude_unset=True)
|
|
for key, value in update_data.items():
|
|
setattr(plugin, key, value)
|
|
|
|
await db.commit()
|
|
await db.refresh(plugin)
|
|
|
|
# 如果插件已启用,重新加载
|
|
if plugin.enabled:
|
|
await mcp_registry.reload_plugin(plugin)
|
|
|
|
logger.info(f"用户 {user.user_id} 更新插件: {plugin.plugin_name}")
|
|
return plugin
|
|
|
|
|
|
@router.delete("/{plugin_id}")
|
|
async def delete_plugin(
|
|
plugin_id: str,
|
|
user: User = Depends(require_login),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
删除插件
|
|
"""
|
|
result = await db.execute(
|
|
select(MCPPlugin).where(
|
|
MCPPlugin.id == plugin_id,
|
|
MCPPlugin.user_id == user.user_id
|
|
)
|
|
)
|
|
plugin = result.scalar_one_or_none()
|
|
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail="插件不存在")
|
|
|
|
# 从注册表卸载
|
|
await mcp_registry.unload_plugin(user.user_id, plugin.plugin_name)
|
|
|
|
# 删除数据库记录
|
|
await db.delete(plugin)
|
|
await db.commit()
|
|
|
|
logger.info(f"用户 {user.user_id} 删除插件: {plugin.plugin_name}")
|
|
return {"message": "插件已删除", "plugin_name": plugin.plugin_name}
|
|
|
|
|
|
@router.post("/{plugin_id}/toggle", response_model=MCPPluginResponse)
|
|
async def toggle_plugin(
|
|
plugin_id: str,
|
|
enabled: bool = Query(..., description="启用或禁用"),
|
|
user: User = Depends(require_login),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
启用或禁用插件
|
|
"""
|
|
result = await db.execute(
|
|
select(MCPPlugin).where(
|
|
MCPPlugin.id == plugin_id,
|
|
MCPPlugin.user_id == user.user_id
|
|
)
|
|
)
|
|
plugin = result.scalar_one_or_none()
|
|
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail="插件不存在")
|
|
|
|
plugin.enabled = enabled
|
|
|
|
if enabled:
|
|
# 启用:加载到注册表
|
|
success = await mcp_registry.load_plugin(plugin)
|
|
if success:
|
|
plugin.status = "active"
|
|
plugin.last_error = None
|
|
else:
|
|
plugin.status = "error"
|
|
plugin.last_error = "加载失败"
|
|
else:
|
|
# 禁用:从注册表卸载
|
|
await mcp_registry.unload_plugin(user.user_id, plugin.plugin_name)
|
|
plugin.status = "inactive"
|
|
|
|
await db.commit()
|
|
await db.refresh(plugin)
|
|
|
|
action = "启用" if enabled else "禁用"
|
|
logger.info(f"用户 {user.user_id} {action}插件: {plugin.plugin_name}")
|
|
return plugin
|
|
|
|
|
|
@router.post("/{plugin_id}/test", response_model=MCPTestResult)
|
|
async def test_plugin(
|
|
plugin_id: str,
|
|
user: User = Depends(require_login),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
测试插件连接并调用工具验证功能
|
|
|
|
测试流程:
|
|
1. 测试MCP服务器连接
|
|
2. 获取工具列表
|
|
3. 自动选择一个工具进行实际调用测试
|
|
4. 返回完整测试结果
|
|
"""
|
|
import time
|
|
|
|
result = await db.execute(
|
|
select(MCPPlugin).where(
|
|
MCPPlugin.id == plugin_id,
|
|
MCPPlugin.user_id == user.user_id
|
|
)
|
|
)
|
|
plugin = result.scalar_one_or_none()
|
|
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail="插件不存在")
|
|
|
|
if not plugin.enabled:
|
|
return MCPTestResult(
|
|
success=False,
|
|
message="插件未启用",
|
|
error="请先启用插件",
|
|
suggestions=["点击开关按钮启用插件"]
|
|
)
|
|
|
|
start_time = time.time()
|
|
|
|
try:
|
|
# 1. 确保插件已加载
|
|
if not mcp_registry.get_client(user.user_id, plugin.plugin_name):
|
|
success = await mcp_registry.load_plugin(plugin)
|
|
if not success:
|
|
return MCPTestResult(
|
|
success=False,
|
|
message="插件加载失败",
|
|
error="无法创建MCP客户端",
|
|
suggestions=["请检查插件配置", "请确认服务器URL正确"]
|
|
)
|
|
|
|
# 2. 测试连接并获取工具列表
|
|
test_result = await mcp_registry.test_plugin(user.user_id, plugin.plugin_name)
|
|
|
|
if not test_result["success"]:
|
|
plugin.status = "error"
|
|
plugin.last_error = test_result.get("error", "连接测试失败")
|
|
plugin.last_test_at = datetime.now()
|
|
await db.commit()
|
|
return MCPTestResult(**test_result)
|
|
|
|
tools = test_result.get("tools", [])
|
|
|
|
if not tools:
|
|
plugin.status = "error"
|
|
plugin.last_error = "插件没有提供任何工具"
|
|
plugin.last_test_at = datetime.now()
|
|
await db.commit()
|
|
|
|
return MCPTestResult(
|
|
success=False,
|
|
message="插件没有提供任何工具",
|
|
error="工具列表为空",
|
|
response_time_ms=test_result.get("response_time_ms"),
|
|
suggestions=["请检查插件配置", "请确认MCP服务器正常运行"]
|
|
)
|
|
|
|
# 3. 使用AI智能选择工具并生成测试参数
|
|
logger.info(f"使用AI分析工具并生成测试计划...")
|
|
|
|
# 获取用户的AI设置
|
|
settings_result = await db.execute(
|
|
select(UserSettings).where(UserSettings.user_id == user.user_id)
|
|
)
|
|
user_settings = settings_result.scalar_one_or_none()
|
|
|
|
if not user_settings or not user_settings.api_key:
|
|
# 如果没有AI配置,回退到简单测试
|
|
logger.warning("用户未配置AI服务,使用简单连接测试")
|
|
plugin.status = "active"
|
|
plugin.last_error = None
|
|
plugin.last_test_at = datetime.now()
|
|
plugin.tools = tools
|
|
await db.commit()
|
|
|
|
return MCPTestResult(
|
|
success=True,
|
|
message=f"✅ 连接测试成功(未配置AI,跳过工具调用测试)",
|
|
response_time_ms=test_result.get("response_time_ms"),
|
|
tools_count=len(tools),
|
|
suggestions=[
|
|
f"连接测试: 成功",
|
|
f"可用工具数: {len(tools)}",
|
|
"提示: 配置AI服务后可进行智能工具调用测试"
|
|
]
|
|
)
|
|
|
|
# 使用AI的标准Function Calling机制选择工具
|
|
ai_service = create_user_ai_service(
|
|
api_provider=user_settings.api_provider,
|
|
api_key=user_settings.api_key,
|
|
api_base_url=user_settings.api_base_url,
|
|
model_name=user_settings.llm_model,
|
|
temperature=0.3,
|
|
max_tokens=1000
|
|
)
|
|
|
|
# 将MCP工具格式转换为OpenAI Function Calling格式
|
|
openai_tools = []
|
|
for tool in tools:
|
|
openai_tool = {
|
|
"type": "function",
|
|
"function": {
|
|
"name": tool["name"],
|
|
"description": tool.get("description", ""),
|
|
}
|
|
}
|
|
# 将 inputSchema 转换为 parameters
|
|
if "inputSchema" in tool:
|
|
openai_tool["function"]["parameters"] = tool["inputSchema"]
|
|
|
|
openai_tools.append(openai_tool)
|
|
|
|
logger.info(f"转换了 {len(openai_tools)} 个MCP工具为OpenAI格式")
|
|
logger.info(f"工具列表: {[t['function']['name'] for t in openai_tools]}")
|
|
|
|
# 使用标准的Function Calling,将转换后的工具传递给AI
|
|
prompt = f"""你是MCP插件测试助手,需要测试插件 '{plugin.plugin_name}' 的功能。
|
|
|
|
请选择一个合适的工具进行测试,优先选择搜索、查询类工具。
|
|
生成真实有效的测试参数(例如搜索"人工智能最新进展"而不是"test")。
|
|
|
|
现在开始测试这个插件。"""
|
|
|
|
system_prompt = "你是专业的API测试工具。当给定工具列表时,选择一个工具并使用合适的参数调用它。"
|
|
|
|
# 调用AI的Function Calling
|
|
logger.info(f"📞 准备调用AI Function Calling")
|
|
logger.info(f" - Provider: {user_settings.api_provider}")
|
|
logger.info(f" - Model: {user_settings.llm_model}")
|
|
logger.info(f" - Tools count: {len(openai_tools)}")
|
|
logger.debug(f" - Tools: {json.dumps(openai_tools, ensure_ascii=False, indent=2)}")
|
|
|
|
ai_response = await ai_service.generate_text(
|
|
prompt=prompt,
|
|
system_prompt=system_prompt,
|
|
tools=openai_tools, # 传递转换后的OpenAI格式工具
|
|
tool_choice="required" # 要求AI必须选择一个工具
|
|
)
|
|
|
|
logger.info(f"📥 收到AI响应")
|
|
logger.info(f" - Response keys: {list(ai_response.keys())}")
|
|
logger.debug(f" - Full response: {json.dumps(ai_response, ensure_ascii=False, indent=2)}")
|
|
|
|
# 检查AI是否请求调用工具
|
|
if not ai_response.get("tool_calls"):
|
|
# AI未调用工具,记录详细信息
|
|
logger.error(f"❌ AI未返回工具调用")
|
|
logger.error(f" - Response: {ai_response}")
|
|
logger.error(f" - Content: {ai_response.get('content', 'N/A')}")
|
|
logger.error(f" - Finish reason: {ai_response.get('finish_reason', 'N/A')}")
|
|
|
|
plugin.status = "error"
|
|
plugin.last_error = "AI未返回工具调用请求"
|
|
plugin.last_test_at = datetime.now()
|
|
await db.commit()
|
|
|
|
return MCPTestResult(
|
|
success=False,
|
|
message="❌ AI Function Calling失败",
|
|
error=f"AI未返回工具调用请求。响应: {ai_response.get('content', 'N/A')[:200]}",
|
|
tools_count=len(tools),
|
|
suggestions=[
|
|
"请确认使用的AI模型支持Function Calling",
|
|
"OpenAI: 需要gpt-4, gpt-3.5-turbo等模型",
|
|
"Anthropic: 需要claude-3系列模型",
|
|
f"当前Provider: {user_settings.api_provider}",
|
|
f"当前模型: {user_settings.llm_model}",
|
|
f"AI返回内容: {ai_response.get('content', 'N/A')[:100]}"
|
|
]
|
|
)
|
|
|
|
# 获取第一个工具调用
|
|
tool_call = ai_response["tool_calls"][0]
|
|
function = tool_call["function"]
|
|
tool_name = function["name"]
|
|
test_arguments = function["arguments"]
|
|
|
|
# AI返回的arguments可能是JSON字符串,需要解析
|
|
if isinstance(test_arguments, str):
|
|
try:
|
|
test_arguments = json.loads(test_arguments)
|
|
logger.info(f"✅ 解析AI返回的JSON字符串参数")
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"❌ 解析AI参数失败: {e}")
|
|
return MCPTestResult(
|
|
success=False,
|
|
message="❌ AI返回的参数格式错误",
|
|
error=f"无法解析参数JSON: {str(e)}",
|
|
tools_count=len(tools),
|
|
suggestions=["AI返回的参数不是有效的JSON格式"]
|
|
)
|
|
|
|
logger.info(f"🤖 AI通过Function Calling选择的工具: {tool_name}")
|
|
logger.info(f"📝 AI生成的参数: {test_arguments}")
|
|
logger.info(f"📝 参数类型: {type(test_arguments).__name__}")
|
|
|
|
# 4. 使用AI选择的工具和参数调用MCP工具
|
|
call_start = time.time()
|
|
try:
|
|
tool_result = await mcp_registry.call_tool(
|
|
user.user_id,
|
|
plugin.plugin_name,
|
|
tool_name,
|
|
test_arguments
|
|
)
|
|
|
|
call_end = time.time()
|
|
call_time = round((call_end - call_start) * 1000, 2)
|
|
total_time = round((call_end - start_time) * 1000, 2)
|
|
|
|
# 6. 测试成功,更新插件状态
|
|
plugin.status = "active"
|
|
plugin.last_error = None
|
|
plugin.last_test_at = datetime.now()
|
|
plugin.tools = tools # 缓存工具列表
|
|
await db.commit()
|
|
|
|
# 格式化工具结果用于显示
|
|
result_str = str(tool_result)
|
|
|
|
# 如果结果太长,截取前800字符
|
|
if len(result_str) > 800:
|
|
result_preview = result_str[:800] + "\n...(结果已截断,完整结果请查看日志)"
|
|
else:
|
|
result_preview = result_str
|
|
|
|
return MCPTestResult(
|
|
success=True,
|
|
message=f"✅ Function Calling测试成功!工具 '{tool_name}' 调用正常",
|
|
response_time_ms=total_time,
|
|
tools_count=len(tools),
|
|
suggestions=[
|
|
f"🤖 AI (Function Calling) 选择: {tool_name}",
|
|
f"📝 AI生成的参数: {json.dumps(test_arguments, ensure_ascii=False)}",
|
|
f"⏱️ 调用耗时: {call_time}ms",
|
|
f"📊 返回结果:\n{result_preview}"
|
|
]
|
|
)
|
|
|
|
except Exception as call_error:
|
|
call_end = time.time()
|
|
total_time = round((call_end - start_time) * 1000, 2)
|
|
|
|
logger.warning(f"工具调用失败: {tool_name}, 错误: {call_error}")
|
|
|
|
# 工具调用失败,但连接成功
|
|
plugin.status = "active" # 仍标记为active,因为连接是成功的
|
|
plugin.last_error = f"工具调用测试失败: {str(call_error)}"
|
|
plugin.last_test_at = datetime.now()
|
|
plugin.tools = tools
|
|
await db.commit()
|
|
|
|
return MCPTestResult(
|
|
success=True, # 连接成功就算测试通过
|
|
message=f"⚠️ 连接成功,但工具调用失败",
|
|
response_time_ms=total_time,
|
|
tools_count=len(tools),
|
|
error=f"工具 '{tool_name}' 调用失败: {str(call_error)}",
|
|
suggestions=[
|
|
f"✅ 连接测试: 成功",
|
|
f"❌ 工具调用测试: 失败",
|
|
f"🤖 AI (Function Calling) 选择: {tool_name}",
|
|
f"📝 AI生成的参数: {json.dumps(test_arguments, ensure_ascii=False)}",
|
|
f"❌ 错误: {str(call_error)}",
|
|
"💡 可能原因: API Key无效、参数错误或服务限制"
|
|
]
|
|
)
|
|
|
|
except Exception as e:
|
|
end_time = time.time()
|
|
total_time = round((end_time - start_time) * 1000, 2)
|
|
|
|
logger.error(f"测试插件失败: {plugin.plugin_name}, 错误: {e}")
|
|
|
|
plugin.status = "error"
|
|
plugin.last_error = str(e)
|
|
plugin.last_test_at = datetime.now()
|
|
await db.commit()
|
|
|
|
return MCPTestResult(
|
|
success=False,
|
|
message="❌ 测试失败",
|
|
response_time_ms=total_time,
|
|
error=str(e),
|
|
error_type=type(e).__name__,
|
|
suggestions=["请检查服务器是否在线", "请确认配置正确", "请检查API Key是否有效"]
|
|
)
|
|
|
|
|
|
def _build_test_arguments(tool_name: str, input_schema: dict, plugin_name: str) -> dict:
|
|
"""
|
|
根据工具schema智能构造测试参数
|
|
|
|
Args:
|
|
tool_name: 工具名称
|
|
input_schema: 输入schema
|
|
plugin_name: 插件名称
|
|
|
|
Returns:
|
|
测试参数字典
|
|
"""
|
|
# 针对常见MCP工具的默认测试参数
|
|
test_cases = {
|
|
# Exa搜索工具
|
|
"search": {
|
|
"query": "AI technology",
|
|
"num_results": 3
|
|
},
|
|
"search_and_contents": {
|
|
"query": "artificial intelligence",
|
|
"num_results": 2
|
|
},
|
|
# Brave搜索
|
|
"brave_web_search": {
|
|
"query": "AI news",
|
|
"count": 3
|
|
},
|
|
# Filesystem工具
|
|
"read_file": {
|
|
"path": "README.md"
|
|
},
|
|
"list_directory": {
|
|
"path": "."
|
|
},
|
|
}
|
|
|
|
# 如果有针对特定工具的测试用例,使用它
|
|
if tool_name in test_cases:
|
|
logger.info(f"使用预定义测试参数: {test_cases[tool_name]}")
|
|
return test_cases[tool_name]
|
|
|
|
# 否则根据schema自动构造
|
|
properties = input_schema.get("properties", {})
|
|
required = input_schema.get("required", [])
|
|
|
|
test_args = {}
|
|
|
|
for prop_name, prop_schema in properties.items():
|
|
# 只填充必需的参数
|
|
if prop_name not in required:
|
|
continue
|
|
|
|
prop_type = prop_schema.get("type", "string")
|
|
|
|
# 根据参数名称和类型猜测合适的测试值
|
|
if "query" in prop_name.lower() or "search" in prop_name.lower():
|
|
test_args[prop_name] = "test query"
|
|
elif "url" in prop_name.lower():
|
|
test_args[prop_name] = "https://example.com"
|
|
elif "path" in prop_name.lower():
|
|
test_args[prop_name] = "."
|
|
elif "count" in prop_name.lower() or "limit" in prop_name.lower() or "num" in prop_name.lower():
|
|
test_args[prop_name] = 3
|
|
elif prop_type == "string":
|
|
test_args[prop_name] = "test"
|
|
elif prop_type == "number" or prop_type == "integer":
|
|
test_args[prop_name] = 1
|
|
elif prop_type == "boolean":
|
|
test_args[prop_name] = True
|
|
elif prop_type == "array":
|
|
test_args[prop_name] = []
|
|
elif prop_type == "object":
|
|
test_args[prop_name] = {}
|
|
|
|
logger.info(f"自动构造测试参数: {test_args}")
|
|
return test_args
|
|
|
|
|
|
@router.get("/{plugin_id}/tools")
|
|
async def get_plugin_tools(
|
|
plugin_id: str,
|
|
user: User = Depends(require_login),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
获取插件提供的工具列表
|
|
"""
|
|
result = await db.execute(
|
|
select(MCPPlugin).where(
|
|
MCPPlugin.id == plugin_id,
|
|
MCPPlugin.user_id == user.user_id
|
|
)
|
|
)
|
|
plugin = result.scalar_one_or_none()
|
|
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail="插件不存在")
|
|
|
|
if not plugin.enabled:
|
|
raise HTTPException(status_code=400, detail="插件未启用")
|
|
|
|
try:
|
|
tools = await mcp_registry.get_plugin_tools(user.user_id, plugin.plugin_name)
|
|
|
|
# 更新缓存
|
|
plugin.tools = tools
|
|
await db.commit()
|
|
|
|
return {
|
|
"plugin_name": plugin.plugin_name,
|
|
"tools": tools,
|
|
"count": len(tools)
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"获取工具列表失败: {plugin.plugin_name}, 错误: {e}")
|
|
raise HTTPException(status_code=500, detail=f"获取工具列表失败: {str(e)}")
|
|
|
|
|
|
@router.post("/call")
|
|
async def call_mcp_tool(
|
|
data: MCPToolCall,
|
|
user: User = Depends(require_login),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
调用MCP工具
|
|
"""
|
|
# 获取插件
|
|
result = await db.execute(
|
|
select(MCPPlugin).where(
|
|
MCPPlugin.id == data.plugin_id,
|
|
MCPPlugin.user_id == user.user_id
|
|
)
|
|
)
|
|
plugin = result.scalar_one_or_none()
|
|
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail="插件不存在")
|
|
|
|
if not plugin.enabled:
|
|
raise HTTPException(status_code=400, detail="插件未启用")
|
|
|
|
try:
|
|
# 调用工具
|
|
result = await mcp_registry.call_tool(
|
|
user.user_id,
|
|
plugin.plugin_name,
|
|
data.tool_name,
|
|
data.arguments
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"plugin_name": plugin.plugin_name,
|
|
"tool_name": data.tool_name,
|
|
"result": result
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"调用工具失败: {plugin.plugin_name}.{data.tool_name}, 错误: {e}")
|
|
raise HTTPException(status_code=500, detail=f"工具调用失败: {str(e)}") |