fix: 修复MCP插件创建和测试时的500错误
问题:MCP SDK使用anyio TaskGroup,与FastAPI请求上下文不兼容, 导致在请求中直接await MCP操作时报RuntimeError: No response returned 解决方案: - 将MCP连接操作放到后台任务执行,避免阻塞请求 - 添加is_registered()和get_session_status()同步检查方法 - 测试时先检查会话是否存在,不存在则触发后台注册 - 改进ExceptionGroup错误处理,显示详细错误信息 - 状态同步改用异步队列,避免阻塞 修改文件: - backend/app/api/mcp_plugins.py: 重写test_plugin和create_plugin_simple - backend/app/mcp/facade.py: 添加同步检查方法和改进错误处理 - backend/app/mcp/status_sync.py: 使用异步队列同步状态 - backend/app/services/mcp_test_service.py: 使用同步检查代替异步ensure
This commit is contained in:
+162
-48
@@ -2,13 +2,14 @@
|
||||
|
||||
重构后使用统一的MCPClientFacade门面来管理所有MCP操作。
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
import asyncio
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, Request, BackgroundTasks
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
from sqlalchemy import select, update
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from app.database import get_db
|
||||
from app.database import get_db, get_engine
|
||||
from app.models.mcp_plugin import MCPPlugin
|
||||
from app.schemas.mcp_plugin import (
|
||||
MCPPluginCreate,
|
||||
@@ -36,6 +37,81 @@ def require_login(request: Request) -> User:
|
||||
return request.state.user
|
||||
|
||||
|
||||
async def _register_plugin_background(
|
||||
user_id: str,
|
||||
plugin_name: str,
|
||||
plugin_type: str,
|
||||
server_url: str,
|
||||
headers: Optional[dict],
|
||||
config: Optional[dict]
|
||||
):
|
||||
"""
|
||||
后台任务:注册MCP插件并更新数据库状态
|
||||
|
||||
在独立的任务中执行MCP连接,避免阻塞请求处理
|
||||
"""
|
||||
try:
|
||||
logger.info(f"后台注册MCP插件: {plugin_name}")
|
||||
|
||||
if plugin_type in ["http", "streamable_http", "sse"] and server_url:
|
||||
success = await mcp_client.register(MCPPluginConfig(
|
||||
user_id=user_id,
|
||||
plugin_name=plugin_name,
|
||||
url=server_url,
|
||||
plugin_type=plugin_type,
|
||||
headers=headers,
|
||||
timeout=config.get('timeout', 60.0) if config else 60.0
|
||||
))
|
||||
else:
|
||||
success = False
|
||||
|
||||
# 更新数据库状态
|
||||
engine = await get_engine(user_id)
|
||||
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
stmt = (
|
||||
update(MCPPlugin)
|
||||
.where(MCPPlugin.user_id == user_id, MCPPlugin.plugin_name == plugin_name)
|
||||
.values(
|
||||
status="active" if success else "error",
|
||||
last_error=None if success else "连接失败"
|
||||
)
|
||||
)
|
||||
await db.execute(stmt)
|
||||
await db.commit()
|
||||
|
||||
if success:
|
||||
logger.info(f"后台注册MCP插件成功: {plugin_name}")
|
||||
else:
|
||||
logger.warning(f"后台注册MCP插件失败: {plugin_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"后台注册MCP插件异常: {plugin_name}, 错误: {e}")
|
||||
try:
|
||||
engine = await get_engine(user_id)
|
||||
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
async with AsyncSessionLocal() as db:
|
||||
stmt = (
|
||||
update(MCPPlugin)
|
||||
.where(MCPPlugin.user_id == user_id, MCPPlugin.plugin_name == plugin_name)
|
||||
.values(status="error", last_error=str(e))
|
||||
)
|
||||
await db.execute(stmt)
|
||||
await db.commit()
|
||||
except Exception as db_error:
|
||||
logger.error(f"更新插件状态失败: {db_error}")
|
||||
|
||||
|
||||
async def _unregister_plugin_safe(user_id: str, plugin_name: str):
|
||||
"""安全地在后台注销MCP插件"""
|
||||
try:
|
||||
await mcp_client.unregister(user_id, plugin_name)
|
||||
logger.info(f"后台注销MCP插件成功: {plugin_name}")
|
||||
except Exception as e:
|
||||
logger.warning(f"后台注销MCP插件出错: {plugin_name}, 错误: {e}")
|
||||
|
||||
|
||||
async def _register_plugin_to_facade(plugin: MCPPlugin, user_id: str) -> bool:
|
||||
"""
|
||||
将插件注册到统一门面
|
||||
@@ -228,30 +304,31 @@ async def create_plugin_simple(
|
||||
# 更新字段
|
||||
for key, value in plugin_data.items():
|
||||
setattr(existing, key, value)
|
||||
|
||||
|
||||
# 设置为pending状态,等待后台连接
|
||||
if plugin_data.get("enabled"):
|
||||
existing.status = "pending"
|
||||
|
||||
plugin = existing
|
||||
await db.commit()
|
||||
await db.refresh(plugin)
|
||||
|
||||
# 数据库完成后进行MCP操作
|
||||
|
||||
# 后台执行MCP操作(不阻塞请求)
|
||||
if old_enabled:
|
||||
try:
|
||||
await mcp_client.unregister(user.user_id, old_plugin_name)
|
||||
except Exception as e:
|
||||
logger.warning(f"注销旧插件出错: {e}")
|
||||
|
||||
# 注销旧插件(使用create_task在后台执行)
|
||||
asyncio.create_task(_unregister_plugin_safe(user.user_id, old_plugin_name))
|
||||
|
||||
if plugin.enabled:
|
||||
try:
|
||||
success = await _register_plugin_to_facade(plugin, user.user_id)
|
||||
plugin.status = "active" if success else "error"
|
||||
plugin.last_error = None if success else "加载失败"
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"注册插件失败: {e}")
|
||||
plugin.status = "error"
|
||||
plugin.last_error = str(e)
|
||||
await db.commit()
|
||||
|
||||
# 后台注册新插件
|
||||
asyncio.create_task(_register_plugin_background(
|
||||
user_id=user.user_id,
|
||||
plugin_name=plugin.plugin_name,
|
||||
plugin_type=plugin.plugin_type,
|
||||
server_url=plugin.server_url,
|
||||
headers=plugin.headers,
|
||||
config=plugin.config
|
||||
))
|
||||
|
||||
logger.info(f"用户 {user.user_id} 更新插件: {plugin_name}")
|
||||
else:
|
||||
# 创建新插件
|
||||
@@ -259,24 +336,26 @@ async def create_plugin_simple(
|
||||
user_id=user.user_id,
|
||||
**plugin_data
|
||||
)
|
||||
|
||||
|
||||
# 设置为pending状态,等待后台连接
|
||||
if plugin_data.get("enabled"):
|
||||
plugin.status = "pending"
|
||||
|
||||
db.add(plugin)
|
||||
await db.commit()
|
||||
await db.refresh(plugin)
|
||||
|
||||
# 数据库完成后进行MCP操作
|
||||
|
||||
# 后台执行MCP注册(不阻塞请求)
|
||||
if plugin.enabled:
|
||||
try:
|
||||
success = await _register_plugin_to_facade(plugin, user.user_id)
|
||||
plugin.status = "active" if success else "error"
|
||||
plugin.last_error = None if success else "加载失败"
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"注册插件失败: {e}")
|
||||
plugin.status = "error"
|
||||
plugin.last_error = str(e)
|
||||
await db.commit()
|
||||
|
||||
asyncio.create_task(_register_plugin_background(
|
||||
user_id=user.user_id,
|
||||
plugin_name=plugin.plugin_name,
|
||||
plugin_type=plugin.plugin_type,
|
||||
server_url=plugin.server_url,
|
||||
headers=plugin.headers,
|
||||
config=plugin.config
|
||||
))
|
||||
|
||||
logger.info(f"用户 {user.user_id} 通过简化配置创建插件: {plugin_name}")
|
||||
|
||||
return plugin
|
||||
@@ -465,10 +544,11 @@ async def test_plugin(
|
||||
):
|
||||
"""
|
||||
测试插件连接并调用工具验证功能
|
||||
|
||||
使用MCPTestService进行测试
|
||||
|
||||
使用MCPTestService进行测试。
|
||||
如果插件会话尚未建立,会先在后台注册,需要再次调用测试。
|
||||
"""
|
||||
|
||||
|
||||
result = await db.execute(
|
||||
select(MCPPlugin).where(
|
||||
MCPPlugin.id == plugin_id,
|
||||
@@ -476,10 +556,10 @@ async def test_plugin(
|
||||
)
|
||||
)
|
||||
plugin = result.scalar_one_or_none()
|
||||
|
||||
|
||||
if not plugin:
|
||||
raise HTTPException(status_code=404, detail="插件不存在")
|
||||
|
||||
|
||||
if not plugin.enabled:
|
||||
return MCPTestResult(
|
||||
success=False,
|
||||
@@ -487,11 +567,45 @@ async def test_plugin(
|
||||
error="请先启用插件",
|
||||
suggestions=["点击开关按钮启用插件"]
|
||||
)
|
||||
|
||||
# 使用测试服务
|
||||
|
||||
# 检查会话是否已注册
|
||||
is_registered = mcp_client.is_registered(user.user_id, plugin.plugin_name)
|
||||
session_status = mcp_client.get_session_status(user.user_id, plugin.plugin_name)
|
||||
|
||||
if not is_registered:
|
||||
# 会话不存在或状态异常,需要在后台注册
|
||||
logger.info(f"插件 {plugin.plugin_name} 会话不存在(状态: {session_status}),启动后台注册")
|
||||
|
||||
# 更新数据库状态为pending
|
||||
plugin.status = "pending"
|
||||
plugin.last_error = None
|
||||
await db.commit()
|
||||
|
||||
# 在后台注册插件
|
||||
asyncio.create_task(_register_plugin_background(
|
||||
user_id=user.user_id,
|
||||
plugin_name=plugin.plugin_name,
|
||||
plugin_type=plugin.plugin_type,
|
||||
server_url=plugin.server_url,
|
||||
headers=plugin.headers,
|
||||
config=plugin.config
|
||||
))
|
||||
|
||||
return MCPTestResult(
|
||||
success=False,
|
||||
message="正在建立连接...",
|
||||
error="插件会话正在初始化,请稍后重试",
|
||||
suggestions=[
|
||||
"插件正在连接MCP服务器",
|
||||
"请等待2-3秒后再次点击测试",
|
||||
"如果持续失败,请检查服务器地址是否正确"
|
||||
]
|
||||
)
|
||||
|
||||
# 会话已存在,直接执行测试
|
||||
try:
|
||||
test_result = await mcp_test_service.test_plugin_with_ai(plugin, user, db)
|
||||
|
||||
|
||||
# 更新插件状态
|
||||
if test_result.success:
|
||||
plugin.status = "active"
|
||||
@@ -499,12 +613,12 @@ async def test_plugin(
|
||||
else:
|
||||
plugin.status = "error"
|
||||
plugin.last_error = test_result.error
|
||||
|
||||
|
||||
plugin.last_test_at = datetime.now()
|
||||
await db.commit()
|
||||
|
||||
|
||||
return test_result
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"测试插件失败: {plugin.plugin_name}, 错误: {e}")
|
||||
plugin.status = "error"
|
||||
|
||||
Reference in New Issue
Block a user