diff --git a/astrbot/core/agent/mcp_client.py b/astrbot/core/agent/mcp_client.py index 96312abc2..18f4d47e0 100644 --- a/astrbot/core/agent/mcp_client.py +++ b/astrbot/core/agent/mcp_client.py @@ -15,7 +15,6 @@ from tenacity import ( from astrbot import logger from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.utils.log_pipe import LogPipe -from astrbot.dashboard.shared import MCP_STDIO_COMMAND_NOT_FOUND from .run_context import TContext from .tool import FunctionTool @@ -37,44 +36,6 @@ except (ModuleNotFoundError, ImportError): ) -class MCPStdioCommandNotFoundError(Exception): - """Raised when the configured stdio MCP command cannot be started.""" - - code = MCP_STDIO_COMMAND_NOT_FOUND - - def __init__( - self, command: str | None, original_error: Exception | None = None - ) -> None: - self.command = str(command or "").strip() or "" - self.raw_error = str(original_error).strip() if original_error else "" - super().__init__( - _build_missing_stdio_command_message(self.command, self.raw_error or None) - ) - - def to_response_data(self) -> dict: - return { - "error": { - "code": self.code, - "command": self.command, - "raw_error": self.raw_error, - } - } - - -def _build_missing_stdio_command_message( - command: str | None, original_error: str | None = None -) -> str: - normalized_command = str(command or "").strip() or "" - sections = [ - "Unable to start the MCP stdio server", - f"Command '{normalized_command}' was not found.", - "Install the command, or set 'command' to the full executable path and ensure it is available in PATH.", - ] - if original_error is not None: - sections.append(f"Original error: {original_error!s}") - return "\n\n".join(sections) - - def _prepare_config(config: dict) -> dict: """Prepare configuration, handle nested format""" if config.get("mcpServers"): @@ -257,20 +218,17 @@ class MCPClient: # Handle MCP service error logs self.server_errlogs.append(msg) - try: - stdio_transport = await self.exit_stack.enter_async_context( - mcp.stdio_client( - server_params, - errlog=LogPipe( - level=logging.ERROR, - logger=logger, - identifier=f"MCPServer-{name}", - callback=callback, - ), # type: ignore - ), - ) - except FileNotFoundError as exc: - raise MCPStdioCommandNotFoundError(cfg.get("command"), exc) from exc + stdio_transport = await self.exit_stack.enter_async_context( + mcp.stdio_client( + server_params, + errlog=LogPipe( + level=logging.ERROR, + logger=logger, + identifier=f"MCPServer-{name}", + callback=callback, + ), # type: ignore + ), + ) # Create a new client session self.session = await self.exit_stack.enter_async_context( diff --git a/astrbot/dashboard/routes/route.py b/astrbot/dashboard/routes/route.py index ff3ffc479..53c623443 100644 --- a/astrbot/dashboard/routes/route.py +++ b/astrbot/dashboard/routes/route.py @@ -45,10 +45,9 @@ class Response: message: str | None = None data: dict | list | None = None - def error(self, message: str, data: dict | list | None = None): + def error(self, message: str): self.status = "error" self.message = message - self.data = data return self def ok(self, data: dict | list | None = None, message: str | None = None): diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index c4d1af168..84f8dcc6d 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -3,18 +3,13 @@ import traceback from quart import request from astrbot.core import logger -from astrbot.core.agent.mcp_client import MCPStdioCommandNotFoundError, MCPTool +from astrbot.core.agent.mcp_client import MCPTool from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.star import star_map -from astrbot.dashboard.shared import MCP_TEST_CONNECTION_FAILED from .route import Response, Route, RouteContext DEFAULT_MCP_CONFIG = {"mcpServers": {}} -MCP_TEST_CONNECTION_ERROR_MESSAGE = "Unable to test the MCP connection." -MCP_TEST_CONNECTION_DETAILS_MESSAGE = ( - "Unable to test the MCP connection. Review the details below." -) class EmptyMcpServersError(ValueError): @@ -75,32 +70,6 @@ class ToolsRoute(Route): logger.error(traceback.format_exc()) return False - def _build_mcp_test_error_response(self, error: Exception) -> dict: - if isinstance(error, MCPStdioCommandNotFoundError): - return ( - Response() - .error( - MCP_TEST_CONNECTION_DETAILS_MESSAGE, - error.to_response_data(), - ) - .__dict__ - ) - - msg = str(error).strip() or MCP_TEST_CONNECTION_ERROR_MESSAGE - return ( - Response() - .error( - MCP_TEST_CONNECTION_ERROR_MESSAGE, - { - "error": { - "code": MCP_TEST_CONNECTION_FAILED, - "detail": msg, - } - }, - ) - .__dict__ - ) - async def get_mcp_servers(self): try: config = self.tool_mgr.load_mcp_config() @@ -454,7 +423,7 @@ class ToolsRoute(Route): except Exception as e: logger.error(traceback.format_exc()) - return self._build_mcp_test_error_response(e) + return Response().error(f"Failed to test MCP connection: {e!s}").__dict__ async def get_tool_list(self): """Get all registered tools.""" diff --git a/astrbot/dashboard/shared/__init__.py b/astrbot/dashboard/shared/__init__.py deleted file mode 100644 index 557127ff3..000000000 --- a/astrbot/dashboard/shared/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .mcp_test_connection_error_codes import ( - MCP_STDIO_COMMAND_NOT_FOUND, - MCP_TEST_CONNECTION_FAILED, -) - -__all__ = [ - "MCP_STDIO_COMMAND_NOT_FOUND", - "MCP_TEST_CONNECTION_FAILED", -] diff --git a/astrbot/dashboard/shared/mcp_test_connection_error_codes.json b/astrbot/dashboard/shared/mcp_test_connection_error_codes.json deleted file mode 100644 index 8bed40f8d..000000000 --- a/astrbot/dashboard/shared/mcp_test_connection_error_codes.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "MCP_STDIO_COMMAND_NOT_FOUND": "mcp_stdio_command_not_found", - "MCP_TEST_CONNECTION_FAILED": "mcp_test_connection_failed" -} diff --git a/astrbot/dashboard/shared/mcp_test_connection_error_codes.py b/astrbot/dashboard/shared/mcp_test_connection_error_codes.py deleted file mode 100644 index 4ce3de4f9..000000000 --- a/astrbot/dashboard/shared/mcp_test_connection_error_codes.py +++ /dev/null @@ -1,8 +0,0 @@ -import json -from pathlib import Path - -_ERROR_CODES_PATH = Path(__file__).with_name("mcp_test_connection_error_codes.json") -_ERROR_CODES = json.loads(_ERROR_CODES_PATH.read_text(encoding="utf-8")) - -MCP_STDIO_COMMAND_NOT_FOUND = _ERROR_CODES["MCP_STDIO_COMMAND_NOT_FOUND"] -MCP_TEST_CONNECTION_FAILED = _ERROR_CODES["MCP_TEST_CONNECTION_FAILED"] diff --git a/dashboard/src/components/extension/McpServersSection.vue b/dashboard/src/components/extension/McpServersSection.vue index 244cae634..d24bcec58 100644 --- a/dashboard/src/components/extension/McpServersSection.vue +++ b/dashboard/src/components/extension/McpServersSection.vue @@ -129,30 +129,9 @@ - -
- {{ addServerDialogFeedback.title }} -
-
- {{ detail }} -
-
- {{ tm('dialogs.addServer.feedback.rawError') }} {{ addServerDialogFeedback.rawError }} -
-
+
+ {{ addServerDialogMessage }} +
@@ -238,7 +217,6 @@ import axios from 'axios'; import { VueMonacoEditor } from '@guolao/vue-monaco-editor'; import ItemCard from '@/components/shared/ItemCard.vue'; -import { MCP_TEST_CONNECTION_ERROR_CODES } from '@/constants/mcpTestConnectionErrorCodes'; import { useI18n, useModuleI18n } from '@/i18n/composables'; import { askForConfirmation as askForConfirmationDialog, @@ -266,7 +244,7 @@ export default { mcpServerProviderList: ['modelscope'], mcpProviderToken: '', showSyncMcpServerDialog: false, - addServerDialogFeedback: null, + addServerDialogMessage: '', loading: false, loadingGettingServers: false, mcpServerUpdateLoaders: {}, @@ -403,7 +381,7 @@ export default { return; } this.showMcpServerDialog = false; - this.clearAddServerDialogFeedback(); + this.addServerDialogMessage = ''; this.getServers(); this.showSuccess(response.data.message || this.tm('messages.saveSuccess')); this.resetForm(); @@ -467,73 +445,14 @@ export default { }, closeServerDialog() { this.showMcpServerDialog = false; - this.clearAddServerDialogFeedback(); + this.addServerDialogMessage = ''; this.resetForm(); }, - setAddServerDialogFeedback(feedback = null) { - this.addServerDialogFeedback = feedback; - }, - clearAddServerDialogFeedback() { - this.setAddServerDialogFeedback(null); - }, - buildAddServerDialogErrorFeedback(message, errorData = null) { - const normalizedMessage = String(message || '').trim(); - if (errorData?.code === MCP_TEST_CONNECTION_ERROR_CODES.STDIO_COMMAND_NOT_FOUND) { - return { - type: 'error', - icon: 'mdi-alert-circle', - title: this.tm('dialogs.addServer.feedback.stdioCommandNotFound.title'), - details: [ - this.tm('dialogs.addServer.feedback.stdioCommandNotFound.reason', { - command: errorData.command || '' - }), - this.tm('dialogs.addServer.feedback.stdioCommandNotFound.action') - ], - rawError: errorData.raw_error || '' - }; - } - if (errorData?.code === MCP_TEST_CONNECTION_ERROR_CODES.TEST_CONNECTION_FAILED) { - const detail = String(errorData.detail || '').trim(); - return { - type: 'error', - icon: 'mdi-alert-circle', - title: this.tm('dialogs.addServer.feedback.errorTitle'), - details: [detail || this.tm('dialogs.addServer.feedback.genericDetail')], - rawError: errorData.raw_error || '' - }; - } - return { - type: 'error', - icon: 'mdi-alert-circle', - title: this.tm('dialogs.addServer.feedback.errorTitle'), - details: [ - normalizedMessage || this.tm('dialogs.addServer.feedback.genericDetail') - ], - rawError: errorData?.raw_error || '' - }; - }, - buildAddServerDialogSuccessFeedback(message, tools = '') { - const details = []; - if (message) { - details.push(message); - } - if (tools) { - details.push(this.tm('dialogs.addServer.feedback.availableTools', { tools })); - } - return { - type: 'success', - icon: 'mdi-check-circle', - title: this.tm('dialogs.addServer.feedback.successTitle'), - details, - rawError: '' - }; - }, testServerConnection() { if (!this.validateJson()) { return; } this.loading = true; - this.clearAddServerDialogFeedback(); let configObj; try { configObj = JSON.parse(this.serverConfigJson); @@ -547,33 +466,11 @@ export default { }) .then(response => { this.loading = false; - if (response.data.status === 'error') { - this.showError( - response.data.message || this.tm('dialogs.addServer.feedback.genericDetail'), - { - inlineDialog: true, - errorData: response.data.data?.error - } - ); - return; - } - - const tools = Array.isArray(response.data.data) - ? response.data.data.join(', ') - : response.data.data; - this.setAddServerDialogFeedback( - this.buildAddServerDialogSuccessFeedback(response.data.message, tools) - ); + this.addServerDialogMessage = `${response.data.message} (tools: ${response.data.data})`; }) .catch(error => { this.loading = false; - this.showError( - error.response?.data?.message || error.message || this.tm('dialogs.addServer.feedback.genericDetail'), - { - inlineDialog: true, - errorData: error.response?.data?.data?.error - } - ); + this.showError(this.tm('messages.testError', { error: error.response?.data?.message || error.message })); }); }, resetForm() { @@ -592,13 +489,7 @@ export default { this.save_message_success = 'success'; this.save_message_snack = true; }, - showError(message, options = {}) { - if (options.inlineDialog) { - this.setAddServerDialogFeedback( - this.buildAddServerDialogErrorFeedback(message, options.errorData) - ); - return; - } + showError(message) { this.save_message = message; this.save_message_success = 'error'; this.save_message_snack = true; diff --git a/dashboard/src/constants/mcpTestConnectionErrorCodes.js b/dashboard/src/constants/mcpTestConnectionErrorCodes.js deleted file mode 100644 index 3f3585fb8..000000000 --- a/dashboard/src/constants/mcpTestConnectionErrorCodes.js +++ /dev/null @@ -1,6 +0,0 @@ -import errorCodes from '@dashboard-shared/mcp_test_connection_error_codes.json'; - -export const MCP_TEST_CONNECTION_ERROR_CODES = { - STDIO_COMMAND_NOT_FOUND: errorCodes.MCP_STDIO_COMMAND_NOT_FOUND, - TEST_CONNECTION_FAILED: errorCodes.MCP_TEST_CONNECTION_FAILED -}; 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 cf8a2062a..2c68b8243 100644 --- a/dashboard/src/i18n/locales/en-US/features/tool-use.json +++ b/dashboard/src/i18n/locales/en-US/features/tool-use.json @@ -86,18 +86,6 @@ }, "tips": { "timeoutConfig": "Please configure tool call timeout separately in the configuration page" - }, - "feedback": { - "successTitle": "Connection test passed", - "errorTitle": "Connection test failed", - "genericDetail": "The connection test did not complete successfully.", - "availableTools": "Available tools: {tools}", - "rawError": "Original error:", - "stdioCommandNotFound": { - "title": "Unable to start the MCP stdio server", - "reason": "Command '{command}' was not found.", - "action": "Install the command, or set 'command' to the full executable path and ensure it is available in PATH." - } } }, "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 7f6a14528..f6e6c4407 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/tool-use.json +++ b/dashboard/src/i18n/locales/zh-CN/features/tool-use.json @@ -86,18 +86,6 @@ }, "tips": { "timeoutConfig": "工具调用的超时时间请前往配置页面单独配置" - }, - "feedback": { - "successTitle": "连接测试通过", - "errorTitle": "连接测试失败", - "genericDetail": "连接测试未能成功完成。", - "availableTools": "可用工具:{tools}", - "rawError": "原始错误:", - "stdioCommandNotFound": { - "title": "无法启动 MCP stdio 服务", - "reason": "未找到命令 “{command}”。", - "action": "请先安装该命令,或将 command 修改为本机可执行文件的完整路径,并确认它已加入 PATH。" - } } }, "serverDetail": { @@ -168,4 +156,4 @@ "toggleToolError": "工具状态切换失败: {error}", "testError": "测试连接失败: {error}" } -} +} \ No newline at end of file diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index 2d6300191..b53e0310d 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -20,10 +20,7 @@ export default defineConfig({ resolve: { alias: { mermaid: 'mermaid/dist/mermaid.js', - '@': fileURLToPath(new URL('./src', import.meta.url)), - '@dashboard-shared': fileURLToPath( - new URL('../astrbot/dashboard/shared', import.meta.url) - ) + '@': fileURLToPath(new URL('./src', import.meta.url)) } }, css: { diff --git a/pyproject.toml b/pyproject.toml index cdab52110..6c441f89a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,7 @@ allow-direct-references = true # Include bundled dashboard dist even though it is not tracked by VCS. [tool.hatch.build.targets.wheel] -artifacts = ["astrbot/dashboard/dist/**", "astrbot/dashboard/shared/*.json"] +artifacts = ["astrbot/dashboard/dist/**"] # Custom build hook: builds the Vue dashboard and copies dist into the package. [tool.hatch.build.hooks.custom] diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index f655c9c51..ce28316af 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -3,7 +3,6 @@ import io import os import sys import zipfile -from contextlib import asynccontextmanager from datetime import datetime from types import SimpleNamespace @@ -17,11 +16,6 @@ from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.db.sqlite import SQLiteDatabase from astrbot.core.star.star import star_registry from astrbot.core.star.star_handler import star_handlers_registry -from astrbot.core.agent import mcp_client as mcp_client_module -from astrbot.dashboard.shared import ( - MCP_STDIO_COMMAND_NOT_FOUND, - MCP_TEST_CONNECTION_FAILED, -) from astrbot.dashboard.routes.plugin import PluginRoute from astrbot.dashboard.server import AstrBotDashboard from tests.fixtures.helpers import ( @@ -276,89 +270,10 @@ async def test_commands_api(app: Quart, authenticated_header: dict): assert response.status_code == 200 data = await response.get_json() assert data["status"] == "ok" + # conflicts is a list assert isinstance(data["data"], list) -@pytest.mark.asyncio -async def test_mcp_test_connection_returns_clear_missing_stdio_command_message( - app: Quart, - authenticated_header: dict, - monkeypatch, -): - test_client = app.test_client() - - @asynccontextmanager - async def fake_stdio_client(*args, **kwargs): - raise FileNotFoundError(2, "系统找不到指定的文件。") - yield - - monkeypatch.setattr(mcp_client_module.mcp, "stdio_client", fake_stdio_client) - - response = await test_client.post( - "/api/tools/mcp/test", - json={ - "mcp_server_config": { - "command": "uvx", - "args": ["mcp-server-fetch"], - } - }, - headers=authenticated_header, - ) - assert response.status_code == 200 - - data = await response.get_json() - assert data["status"] == "error" - assert data["message"] == "Unable to test the MCP connection. Review the details below." - assert data["data"] == { - "error": { - "code": MCP_STDIO_COMMAND_NOT_FOUND, - "command": "uvx", - "raw_error": "[Errno 2] 系统找不到指定的文件。", - } - } - - -@pytest.mark.asyncio -async def test_mcp_test_connection_uses_fallback_for_blank_error_message( - app: Quart, - authenticated_header: dict, - core_lifecycle_td: AstrBotCoreLifecycle, - monkeypatch, -): - test_client = app.test_client() - - async def raise_blank_error(*args, **kwargs): - raise Exception(" ") - - monkeypatch.setattr( - core_lifecycle_td.provider_manager.llm_tools, - "test_mcp_server_connection", - raise_blank_error, - ) - - response = await test_client.post( - "/api/tools/mcp/test", - json={ - "mcp_server_config": { - "command": "uvx", - "args": ["mcp-server-fetch"], - } - }, - headers=authenticated_header, - ) - assert response.status_code == 200 - - data = await response.get_json() - assert data["status"] == "error" - assert data["message"] == "Unable to test the MCP connection." - assert data["data"] == { - "error": { - "code": MCP_TEST_CONNECTION_FAILED, - "detail": "Unable to test the MCP connection.", - } - } - - @pytest.mark.asyncio async def test_check_update( app: Quart,