From 2f8d921adff58b989b8559a0638cae973f6c8d6f Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:24:07 +0800 Subject: [PATCH] feat: add support to sync mcp servers from ModelScope (#2313) --- astrbot/core/provider/func_tool_manager.py | 95 +++++++++++++ astrbot/dashboard/routes/tools.py | 64 ++++----- .../i18n/locales/en-US/features/tool-use.json | 6 +- .../i18n/locales/zh-CN/features/tool-use.json | 44 +++++- dashboard/src/views/ToolUsePage.vue | 131 ++++++++++++++++-- 5 files changed, 284 insertions(+), 56 deletions(-) diff --git a/astrbot/core/provider/func_tool_manager.py b/astrbot/core/provider/func_tool_manager.py index 117a03800..a7a3d14b3 100644 --- a/astrbot/core/provider/func_tool_manager.py +++ b/astrbot/core/provider/func_tool_manager.py @@ -3,6 +3,7 @@ import json import os import asyncio import logging +import aiohttp from datetime import timedelta from deprecated import deprecated @@ -871,6 +872,100 @@ class FunctionToolManager: return True return False + @property + def mcp_config_path(self): + data_dir = get_astrbot_data_path() + return os.path.join(data_dir, "mcp_server.json") + + def load_mcp_config(self): + if not os.path.exists(self.mcp_config_path): + # 配置文件不存在,创建默认配置 + os.makedirs(os.path.dirname(self.mcp_config_path), exist_ok=True) + with open(self.mcp_config_path, "w", encoding="utf-8") as f: + json.dump(DEFAULT_MCP_CONFIG, f, ensure_ascii=False, indent=4) + return DEFAULT_MCP_CONFIG + + try: + with open(self.mcp_config_path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + logger.error(f"加载 MCP 配置失败: {e}") + return DEFAULT_MCP_CONFIG + + def save_mcp_config(self, config: dict): + try: + with open(self.mcp_config_path, "w", encoding="utf-8") as f: + json.dump(config, f, ensure_ascii=False, indent=4) + return True + except Exception as e: + logger.error(f"保存 MCP 配置失败: {e}") + return False + + async def sync_modelscope_mcp_servers(self, access_token: str) -> None: + """从 ModelScope 平台同步 MCP 服务器配置""" + base_url = "https://www.modelscope.cn/openapi/v1" + url = f"{base_url}/mcp/servers/operational" + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + } + + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers) as response: + if response.status == 200: + data = await response.json() + mcp_server_list = data.get("data", {}).get( + "mcp_server_list", [] + ) + local_mcp_config = self.load_mcp_config() + + synced_count = 0 + for server in mcp_server_list: + server_name = server["name"] + operational_urls = server.get("operational_urls", []) + if not operational_urls: + continue + url_info = operational_urls[0] + server_url = url_info.get("url") + if not server_url: + continue + # 添加到配置中(同名会覆盖) + local_mcp_config["mcpServers"][server_name] = { + "url": server_url, + "transport": "sse", + "active": True, + "provider": "modelscope", + } + synced_count += 1 + + if synced_count > 0: + self.save_mcp_config(local_mcp_config) + tasks = [] + for server in mcp_server_list: + name = server["name"] + tasks.append( + self.enable_mcp_server( + name=name, + config=local_mcp_config["mcpServers"][name], + ) + ) + await asyncio.gather(*tasks) + logger.info( + f"从 ModelScope 同步了 {synced_count} 个 MCP 服务器" + ) + else: + logger.warning("没有找到可用的 ModelScope MCP 服务器") + else: + raise Exception( + f"ModelScope API 请求失败: HTTP {response.status}" + ) + + except aiohttp.ClientError as e: + raise Exception(f"网络连接错误: {str(e)}") + except Exception as e: + raise Exception(f"同步 ModelScope MCP 服务器时发生错误: {str(e)}") + def __str__(self): return str(self.func_list) diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index 324fc62d3..b35f977f5 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -1,5 +1,3 @@ -import json -import os import traceback import aiohttp @@ -7,7 +5,6 @@ from quart import request from astrbot.core import logger from astrbot.core.core_lifecycle import AstrBotCoreLifecycle -from astrbot.core.utils.astrbot_path import get_astrbot_data_path from astrbot.core.star import star_map from .route import Response, Route, RouteContext @@ -30,42 +27,14 @@ class ToolsRoute(Route): "/tools/mcp/test": ("POST", self.test_mcp_connection), "/tools/list": ("GET", self.get_tool_list), "/tools/toggle-tool": ("POST", self.toggle_tool), + "/tools/mcp/sync-provider": ("POST", self.sync_provider), } self.register_routes() self.tool_mgr = self.core_lifecycle.provider_manager.llm_tools - @property - def mcp_config_path(self): - data_dir = get_astrbot_data_path() - return os.path.join(data_dir, "mcp_server.json") - - def load_mcp_config(self): - if not os.path.exists(self.mcp_config_path): - # 配置文件不存在,创建默认配置 - os.makedirs(os.path.dirname(self.mcp_config_path), exist_ok=True) - with open(self.mcp_config_path, "w", encoding="utf-8") as f: - json.dump(DEFAULT_MCP_CONFIG, f, ensure_ascii=False, indent=4) - return DEFAULT_MCP_CONFIG - - try: - with open(self.mcp_config_path, "r", encoding="utf-8") as f: - return json.load(f) - except Exception as e: - logger.error(f"加载 MCP 配置失败: {e}") - return DEFAULT_MCP_CONFIG - - def save_mcp_config(self, config): - try: - with open(self.mcp_config_path, "w", encoding="utf-8") as f: - json.dump(config, f, ensure_ascii=False, indent=4) - return True - except Exception as e: - logger.error(f"保存 MCP 配置失败: {e}") - return False - async def get_mcp_servers(self): try: - config = self.load_mcp_config() + config = self.tool_mgr.load_mcp_config() servers = [] # 获取所有服务器并添加它们的工具列表 @@ -128,14 +97,14 @@ class ToolsRoute(Route): if not has_valid_config: return Response().error("必须提供有效的服务器配置").__dict__ - config = self.load_mcp_config() + config = self.tool_mgr.load_mcp_config() if name in config["mcpServers"]: return Response().error(f"服务器 {name} 已存在").__dict__ config["mcpServers"][name] = server_config - if self.save_mcp_config(config): + if self.tool_mgr.save_mcp_config(config): try: await self.tool_mgr.enable_mcp_server( name, server_config, timeout=30 @@ -165,7 +134,7 @@ class ToolsRoute(Route): if not name: return Response().error("服务器名称不能为空").__dict__ - config = self.load_mcp_config() + config = self.tool_mgr.load_mcp_config() if name not in config["mcpServers"]: return Response().error(f"服务器 {name} 不存在").__dict__ @@ -201,7 +170,7 @@ class ToolsRoute(Route): config["mcpServers"][name] = server_config - if self.save_mcp_config(config): + if self.tool_mgr.save_mcp_config(config): # 处理MCP客户端状态变化 if active: if name in self.tool_mgr.mcp_client_dict or not only_update_active: @@ -269,14 +238,14 @@ class ToolsRoute(Route): if not name: return Response().error("服务器名称不能为空").__dict__ - config = self.load_mcp_config() + config = self.tool_mgr.load_mcp_config() if name not in config["mcpServers"]: return Response().error(f"服务器 {name} 不存在").__dict__ del config["mcpServers"][name] - if self.save_mcp_config(config): + if self.tool_mgr.save_mcp_config(config): if name in self.tool_mgr.mcp_client_dict: try: await self.tool_mgr.disable_mcp_server(name, timeout=10) @@ -376,3 +345,20 @@ class ToolsRoute(Route): except Exception as e: logger.error(traceback.format_exc()) return Response().error(f"操作工具失败: {str(e)}").__dict__ + + async def sync_provider(self): + """同步 MCP 提供者配置""" + try: + data = await request.json + provider_name = data.get("name") # modelscope, or others + match provider_name: + case "modelscope": + access_token = data.get("access_token", "") + await self.tool_mgr.sync_modelscope_mcp_servers(access_token) + case _: + return Response().error(f"未知: {provider_name}").__dict__ + + return Response().ok(message="同步成功").__dict__ + except Exception as e: + logger.error(traceback.format_exc()) + return Response().error(f"同步失败: {str(e)}").__dict__ diff --git a/dashboard/src/i18n/locales/en-US/features/tool-use.json b/dashboard/src/i18n/locales/en-US/features/tool-use.json index 1c4c54e97..4f66d564e 100644 --- a/dashboard/src/i18n/locales/en-US/features/tool-use.json +++ b/dashboard/src/i18n/locales/en-US/features/tool-use.json @@ -17,7 +17,8 @@ "add": "Add Server", "useTemplateStdio": "Stdio Template", "useTemplateStreamableHttp": "Streamable HTTP Template", - "useTemplateSse": "SSE Template" + "useTemplateSse": "SSE Template", + "sync": "Sync MCP Servers" }, "empty": "No MCP servers available, click Add Server to add one", "status": { @@ -77,7 +78,8 @@ "buttons": { "cancel": "Cancel", "save": "Save", - "testConnection": "Test Connection" + "testConnection": "Test Connection", + "sync": "Sync" } }, "serverDetail": { diff --git a/dashboard/src/i18n/locales/zh-CN/features/tool-use.json b/dashboard/src/i18n/locales/zh-CN/features/tool-use.json index 663488497..dfd77f0a1 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/tool-use.json +++ b/dashboard/src/i18n/locales/zh-CN/features/tool-use.json @@ -17,7 +17,8 @@ "add": "新增服务器", "useTemplateStdio": "Stdio 模板", "useTemplateStreamableHttp": "Streamable HTTP 模板", - "useTemplateSse": "SSE 模板" + "useTemplateSse": "SSE 模板", + "sync": "同步服务器" }, "empty": "暂无 MCP 服务器,点击 新增服务器 添加", "status": { @@ -77,7 +78,8 @@ "buttons": { "cancel": "取消", "save": "保存", - "testConnection": "测试连接" + "testConnection": "测试连接", + "sync": "同步" } }, "serverDetail": { @@ -89,7 +91,43 @@ "importConfig": "导入配置" } }, - "confirmDelete": "确定要删除服务器 {name} 吗?" + "confirmDelete": "确定要删除服务器 {name} 吗?", + "syncProvider": { + "title": "同步 MCP 服务器", + "subtitle": "从提供商同步 MCP 服务器配置到本地", + "steps": { + "selectProvider": "步骤 1: 选择提供商", + "configureAuth": "步骤 2: 配置认证", + "syncServers": "步骤 3: 同步服务器" + }, + "providers": { + "modelscope": "ModelScope", + "description": "ModelScope 是一个开源的模型社区,提供各种机器学习和AI服务的MCP服务器" + }, + "fields": { + "provider": "选择提供商", + "accessToken": "访问令牌", + "tokenRequired": "访问令牌是必填项", + "tokenHint": "请输入您的 ModelScope 访问令牌" + }, + "buttons": { + "cancel": "取消", + "previous": "上一步", + "next": "下一步", + "sync": "开始同步", + "getToken": "获取令牌" + }, + "status": { + "selectProvider": "请选择一个 MCP 服务器提供商", + "enterToken": "请输入访问令牌以继续", + "readyToSync": "准备同步服务器配置" + }, + "messages": { + "syncSuccess": "MCP 服务器同步成功!", + "syncError": "同步失败: {error}", + "tokenHelp": "如何获取 ModelScope 访问令牌?点击右侧按钮查看说明" + } + } }, "messages": { "getServersError": "获取 MCP 服务器列表失败: {error}", diff --git a/dashboard/src/views/ToolUsePage.vue b/dashboard/src/views/ToolUsePage.vue index c89634153..348a05db9 100644 --- a/dashboard/src/views/ToolUsePage.vue +++ b/dashboard/src/views/ToolUsePage.vue @@ -25,10 +25,14 @@ rounded="xl" size="x-large"> {{ tm('functionTools.buttons.view') }}({{ tools.length }}) - + {{ tm('mcpServers.buttons.add') }} + + {{ tm('mcpServers.buttons.sync') }} + @@ -59,8 +63,8 @@ - +