diff --git a/server/routers/__init__.py b/server/routers/__init__.py
index 8649620c..aaae02fa 100644
--- a/server/routers/__init__.py
+++ b/server/routers/__init__.py
@@ -6,6 +6,7 @@ from server.routers.dashboard_router import dashboard
from server.routers.graph_router import graph
from server.routers.knowledge_router import knowledge
from server.routers.evaluation_router import evaluation
+from server.routers.mcp_router import mcp
from server.routers.mindmap_router import mindmap
from server.routers.system_router import system
from server.routers.task_router import tasks
@@ -22,3 +23,5 @@ router.include_router(evaluation) # /api/evaluation/*
router.include_router(mindmap) # /api/mindmap/*
router.include_router(graph) # /api/graph/*
router.include_router(tasks) # /api/tasks/*
+router.include_router(mcp) # /api/system/mcp-servers/*
+
diff --git a/server/routers/mcp_router.py b/server/routers/mcp_router.py
new file mode 100644
index 00000000..b049318b
--- /dev/null
+++ b/server/routers/mcp_router.py
@@ -0,0 +1,420 @@
+"""MCP 服务器管理路由"""
+
+from fastapi import APIRouter, Body, Depends, HTTPException
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from src.agents.common.mcp import (
+ clear_mcp_server_tools_cache,
+ get_mcp_tools,
+ sync_mcp_server_to_cache,
+)
+from src.storage.db.models import MCPServer, User
+from src.utils import logger
+from server.utils.auth_middleware import get_admin_user, get_db
+
+mcp = APIRouter(prefix="/system/mcp-servers", tags=["mcp"])
+
+
+# =============================================================================
+# === MCP 服务器 CRUD ===
+# =============================================================================
+
+
+@mcp.get("")
+async def get_mcp_servers(
+ current_user: User = Depends(get_admin_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """获取所有 MCP 服务器配置"""
+ try:
+ result = await db.execute(select(MCPServer))
+ servers = result.scalars().all()
+ return {"success": True, "data": [s.to_dict() for s in servers]}
+ except Exception as e:
+ logger.error(f"Failed to get MCP servers: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@mcp.post("")
+async def create_mcp_server(
+ name: str = Body(..., description="服务器名称"),
+ transport: str = Body(..., description="传输类型:sse/streamable_http"),
+ url: str = Body(..., description="服务器 URL"),
+ description: str = Body(None, description="描述"),
+ headers: dict = Body(None, description="HTTP 请求头"),
+ timeout: int = Body(None, description="HTTP 超时时间(秒)"),
+ sse_read_timeout: int = Body(None, description="SSE 读取超时(秒)"),
+ tags: list = Body(None, description="标签数组"),
+ icon: str = Body(None, description="图标(emoji)"),
+ current_user: User = Depends(get_admin_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """创建新的 MCP 服务器"""
+ # 校验传输类型
+ if transport not in ("sse", "streamable_http"):
+ raise HTTPException(status_code=400, detail="传输类型必须是 sse 或 streamable_http")
+
+ try:
+ # 检查名称是否已存在
+ result = await db.execute(select(MCPServer).filter(MCPServer.name == name))
+ existing = result.scalar_one_or_none()
+ if existing:
+ raise HTTPException(status_code=400, detail=f"服务器名称 '{name}' 已存在")
+
+ server = MCPServer(
+ name=name,
+ description=description,
+ transport=transport,
+ url=url,
+ headers=headers,
+ timeout=timeout,
+ sse_read_timeout=sse_read_timeout,
+ tags=tags,
+ icon=icon,
+ enabled=1,
+ created_by=current_user.username,
+ updated_by=current_user.username,
+ )
+ db.add(server)
+ await db.commit()
+ await db.refresh(server)
+
+ # 同步到缓存
+ sync_mcp_server_to_cache(name, server.to_mcp_config())
+
+ return {"success": True, "data": server.to_dict()}
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Failed to create MCP server: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@mcp.get("/{name}")
+async def get_mcp_server(
+ name: str,
+ current_user: User = Depends(get_admin_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """获取单个 MCP 服务器配置"""
+ try:
+ result = await db.execute(select(MCPServer).filter(MCPServer.name == name))
+ server = result.scalar_one_or_none()
+ if not server:
+ raise HTTPException(status_code=404, detail=f"服务器 '{name}' 不存在")
+ return {"success": True, "data": server.to_dict()}
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Failed to get MCP server: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@mcp.put("/{name}")
+async def update_mcp_server(
+ name: str,
+ description: str = Body(None, description="描述"),
+ transport: str = Body(None, description="传输类型"),
+ url: str = Body(None, description="服务器 URL"),
+ headers: dict = Body(None, description="HTTP 请求头"),
+ timeout: int = Body(None, description="HTTP 超时时间(秒)"),
+ sse_read_timeout: int = Body(None, description="SSE 读取超时(秒)"),
+ tags: list = Body(None, description="标签数组"),
+ icon: str = Body(None, description="图标(emoji)"),
+ current_user: User = Depends(get_admin_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """更新 MCP 服务器配置"""
+ # 校验传输类型
+ if transport is not None and transport not in ("sse", "streamable_http"):
+ raise HTTPException(status_code=400, detail="传输类型必须是 sse 或 streamable_http")
+
+ try:
+ result = await db.execute(select(MCPServer).filter(MCPServer.name == name))
+ server = result.scalar_one_or_none()
+ if not server:
+ raise HTTPException(status_code=404, detail=f"服务器 '{name}' 不存在")
+
+ # 更新字段
+ if description is not None:
+ server.description = description
+ if transport is not None:
+ server.transport = transport
+ if url is not None:
+ server.url = url
+ if headers is not None:
+ server.headers = headers
+ if timeout is not None:
+ server.timeout = timeout
+ if sse_read_timeout is not None:
+ server.sse_read_timeout = sse_read_timeout
+ if tags is not None:
+ server.tags = tags
+ if icon is not None:
+ server.icon = icon
+
+ server.updated_by = current_user.username
+ await db.commit()
+ await db.refresh(server)
+
+ # 同步到缓存(如果启用)
+ if server.enabled:
+ sync_mcp_server_to_cache(name, server.to_mcp_config())
+
+ return {"success": True, "data": server.to_dict()}
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Failed to update MCP server: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@mcp.delete("/{name}")
+async def delete_mcp_server(
+ name: str,
+ current_user: User = Depends(get_admin_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """删除 MCP 服务器"""
+ try:
+ result = await db.execute(select(MCPServer).filter(MCPServer.name == name))
+ server = result.scalar_one_or_none()
+ if not server:
+ raise HTTPException(status_code=404, detail=f"服务器 '{name}' 不存在")
+
+ await db.delete(server)
+ await db.commit()
+
+ # 从缓存中删除
+ sync_mcp_server_to_cache(name, None)
+
+ return {"success": True, "message": f"服务器 '{name}' 已删除"}
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Failed to delete MCP server: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+# =============================================================================
+# === MCP 服务器操作 ===
+# =============================================================================
+
+
+@mcp.post("/{name}/test")
+async def test_mcp_server(
+ name: str,
+ current_user: User = Depends(get_admin_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """测试 MCP 服务器连接"""
+ try:
+ result = await db.execute(select(MCPServer).filter(MCPServer.name == name))
+ server = result.scalar_one_or_none()
+ if not server:
+ raise HTTPException(status_code=404, detail=f"服务器 '{name}' 不存在")
+
+ # 获取配置用于测试
+ config = server.to_mcp_config()
+
+ try:
+ tools = await get_mcp_tools(name, {name: config})
+ return {
+ "success": True,
+ "message": f"连接成功,共发现 {len(tools)} 个工具",
+ "tool_count": len(tools),
+ }
+ except Exception as test_error:
+ return {
+ "success": False,
+ "message": f"连接失败: {str(test_error)}",
+ }
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Failed to test MCP server: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@mcp.put("/{name}/toggle")
+async def toggle_mcp_server(
+ name: str,
+ current_user: User = Depends(get_admin_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """切换 MCP 服务器启用状态"""
+ try:
+ result = await db.execute(select(MCPServer).filter(MCPServer.name == name))
+ server = result.scalar_one_or_none()
+ if not server:
+ raise HTTPException(status_code=404, detail=f"服务器 '{name}' 不存在")
+
+ # 切换状态
+ server.enabled = 0 if server.enabled else 1
+ server.updated_by = current_user.username
+ await db.commit()
+
+ # 获取更新后的状态
+ is_enabled = bool(server.enabled)
+ server_config = server.to_mcp_config() if is_enabled else None
+
+ # 同步到缓存
+ sync_mcp_server_to_cache(name, server_config)
+
+ return {
+ "success": True,
+ "enabled": is_enabled,
+ "message": f"服务器 '{name}' 已{'启用' if is_enabled else '禁用'}",
+ }
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Failed to toggle MCP server: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+# =============================================================================
+# === MCP 工具管理 ===
+# =============================================================================
+
+
+@mcp.get("/{name}/tools")
+async def get_mcp_server_tools(
+ name: str,
+ current_user: User = Depends(get_admin_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """获取 MCP 服务器的工具列表"""
+ try:
+ result = await db.execute(select(MCPServer).filter(MCPServer.name == name))
+ server = result.scalar_one_or_none()
+ if not server:
+ raise HTTPException(status_code=404, detail=f"服务器 '{name}' 不存在")
+
+ # 获取配置
+ config = server.to_mcp_config()
+ disabled_tools = server.disabled_tools or []
+
+ try:
+ tools = await get_mcp_tools(name, {name: config})
+ tool_list = []
+
+ for tool in tools:
+ original_name = tool.name
+ unique_id = tool.metadata.get("id") if tool.metadata else original_name
+
+ tool_info = {
+ "name": original_name,
+ "id": unique_id,
+ "description": getattr(tool, "description", ""),
+ "enabled": original_name not in disabled_tools,
+ }
+ # 提取参数信息
+ if hasattr(tool, "args_schema") and tool.args_schema:
+ schema = tool.args_schema.schema() if hasattr(tool.args_schema, "schema") else {}
+ tool_info["parameters"] = schema.get("properties", {})
+ tool_info["required"] = schema.get("required", [])
+ else:
+ tool_info["parameters"] = {}
+ tool_info["required"] = []
+ tool_list.append(tool_info)
+
+ return {
+ "success": True,
+ "data": tool_list,
+ "total": len(tool_list),
+ }
+ except Exception as tool_error:
+ logger.error(f"Failed to get tools from MCP server '{name}': {tool_error}")
+ return {
+ "success": False,
+ "message": f"获取工具失败: {str(tool_error)}",
+ "data": [],
+ "total": 0,
+ }
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Failed to get MCP server tools: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@mcp.post("/{name}/tools/refresh")
+async def refresh_mcp_server_tools(
+ name: str,
+ current_user: User = Depends(get_admin_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """刷新 MCP 服务器的工具列表(清除缓存重新获取)"""
+ try:
+ result = await db.execute(select(MCPServer).filter(MCPServer.name == name))
+ server = result.scalar_one_or_none()
+ if not server:
+ raise HTTPException(status_code=404, detail=f"服务器 '{name}' 不存在")
+
+ # 清除该服务器的工具缓存
+ clear_mcp_server_tools_cache(name)
+
+ # 获取配置
+ config = server.to_mcp_config()
+
+ try:
+ tools = await get_mcp_tools(name, {name: config})
+ return {
+ "success": True,
+ "message": f"工具列表已刷新,共发现 {len(tools)} 个工具",
+ "tool_count": len(tools),
+ }
+ except Exception as tool_error:
+ return {
+ "success": False,
+ "message": f"刷新失败: {str(tool_error)}",
+ }
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Failed to refresh MCP server tools: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@mcp.put("/{name}/tools/{tool_name}/toggle")
+async def toggle_mcp_server_tool(
+ name: str,
+ tool_name: str,
+ current_user: User = Depends(get_admin_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """切换单个工具的启用状态"""
+ try:
+ result = await db.execute(select(MCPServer).filter(MCPServer.name == name))
+ server = result.scalar_one_or_none()
+ if not server:
+ raise HTTPException(status_code=404, detail=f"服务器 '{name}' 不存在")
+
+ disabled_tools = list(server.disabled_tools or [])
+
+ if tool_name in disabled_tools:
+ # 当前禁用,改为启用
+ disabled_tools.remove(tool_name)
+ enabled = True
+ else:
+ # 当前启用,改为禁用
+ disabled_tools.append(tool_name)
+ enabled = False
+
+ server.disabled_tools = disabled_tools
+ server.updated_by = current_user.username
+ await db.commit()
+
+ return {
+ "success": True,
+ "tool_name": tool_name,
+ "enabled": enabled,
+ "message": f"工具 '{tool_name}' 已{'启用' if enabled else '禁用'}",
+ }
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Failed to toggle MCP server tool: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/server/utils/lifespan.py b/server/utils/lifespan.py
index 45e59bbf..65e9e406 100644
--- a/server/utils/lifespan.py
+++ b/server/utils/lifespan.py
@@ -3,11 +3,16 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI
from server.services import tasker
+from src.agents.common.mcp import init_mcp_servers
@asynccontextmanager
async def lifespan(app: FastAPI):
- await tasker.start()
"""FastAPI lifespan事件管理器"""
+ # 初始化 MCP 服务器配置
+ await init_mcp_servers()
+
+ await tasker.start()
yield
await tasker.shutdown()
+
diff --git a/src/agents/common/mcp.py b/src/agents/common/mcp.py
index fe7cf10a..00d0c471 100644
--- a/src/agents/common/mcp.py
+++ b/src/agents/common/mcp.py
@@ -11,30 +11,106 @@ from src.utils import logger
# Global MCP tools cache
_mcp_tools_cache: dict[str, list[Callable[..., Any]]] = {}
-# MCP Server configurations
-MCP_SERVERS = {
+# MCP Server configurations(运行时缓存,从数据库加载)
+MCP_SERVERS: dict[str, dict[str, Any]] = {}
+
+# 默认 MCP 服务器配置(首次启动时导入数据库)
+_DEFAULT_MCP_SERVERS = {
"sequentialthinking": {
"url": "https://remote.mcpservers.org/sequentialthinking/mcp",
"transport": "streamable_http",
+ "description": "顺序思考工具,帮助 AI 将复杂问题分解为多个步骤",
+ "icon": "🧠",
+ "tags": ["工具", "AI"],
},
- # "zhipu-web-search-sse": {
- # "url": f"https://open.bigmodel.cn/api/mcp/web_search/sse?Authorization={os.getenv('ZHIPUAI_API_KEY')}",
- # "transport": "streamable_http",
- # },
- # 这些 stdio 的 MCP server 需要在本地启动,启动的时候需要安装对应的包,需要时间
- # "time": {
- # "command": "uvx",
- # "args": ["mcp-server-time"],
- # "transport": "stdio",
- # },
- # "mcp_server_chart": {
- # "command": "npx",
- # "args": ["-y", "@antv/mcp-server-chart"],
- # "transport": "stdio"
- # },
- # 更多用法参考:https://xerrors.github.io/Yuxi-Know/latest/advanced/agents-config.html#内置工具与-mcp-集成
}
+async def load_mcp_servers_from_db() -> None:
+ """从数据库加载所有启用的 MCP 服务器配置到 MCP_SERVERS 缓存"""
+ global MCP_SERVERS
+
+ # 延迟导入以避免循环引用
+ from sqlalchemy import select
+ from src.storage.db.manager import db_manager
+ from src.storage.db.models import MCPServer
+
+ try:
+ async with db_manager.get_async_session_context() as session:
+ result = await session.execute(select(MCPServer).filter(MCPServer.enabled == 1))
+ servers = result.scalars().all()
+ MCP_SERVERS.clear()
+ for server in servers:
+ MCP_SERVERS[server.name] = server.to_mcp_config()
+ logger.info(f"Loaded {len(MCP_SERVERS)} MCP servers from database: {list(MCP_SERVERS.keys())}")
+ except Exception as e:
+ logger.error(f"Failed to load MCP servers from database: {e}")
+
+
+def sync_mcp_server_to_cache(name: str, config: dict[str, Any] | None) -> None:
+ """同步单个 MCP 服务器配置到缓存
+
+ Args:
+ name: 服务器名称
+ config: 服务器配置,如果为 None 则从缓存中删除
+ """
+ global MCP_SERVERS
+
+ if config is None:
+ MCP_SERVERS.pop(name, None)
+ logger.info(f"Removed MCP server '{name}' from cache")
+ else:
+ MCP_SERVERS[name] = config
+ logger.info(f"Synced MCP server '{name}' to cache")
+
+ # 清除该服务器的工具缓存
+ _mcp_tools_cache.pop(name, None)
+
+
+async def init_mcp_servers() -> None:
+ """初始化 MCP 服务器配置
+
+ 首次启动时,如果数据库为空,将默认配置导入数据库
+ 然后从数据库加载配置到 MCP_SERVERS 缓存
+ """
+ # 延迟导入以避免循环引用
+ from sqlalchemy import func, select
+ from src.storage.db.manager import db_manager
+ from src.storage.db.models import MCPServer
+
+ try:
+ async with db_manager.get_async_session_context() as session:
+ # 检查数据库是否有 MCP 配置
+ result = await session.execute(select(func.count(MCPServer.name)))
+ count = result.scalar()
+
+ if count == 0:
+ # 数据库为空,导入默认配置
+ logger.info("No MCP servers in database, importing default configurations...")
+ for name, config in _DEFAULT_MCP_SERVERS.items():
+ server = MCPServer(
+ name=name,
+ description=config.get("description"),
+ transport=config["transport"],
+ url=config["url"],
+ headers=config.get("headers"),
+ timeout=config.get("timeout"),
+ sse_read_timeout=config.get("sse_read_timeout"),
+ tags=config.get("tags"),
+ icon=config.get("icon"),
+ enabled=1,
+ created_by="system",
+ updated_by="system",
+ )
+ session.add(server)
+ await session.commit()
+ logger.info(f"Imported {len(_DEFAULT_MCP_SERVERS)} default MCP servers to database")
+
+ # 从数据库加载配置到缓存
+ await load_mcp_servers_from_db()
+
+ except Exception as e:
+ logger.error(f"Failed to initialize MCP servers: {e}, traceback: {traceback.format_exc()}")
+
async def get_mcp_client(
server_configs: dict[str, Any] | None = None,
@@ -49,8 +125,19 @@ async def get_mcp_client(
return None
+def to_camel_case(s: str) -> str:
+ """将字符串转换为小驼峰格式"""
+ import re
+ # 处理 - 和 _
+ s = re.sub(r'[-_]+(.)', lambda m: m.group(1).upper(), s)
+ # 首字母小写
+ if len(s) > 0:
+ s = s[0].lower() + s[1:]
+ return s
+
+
async def get_mcp_tools(server_name: str, additional_servers: dict[str, dict] = None) -> list[Callable[..., Any]]:
- """Get MCP tools for a specific server, initializing client if needed."""
+ """Get MCP tools for a specific server, initializing client if needed and rendering unique IDs."""
global _mcp_tools_cache
# Return cached tools if available
@@ -65,13 +152,30 @@ async def get_mcp_tools(server_name: str, additional_servers: dict[str, dict] =
if client is None:
return []
- # Get all tools and filter by server (if tools have server metadata)
+ # Get all tools
all_tools = await client.get_tools()
- tools = cast(list[Callable[..., Any]], all_tools)
+ raw_tools = cast(list[Any], all_tools)
- _mcp_tools_cache[server_name] = tools
- logger.info(f"Loaded {len(tools)} tools from MCP server '{server_name}'")
- return tools
+ # 渲染 ID 规则: mcp__[camelCaseServer]__[camelCaseTool]
+ server_cc = to_camel_case(server_name)
+ processed_tools = []
+
+ for tool in raw_tools:
+ # 渲染唯一 ID 规则: mcp__[camelCaseServer]__[camelCaseTool]
+ original_name = tool.name
+ tool_cc = to_camel_case(original_name)
+ unique_id = f"mcp__{server_cc}__{tool_cc}"
+
+ # 使用 metadata 存储,这是 LangChain 工具扩展属性的标准做法
+ if tool.metadata is None:
+ tool.metadata = {}
+ tool.metadata["id"] = unique_id
+
+ processed_tools.append(tool)
+
+ _mcp_tools_cache[server_name] = processed_tools
+ logger.info(f"Loaded {len(processed_tools)} tools from MCP server '{server_name}' with extra tool IDs")
+ return processed_tools
except AssertionError as e:
logger.warning(f"[assert] Failed to load tools from MCP server '{server_name}': {e}")
return []
@@ -100,3 +204,10 @@ def clear_mcp_cache() -> None:
"""Clear the MCP tools cache (useful for testing)."""
global _mcp_tools_cache
_mcp_tools_cache = {}
+
+
+def clear_mcp_server_tools_cache(server_name: str) -> None:
+ """Clear the tools cache for a specific MCP server."""
+ global _mcp_tools_cache
+ _mcp_tools_cache.pop(server_name, None)
+ logger.info(f"Cleared tools cache for MCP server '{server_name}'")
diff --git a/src/storage/db/models.py b/src/storage/db/models.py
index ad234311..77cfb099 100644
--- a/src/storage/db/models.py
+++ b/src/storage/db/models.py
@@ -348,3 +348,76 @@ class MessageFeedback(Base):
"reason": self.reason,
"created_at": format_utc_datetime(self.created_at),
}
+
+
+class MCPServer(Base):
+ """MCP 服务器配置模型"""
+
+ __tablename__ = "mcp_servers"
+
+ # 核心字段 - name 作为主键
+ name = Column(String(100), primary_key=True, comment="服务器名称(唯一标识)")
+ description = Column(String(500), nullable=True, comment="描述")
+
+ # 连接配置
+ transport = Column(String(20), nullable=False, comment="传输类型:sse/streamable_http")
+ url = Column(String(500), nullable=False, comment="服务器 URL")
+ headers = Column(JSON, nullable=True, comment="HTTP 请求头")
+ timeout = Column(Integer, nullable=True, comment="HTTP 超时时间(秒)")
+ sse_read_timeout = Column(Integer, nullable=True, comment="SSE 读取超时(秒)")
+
+ # UI 增强字段
+ tags = Column(JSON, nullable=True, comment="标签数组")
+ icon = Column(String(50), nullable=True, comment="图标(emoji)")
+
+ # 状态字段
+ enabled = Column(Integer, nullable=False, default=1, comment="是否启用:1=是,0=否")
+ disabled_tools = Column(JSON, nullable=True, comment="禁用的工具名称列表")
+
+ # 用户追踪
+ created_by = Column(String(100), nullable=False, comment="创建人用户名")
+ updated_by = Column(String(100), nullable=False, comment="修改人用户名")
+
+ # 时间戳
+ created_at = Column(DateTime, default=utc_now, comment="创建时间")
+ updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, comment="更新时间")
+
+ def to_dict(self):
+ def format_utc_datetime(dt_value):
+ if dt_value is None:
+ return None
+ if dt_value.tzinfo is None:
+ dt_value = dt_value.replace(tzinfo=dt.UTC)
+ return utc_isoformat(dt_value)
+
+ return {
+ "name": self.name,
+ "description": self.description,
+ "transport": self.transport,
+ "url": self.url,
+ "headers": self.headers or {},
+ "timeout": self.timeout,
+ "sse_read_timeout": self.sse_read_timeout,
+ "tags": self.tags or [],
+ "icon": self.icon,
+ "enabled": bool(self.enabled),
+ "disabled_tools": self.disabled_tools or [],
+ "created_by": self.created_by,
+ "updated_by": self.updated_by,
+ "created_at": format_utc_datetime(self.created_at),
+ "updated_at": format_utc_datetime(self.updated_at),
+ }
+
+ def to_mcp_config(self) -> dict:
+ """转换为 MCP 配置格式(用于加载到 MCP_SERVERS 缓存)"""
+ config = {
+ "transport": self.transport,
+ "url": self.url,
+ }
+ if self.headers:
+ config["headers"] = self.headers
+ if self.timeout is not None:
+ config["timeout"] = self.timeout
+ if self.sse_read_timeout is not None:
+ config["sse_read_timeout"] = self.sse_read_timeout
+ return config
diff --git a/web/src/apis/mcp_api.js b/web/src/apis/mcp_api.js
new file mode 100644
index 00000000..29b4e700
--- /dev/null
+++ b/web/src/apis/mcp_api.js
@@ -0,0 +1,133 @@
+import { apiAdminGet, apiAdminPost, apiAdminPut, apiAdminDelete } from './base'
+
+/**
+ * MCP 服务器管理 API 模块
+ * 包含 MCP 服务器的增删改查和工具管理功能
+ */
+
+const BASE_URL = '/api/system/mcp-servers'
+
+// =============================================================================
+// === MCP 服务器 CRUD ===
+// =============================================================================
+
+/**
+ * 获取所有 MCP 服务器配置
+ * @returns {Promise} - 服务器列表
+ */
+export const getMcpServers = async () => {
+ return apiAdminGet(BASE_URL)
+}
+
+/**
+ * 获取单个 MCP 服务器配置
+ * @param {string} name - 服务器名称
+ * @returns {Promise} - 服务器配置
+ */
+export const getMcpServer = async (name) => {
+ return apiAdminGet(`${BASE_URL}/${encodeURIComponent(name)}`)
+}
+
+/**
+ * 创建新的 MCP 服务器
+ * @param {Object} data - 服务器配置数据
+ * @returns {Promise} - 创建结果
+ */
+export const createMcpServer = async (data) => {
+ return apiAdminPost(BASE_URL, data)
+}
+
+/**
+ * 更新 MCP 服务器配置
+ * @param {string} name - 服务器名称
+ * @param {Object} data - 更新数据
+ * @returns {Promise} - 更新结果
+ */
+export const updateMcpServer = async (name, data) => {
+ return apiAdminPut(`${BASE_URL}/${encodeURIComponent(name)}`, data)
+}
+
+/**
+ * 删除 MCP 服务器
+ * @param {string} name - 服务器名称
+ * @returns {Promise} - 删除结果
+ */
+export const deleteMcpServer = async (name) => {
+ return apiAdminDelete(`${BASE_URL}/${encodeURIComponent(name)}`)
+}
+
+// =============================================================================
+// === MCP 服务器操作 ===
+// =============================================================================
+
+/**
+ * 测试 MCP 服务器连接
+ * @param {string} name - 服务器名称
+ * @returns {Promise} - 测试结果
+ */
+export const testMcpServer = async (name) => {
+ return apiAdminPost(`${BASE_URL}/${encodeURIComponent(name)}/test`, {})
+}
+
+/**
+ * 切换 MCP 服务器启用状态
+ * @param {string} name - 服务器名称
+ * @returns {Promise} - 切换结果
+ */
+export const toggleMcpServer = async (name) => {
+ return apiAdminPut(`${BASE_URL}/${encodeURIComponent(name)}/toggle`, {})
+}
+
+// =============================================================================
+// === MCP 工具管理 ===
+// =============================================================================
+
+/**
+ * 获取 MCP 服务器的工具列表
+ * @param {string} name - 服务器名称
+ * @returns {Promise} - 工具列表
+ */
+export const getMcpServerTools = async (name) => {
+ return apiAdminGet(`${BASE_URL}/${encodeURIComponent(name)}/tools`)
+}
+
+/**
+ * 刷新 MCP 服务器的工具列表(清除缓存重新获取)
+ * @param {string} name - 服务器名称
+ * @returns {Promise} - 刷新结果
+ */
+export const refreshMcpServerTools = async (name) => {
+ return apiAdminPost(`${BASE_URL}/${encodeURIComponent(name)}/tools/refresh`, {})
+}
+
+/**
+ * 切换单个工具的启用状态
+ * @param {string} serverName - 服务器名称
+ * @param {string} toolName - 工具名称
+ * @returns {Promise} - 切换结果
+ */
+export const toggleMcpServerTool = async (serverName, toolName) => {
+ return apiAdminPut(
+ `${BASE_URL}/${encodeURIComponent(serverName)}/tools/${encodeURIComponent(toolName)}/toggle`,
+ {}
+ )
+}
+
+// =============================================================================
+// === 导出为对象形式(兼容现有代码风格)===
+// =============================================================================
+
+export const mcpApi = {
+ getMcpServers,
+ getMcpServer,
+ createMcpServer,
+ updateMcpServer,
+ deleteMcpServer,
+ testMcpServer,
+ toggleMcpServer,
+ getMcpServerTools,
+ refreshMcpServerTools,
+ toggleMcpServerTool,
+}
+
+export default mcpApi
diff --git a/web/src/components/McpServerDetailModal.vue b/web/src/components/McpServerDetailModal.vue
new file mode 100644
index 00000000..febb4d82
--- /dev/null
+++ b/web/src/components/McpServerDetailModal.vue
@@ -0,0 +1,604 @@
+
+
+ 管理 MCP(Model Context Protocol)服务器配置。添加、编辑或删除 MCP 服务器以扩展 AI 的能力。
+ {{ JSON.stringify(server.headers, null, 2) }}
+ MCP 服务器管理
+ {{ server.name }}
+