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:
snemc
2026-01-24 10:03:59 +08:00
parent 165a02ea75
commit 980cc5b0e5
4 changed files with 282 additions and 95 deletions
+162 -48
View File
@@ -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"