From cec4ce84675f65f52e22dc3441430e1b613d6f31 Mon Sep 17 00:00:00 2001 From: Clhikari Date: Mon, 2 Mar 2026 18:09:02 +0800 Subject: [PATCH 1/5] fix: prevent crash on malformed MCP server config (#5666) --- astrbot/dashboard/routes/tools.py | 54 ++++++++++++++----- .../extension/McpServersSection.vue | 8 +++ 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index 333700410..9367d6302 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -40,6 +40,12 @@ async def get_mcp_servers(self): # 获取所有服务器并添加它们的工具列表 for name, server_config in config["mcpServers"].items(): + if not isinstance(server_config, dict): + logger.warning( + f"MCP 服务器 '{name}' 的配置无效(类型为 {type(server_config).__name__}),已跳过" + ) + continue + server_info = { "name": name, "active": server_config.get("active", True), @@ -87,10 +93,19 @@ async def add_mcp_server(self): for key, value in server_data.items(): if key not in ["name", "active", "tools", "errlogs"]: # 排除特殊字段 if key == "mcpServers": - key_0 = list(server_data["mcpServers"].keys())[ - 0 - ] # 不考虑为空的情况 - server_config = server_data["mcpServers"][key_0] + mcp_keys = list(server_data["mcpServers"].keys()) + if not mcp_keys: + return Response().error( + "mcpServers 配置不能为空" + ).__dict__ + key_0 = mcp_keys[0] + extracted = server_data["mcpServers"][key_0] + if not isinstance(extracted, dict): + return Response().error( + "mcpServers 配置格式不正确。请确保 mcpServers 内部的 key 是服务器名称," + "其值为包含 command/url 等字段的对象。" + ).__dict__ + server_config = extracted else: server_config[key] = value has_valid_config = True @@ -146,10 +161,12 @@ async def update_mcp_server(self): return Response().error(f"服务器 {name} 已存在").__dict__ # 获取活动状态 - active = server_data.get( - "active", - config["mcpServers"][old_name].get("active", True), - ) + old_config = config["mcpServers"][old_name] + if isinstance(old_config, dict): + old_active = old_config.get("active", True) + else: + old_active = True + active = server_data.get("active", old_active) # 创建新的配置对象 server_config = {"active": active} @@ -167,17 +184,26 @@ async def update_mcp_server(self): "oldName", ]: # 排除特殊字段 if key == "mcpServers": - key_0 = list(server_data["mcpServers"].keys())[ - 0 - ] # 不考虑为空的情况 - server_config = server_data["mcpServers"][key_0] + mcp_keys = list(server_data["mcpServers"].keys()) + if not mcp_keys: + return Response().error( + "mcpServers 配置不能为空" + ).__dict__ + key_0 = mcp_keys[0] + extracted = server_data["mcpServers"][key_0] + if not isinstance(extracted, dict): + return Response().error( + "mcpServers 配置格式不正确。请确保 mcpServers 内部的 key 是服务器名称," + "其值为包含 command/url 等字段的对象。" + ).__dict__ + server_config = extracted else: server_config[key] = value only_update_active = False # 如果只更新活动状态,保留原始配置 - if only_update_active: - for key, value in config["mcpServers"][old_name].items(): + if only_update_active and isinstance(old_config, dict): + for key, value in old_config.items(): if key != "active": # 除了active之外的所有字段都保留 server_config[key] = value diff --git a/dashboard/src/components/extension/McpServersSection.vue b/dashboard/src/components/extension/McpServersSection.vue index 95b679580..d24bcec58 100644 --- a/dashboard/src/components/extension/McpServersSection.vue +++ b/dashboard/src/components/extension/McpServersSection.vue @@ -300,6 +300,10 @@ export default { this.loadingGettingServers = true; axios.get('/api/tools/mcp/servers') .then(response => { + if (response.data.status === 'error') { + this.showError(response.data.message || this.tm('messages.getServersError', { error: 'Unknown error' })); + return; + } this.mcpServers = response.data.data || []; this.mcpServers.forEach(server => { if (!this.mcpServerUpdateLoaders[server.name]) { @@ -372,6 +376,10 @@ export default { axios.post(endpoint, serverData) .then(response => { this.loading = false; + if (response.data.status === 'error') { + this.showError(response.data.message || this.tm('messages.saveError', { error: 'Unknown error' })); + return; + } this.showMcpServerDialog = false; this.addServerDialogMessage = ''; this.getServers(); From a542f8d90e4a2b834be9af5dd5018358550483b0 Mon Sep 17 00:00:00 2001 From: Clhikari Date: Mon, 2 Mar 2026 18:09:02 +0800 Subject: [PATCH 2/5] fix: prevent crash on malformed MCP server config (#5666) --- astrbot/dashboard/routes/tools.py | 61 ++++++++++++++----- .../extension/McpServersSection.vue | 8 +++ 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index 333700410..e52634fb0 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -12,6 +12,27 @@ DEFAULT_MCP_CONFIG = {"mcpServers": {}} +def _extract_mcp_server_config(mcp_servers_value: object) -> dict | str: + """从用户提交的 mcpServers 字段中提取服务器配置。 + + Returns: + dict: 提取成功的服务器配置 + str: 错误信息 + """ + if not isinstance(mcp_servers_value, dict): + return "mcpServers 必须是一个 JSON 对象" + if not mcp_servers_value: + return "mcpServers 配置不能为空" + key_0 = next(iter(mcp_servers_value)) + extracted = mcp_servers_value[key_0] + if not isinstance(extracted, dict): + return ( + "mcpServers 配置格式不正确。请确保 mcpServers 内部的 key 是服务器名称," + "其值为包含 command/url 等字段的对象。" + ) + return extracted + + class ToolsRoute(Route): def __init__( self, @@ -40,6 +61,12 @@ async def get_mcp_servers(self): # 获取所有服务器并添加它们的工具列表 for name, server_config in config["mcpServers"].items(): + if not isinstance(server_config, dict): + logger.warning( + f"MCP 服务器 '{name}' 的配置无效(类型为 {type(server_config).__name__}),已跳过" + ) + continue + server_info = { "name": name, "active": server_config.get("active", True), @@ -87,10 +114,12 @@ async def add_mcp_server(self): for key, value in server_data.items(): if key not in ["name", "active", "tools", "errlogs"]: # 排除特殊字段 if key == "mcpServers": - key_0 = list(server_data["mcpServers"].keys())[ - 0 - ] # 不考虑为空的情况 - server_config = server_data["mcpServers"][key_0] + result = _extract_mcp_server_config( + server_data["mcpServers"] + ) + if isinstance(result, str): + return Response().error(result).__dict__ + server_config = result else: server_config[key] = value has_valid_config = True @@ -146,10 +175,12 @@ async def update_mcp_server(self): return Response().error(f"服务器 {name} 已存在").__dict__ # 获取活动状态 - active = server_data.get( - "active", - config["mcpServers"][old_name].get("active", True), - ) + old_config = config["mcpServers"][old_name] + if isinstance(old_config, dict): + old_active = old_config.get("active", True) + else: + old_active = True + active = server_data.get("active", old_active) # 创建新的配置对象 server_config = {"active": active} @@ -167,17 +198,19 @@ async def update_mcp_server(self): "oldName", ]: # 排除特殊字段 if key == "mcpServers": - key_0 = list(server_data["mcpServers"].keys())[ - 0 - ] # 不考虑为空的情况 - server_config = server_data["mcpServers"][key_0] + result = _extract_mcp_server_config( + server_data["mcpServers"] + ) + if isinstance(result, str): + return Response().error(result).__dict__ + server_config = result else: server_config[key] = value only_update_active = False # 如果只更新活动状态,保留原始配置 - if only_update_active: - for key, value in config["mcpServers"][old_name].items(): + if only_update_active and isinstance(old_config, dict): + for key, value in old_config.items(): if key != "active": # 除了active之外的所有字段都保留 server_config[key] = value diff --git a/dashboard/src/components/extension/McpServersSection.vue b/dashboard/src/components/extension/McpServersSection.vue index 95b679580..d24bcec58 100644 --- a/dashboard/src/components/extension/McpServersSection.vue +++ b/dashboard/src/components/extension/McpServersSection.vue @@ -300,6 +300,10 @@ export default { this.loadingGettingServers = true; axios.get('/api/tools/mcp/servers') .then(response => { + if (response.data.status === 'error') { + this.showError(response.data.message || this.tm('messages.getServersError', { error: 'Unknown error' })); + return; + } this.mcpServers = response.data.data || []; this.mcpServers.forEach(server => { if (!this.mcpServerUpdateLoaders[server.name]) { @@ -372,6 +376,10 @@ export default { axios.post(endpoint, serverData) .then(response => { this.loading = false; + if (response.data.status === 'error') { + this.showError(response.data.message || this.tm('messages.saveError', { error: 'Unknown error' })); + return; + } this.showMcpServerDialog = false; this.addServerDialogMessage = ''; this.getServers(); From 73cc494135270341b9d073b7bfb9af8c35e585c7 Mon Sep 17 00:00:00 2001 From: Clhikari Date: Tue, 3 Mar 2026 16:22:17 +0800 Subject: [PATCH 3/5] fix: validate MCP connection before persisting server config --- astrbot/dashboard/routes/tools.py | 81 ++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index e52634fb0..dcfb20bd4 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -12,21 +12,20 @@ DEFAULT_MCP_CONFIG = {"mcpServers": {}} -def _extract_mcp_server_config(mcp_servers_value: object) -> dict | str: +def _extract_mcp_server_config(mcp_servers_value: object) -> dict: """从用户提交的 mcpServers 字段中提取服务器配置。 - Returns: - dict: 提取成功的服务器配置 - str: 错误信息 + Raises: + ValueError: 配置不合法 """ if not isinstance(mcp_servers_value, dict): - return "mcpServers 必须是一个 JSON 对象" + raise ValueError("mcpServers 必须是一个 JSON 对象") if not mcp_servers_value: - return "mcpServers 配置不能为空" + raise ValueError("mcpServers 配置不能为空") key_0 = next(iter(mcp_servers_value)) extracted = mcp_servers_value[key_0] if not isinstance(extracted, dict): - return ( + raise ValueError( "mcpServers 配置格式不正确。请确保 mcpServers 内部的 key 是服务器名称," "其值为包含 command/url 等字段的对象。" ) @@ -114,12 +113,12 @@ async def add_mcp_server(self): for key, value in server_data.items(): if key not in ["name", "active", "tools", "errlogs"]: # 排除特殊字段 if key == "mcpServers": - result = _extract_mcp_server_config( - server_data["mcpServers"] - ) - if isinstance(result, str): - return Response().error(result).__dict__ - server_config = result + try: + server_config = _extract_mcp_server_config( + server_data["mcpServers"] + ) + except ValueError as e: + return Response().error(f"{e!s}").__dict__ else: server_config[key] = value has_valid_config = True @@ -132,8 +131,25 @@ async def add_mcp_server(self): if name in config["mcpServers"]: return Response().error(f"服务器 {name} 已存在").__dict__ + try: + await self.tool_mgr.test_mcp_server_connection(server_config) + except Exception as e: + logger.error(traceback.format_exc()) + return Response().error(f"测试 MCP 连接失败: {e!s}").__dict__ + config["mcpServers"][name] = server_config + def rollback_added_server() -> bool: + try: + rollback_config = self.tool_mgr.load_mcp_config() + if name in rollback_config["mcpServers"]: + rollback_config["mcpServers"].pop(name) + return self.tool_mgr.save_mcp_config(rollback_config) + return True + except Exception: + logger.error(traceback.format_exc()) + return False + if self.tool_mgr.save_mcp_config(config): try: await self.tool_mgr.enable_mcp_server( @@ -142,12 +158,18 @@ async def add_mcp_server(self): timeout=30, ) except TimeoutError: - return Response().error(f"启用 MCP 服务器 {name} 超时。").__dict__ + rollback_ok = rollback_added_server() + err_msg = f"启用 MCP 服务器 {name} 超时。" + if not rollback_ok: + err_msg += " 配置回滚失败,请手动检查配置。" + return Response().error(err_msg).__dict__ except Exception as e: logger.error(traceback.format_exc()) - return ( - Response().error(f"启用 MCP 服务器 {name} 失败: {e!s}").__dict__ - ) + rollback_ok = rollback_added_server() + err_msg = f"启用 MCP 服务器 {name} 失败: {e!s}" + if not rollback_ok: + err_msg += " 配置回滚失败,请手动检查配置。" + return Response().error(err_msg).__dict__ return Response().ok(None, f"成功添加 MCP 服务器 {name}").__dict__ return Response().error("保存配置失败").__dict__ except Exception as e: @@ -198,12 +220,12 @@ async def update_mcp_server(self): "oldName", ]: # 排除特殊字段 if key == "mcpServers": - result = _extract_mcp_server_config( - server_data["mcpServers"] - ) - if isinstance(result, str): - return Response().error(result).__dict__ - server_config = result + try: + server_config = _extract_mcp_server_config( + server_data["mcpServers"] + ) + except ValueError as e: + return Response().error(f"{e!s}").__dict__ else: server_config[key] = value only_update_active = False @@ -335,12 +357,15 @@ async def test_mcp_connection(self): return Response().error("无效的 MCP 服务器配置").__dict__ if "mcpServers" in config: - keys = list(config["mcpServers"].keys()) - if not keys: - return Response().error("MCP 服务器配置不能为空").__dict__ - if len(keys) > 1: + mcp_servers = config["mcpServers"] + if isinstance(mcp_servers, dict) and len(mcp_servers) > 1: return Response().error("一次只能配置一个 MCP 服务器配置").__dict__ - config = config["mcpServers"][keys[0]] + try: + config = _extract_mcp_server_config(mcp_servers) + except ValueError as e: + if str(e) == "mcpServers 配置不能为空": + return Response().error("MCP 服务器配置不能为空").__dict__ + return Response().error(f"{e!s}").__dict__ elif not config: return Response().error("MCP 服务器配置不能为空").__dict__ From 4392e3c65eea0fcb5d7dfff20d184b0395ccd132 Mon Sep 17 00:00:00 2001 From: Clhikari Date: Tue, 3 Mar 2026 17:26:35 +0800 Subject: [PATCH 4/5] fix: guard mcpServers type before iterating server list --- astrbot/dashboard/routes/tools.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index dcfb20bd4..bbef82b27 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -57,9 +57,16 @@ async def get_mcp_servers(self): try: config = self.tool_mgr.load_mcp_config() servers = [] + mcp_servers = config.get("mcpServers", {}) + + if not isinstance(mcp_servers, dict): + logger.warning( + f"MCP 服务器配置无效(类型为 {type(mcp_servers).__name__}),应为对象/字典类型,已跳过所有 MCP 服务器" + ) + mcp_servers = {} # 获取所有服务器并添加它们的工具列表 - for name, server_config in config["mcpServers"].items(): + for name, server_config in mcp_servers.items(): if not isinstance(server_config, dict): logger.warning( f"MCP 服务器 '{name}' 的配置无效(类型为 {type(server_config).__name__}),已跳过" From 8ac19934dfa9636465ba35fb6201adb8bc24ce3f Mon Sep 17 00:00:00 2001 From: Clhikari Date: Tue, 3 Mar 2026 17:54:00 +0800 Subject: [PATCH 5/5] refactor: use typed empty-config error and extract MCP rollback helper --- astrbot/dashboard/routes/tools.py | 38 ++++++++++++++++++------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index bbef82b27..456385b48 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -12,6 +12,12 @@ DEFAULT_MCP_CONFIG = {"mcpServers": {}} +class EmptyMcpServersError(ValueError): + """mcpServers 为空时抛出""" + + pass + + def _extract_mcp_server_config(mcp_servers_value: object) -> dict: """从用户提交的 mcpServers 字段中提取服务器配置。 @@ -21,7 +27,7 @@ def _extract_mcp_server_config(mcp_servers_value: object) -> dict: if not isinstance(mcp_servers_value, dict): raise ValueError("mcpServers 必须是一个 JSON 对象") if not mcp_servers_value: - raise ValueError("mcpServers 配置不能为空") + raise EmptyMcpServersError("mcpServers 配置不能为空") key_0 = next(iter(mcp_servers_value)) extracted = mcp_servers_value[key_0] if not isinstance(extracted, dict): @@ -53,6 +59,17 @@ def __init__( self.register_routes() self.tool_mgr = self.core_lifecycle.provider_manager.llm_tools + def _rollback_mcp_server(self, name: str) -> bool: + try: + rollback_config = self.tool_mgr.load_mcp_config() + if name in rollback_config["mcpServers"]: + rollback_config["mcpServers"].pop(name) + return self.tool_mgr.save_mcp_config(rollback_config) + return True + except Exception: + logger.error(traceback.format_exc()) + return False + async def get_mcp_servers(self): try: config = self.tool_mgr.load_mcp_config() @@ -146,17 +163,6 @@ async def add_mcp_server(self): config["mcpServers"][name] = server_config - def rollback_added_server() -> bool: - try: - rollback_config = self.tool_mgr.load_mcp_config() - if name in rollback_config["mcpServers"]: - rollback_config["mcpServers"].pop(name) - return self.tool_mgr.save_mcp_config(rollback_config) - return True - except Exception: - logger.error(traceback.format_exc()) - return False - if self.tool_mgr.save_mcp_config(config): try: await self.tool_mgr.enable_mcp_server( @@ -165,14 +171,14 @@ def rollback_added_server() -> bool: timeout=30, ) except TimeoutError: - rollback_ok = rollback_added_server() + rollback_ok = self._rollback_mcp_server(name) err_msg = f"启用 MCP 服务器 {name} 超时。" if not rollback_ok: err_msg += " 配置回滚失败,请手动检查配置。" return Response().error(err_msg).__dict__ except Exception as e: logger.error(traceback.format_exc()) - rollback_ok = rollback_added_server() + rollback_ok = self._rollback_mcp_server(name) err_msg = f"启用 MCP 服务器 {name} 失败: {e!s}" if not rollback_ok: err_msg += " 配置回滚失败,请手动检查配置。" @@ -369,9 +375,9 @@ async def test_mcp_connection(self): return Response().error("一次只能配置一个 MCP 服务器配置").__dict__ try: config = _extract_mcp_server_config(mcp_servers) + except EmptyMcpServersError: + return Response().error("MCP 服务器配置不能为空").__dict__ except ValueError as e: - if str(e) == "mcpServers 配置不能为空": - return Response().error("MCP 服务器配置不能为空").__dict__ return Response().error(f"{e!s}").__dict__ elif not config: return Response().error("MCP 服务器配置不能为空").__dict__