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 @@ + + + + + diff --git a/web/src/components/McpServersComponent.vue b/web/src/components/McpServersComponent.vue new file mode 100644 index 00000000..2f08b5a4 --- /dev/null +++ b/web/src/components/McpServersComponent.vue @@ -0,0 +1,734 @@ + + + + + diff --git a/web/src/components/SettingsModal.vue b/web/src/components/SettingsModal.vue index 65ef19d3..4cb458fb 100644 --- a/web/src/components/SettingsModal.vue +++ b/web/src/components/SettingsModal.vue @@ -40,6 +40,15 @@ 用户管理 +
+ + MCP 管理 +
@@ -68,6 +77,14 @@ > 用户管理 + @@ -84,6 +101,10 @@
+ +
+ +
@@ -96,11 +117,13 @@ import { useUserStore } from '@/stores/user' import { SettingOutlined, CodeOutlined, - UserOutlined + UserOutlined, + ApiOutlined } from '@ant-design/icons-vue' import BasicSettingsSection from '@/components/BasicSettingsSection.vue' import ModelProvidersComponent from '@/components/ModelProvidersComponent.vue' import UserManagementComponent from '@/components/UserManagementComponent.vue' +import McpServersComponent from '@/components/McpServersComponent.vue' const props = defineProps({ visible: {