Revert "fix: clarify missing MCP stdio command errors (#5992)"

This reverts commit 0c771e4a77.
This commit is contained in:
邹永赫
2026-03-11 00:08:06 +09:00
parent 0c771e4a77
commit e25a1a42cf
13 changed files with 27 additions and 349 deletions
+11 -53
View File
@@ -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 "<unknown>"
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 "<unknown>"
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(
+1 -2
View File
@@ -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):
+2 -33
View File
@@ -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."""
-9
View File
@@ -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",
]
@@ -1,4 +0,0 @@
{
"MCP_STDIO_COMMAND_NOT_FOUND": "mcp_stdio_command_not_found",
"MCP_TEST_CONNECTION_FAILED": "mcp_test_connection_failed"
}
@@ -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"]
@@ -129,30 +129,9 @@
</div>
</v-form>
<v-alert
v-if="addServerDialogFeedback"
:type="addServerDialogFeedback.type"
:icon="addServerDialogFeedback.icon"
variant="tonal"
border="start"
density="comfortable"
class="mt-4"
>
<div class="text-subtitle-2 font-weight-medium">
{{ addServerDialogFeedback.title }}
</div>
<div
v-for="(detail, index) in addServerDialogFeedback.details"
:key="index"
class="text-body-2"
:class="index === 0 ? 'mt-2' : 'mt-1'"
>
{{ detail }}
</div>
<div v-if="addServerDialogFeedback.rawError" class="text-caption text-medium-emphasis mt-3">
{{ tm('dialogs.addServer.feedback.rawError') }} {{ addServerDialogFeedback.rawError }}
</div>
</v-alert>
<div style="margin-top: 8px;">
<small>{{ addServerDialogMessage }}</small>
</div>
</v-card-text>
@@ -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 || '<unknown>'
}),
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;
@@ -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
};
@@ -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": {
@@ -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}"
}
}
}
+1 -4
View File
@@ -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: {
+1 -1
View File
@@ -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]
+1 -86
View File
@@ -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,