Revert "fix: clarify missing MCP stdio command errors (#5992)"
This reverts commit 0c771e4a77.
This commit is contained in:
@@ -15,7 +15,6 @@ from tenacity import (
|
|||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
from astrbot.core.agent.run_context import ContextWrapper
|
from astrbot.core.agent.run_context import ContextWrapper
|
||||||
from astrbot.core.utils.log_pipe import LogPipe
|
from astrbot.core.utils.log_pipe import LogPipe
|
||||||
from astrbot.dashboard.shared import MCP_STDIO_COMMAND_NOT_FOUND
|
|
||||||
|
|
||||||
from .run_context import TContext
|
from .run_context import TContext
|
||||||
from .tool import FunctionTool
|
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:
|
def _prepare_config(config: dict) -> dict:
|
||||||
"""Prepare configuration, handle nested format"""
|
"""Prepare configuration, handle nested format"""
|
||||||
if config.get("mcpServers"):
|
if config.get("mcpServers"):
|
||||||
@@ -257,20 +218,17 @@ class MCPClient:
|
|||||||
# Handle MCP service error logs
|
# Handle MCP service error logs
|
||||||
self.server_errlogs.append(msg)
|
self.server_errlogs.append(msg)
|
||||||
|
|
||||||
try:
|
stdio_transport = await self.exit_stack.enter_async_context(
|
||||||
stdio_transport = await self.exit_stack.enter_async_context(
|
mcp.stdio_client(
|
||||||
mcp.stdio_client(
|
server_params,
|
||||||
server_params,
|
errlog=LogPipe(
|
||||||
errlog=LogPipe(
|
level=logging.ERROR,
|
||||||
level=logging.ERROR,
|
logger=logger,
|
||||||
logger=logger,
|
identifier=f"MCPServer-{name}",
|
||||||
identifier=f"MCPServer-{name}",
|
callback=callback,
|
||||||
callback=callback,
|
), # type: ignore
|
||||||
), # type: ignore
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
except FileNotFoundError as exc:
|
|
||||||
raise MCPStdioCommandNotFoundError(cfg.get("command"), exc) from exc
|
|
||||||
|
|
||||||
# Create a new client session
|
# Create a new client session
|
||||||
self.session = await self.exit_stack.enter_async_context(
|
self.session = await self.exit_stack.enter_async_context(
|
||||||
|
|||||||
@@ -45,10 +45,9 @@ class Response:
|
|||||||
message: str | None = None
|
message: str | None = None
|
||||||
data: dict | list | 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.status = "error"
|
||||||
self.message = message
|
self.message = message
|
||||||
self.data = data
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def ok(self, data: dict | list | None = None, message: str | None = None):
|
def ok(self, data: dict | list | None = None, message: str | None = None):
|
||||||
|
|||||||
@@ -3,18 +3,13 @@ import traceback
|
|||||||
from quart import request
|
from quart import request
|
||||||
|
|
||||||
from astrbot.core import logger
|
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.core_lifecycle import AstrBotCoreLifecycle
|
||||||
from astrbot.core.star import star_map
|
from astrbot.core.star import star_map
|
||||||
from astrbot.dashboard.shared import MCP_TEST_CONNECTION_FAILED
|
|
||||||
|
|
||||||
from .route import Response, Route, RouteContext
|
from .route import Response, Route, RouteContext
|
||||||
|
|
||||||
DEFAULT_MCP_CONFIG = {"mcpServers": {}}
|
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):
|
class EmptyMcpServersError(ValueError):
|
||||||
@@ -75,32 +70,6 @@ class ToolsRoute(Route):
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return False
|
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):
|
async def get_mcp_servers(self):
|
||||||
try:
|
try:
|
||||||
config = self.tool_mgr.load_mcp_config()
|
config = self.tool_mgr.load_mcp_config()
|
||||||
@@ -454,7 +423,7 @@ class ToolsRoute(Route):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(traceback.format_exc())
|
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):
|
async def get_tool_list(self):
|
||||||
"""Get all registered tools."""
|
"""Get all registered tools."""
|
||||||
|
|||||||
@@ -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>
|
</div>
|
||||||
|
|
||||||
</v-form>
|
</v-form>
|
||||||
<v-alert
|
<div style="margin-top: 8px;">
|
||||||
v-if="addServerDialogFeedback"
|
<small>{{ addServerDialogMessage }}</small>
|
||||||
:type="addServerDialogFeedback.type"
|
</div>
|
||||||
: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>
|
|
||||||
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
@@ -238,7 +217,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
|
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
|
||||||
import ItemCard from '@/components/shared/ItemCard.vue';
|
import ItemCard from '@/components/shared/ItemCard.vue';
|
||||||
import { MCP_TEST_CONNECTION_ERROR_CODES } from '@/constants/mcpTestConnectionErrorCodes';
|
|
||||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||||
import {
|
import {
|
||||||
askForConfirmation as askForConfirmationDialog,
|
askForConfirmation as askForConfirmationDialog,
|
||||||
@@ -266,7 +244,7 @@ export default {
|
|||||||
mcpServerProviderList: ['modelscope'],
|
mcpServerProviderList: ['modelscope'],
|
||||||
mcpProviderToken: '',
|
mcpProviderToken: '',
|
||||||
showSyncMcpServerDialog: false,
|
showSyncMcpServerDialog: false,
|
||||||
addServerDialogFeedback: null,
|
addServerDialogMessage: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
loadingGettingServers: false,
|
loadingGettingServers: false,
|
||||||
mcpServerUpdateLoaders: {},
|
mcpServerUpdateLoaders: {},
|
||||||
@@ -403,7 +381,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.showMcpServerDialog = false;
|
this.showMcpServerDialog = false;
|
||||||
this.clearAddServerDialogFeedback();
|
this.addServerDialogMessage = '';
|
||||||
this.getServers();
|
this.getServers();
|
||||||
this.showSuccess(response.data.message || this.tm('messages.saveSuccess'));
|
this.showSuccess(response.data.message || this.tm('messages.saveSuccess'));
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
@@ -467,73 +445,14 @@ export default {
|
|||||||
},
|
},
|
||||||
closeServerDialog() {
|
closeServerDialog() {
|
||||||
this.showMcpServerDialog = false;
|
this.showMcpServerDialog = false;
|
||||||
this.clearAddServerDialogFeedback();
|
this.addServerDialogMessage = '';
|
||||||
this.resetForm();
|
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() {
|
testServerConnection() {
|
||||||
if (!this.validateJson()) {
|
if (!this.validateJson()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.clearAddServerDialogFeedback();
|
|
||||||
let configObj;
|
let configObj;
|
||||||
try {
|
try {
|
||||||
configObj = JSON.parse(this.serverConfigJson);
|
configObj = JSON.parse(this.serverConfigJson);
|
||||||
@@ -547,33 +466,11 @@ export default {
|
|||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
if (response.data.status === 'error') {
|
this.addServerDialogMessage = `${response.data.message} (tools: ${response.data.data})`;
|
||||||
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)
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.showError(
|
this.showError(this.tm('messages.testError', { error: error.response?.data?.message || error.message }));
|
||||||
error.response?.data?.message || error.message || this.tm('dialogs.addServer.feedback.genericDetail'),
|
|
||||||
{
|
|
||||||
inlineDialog: true,
|
|
||||||
errorData: error.response?.data?.data?.error
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
resetForm() {
|
resetForm() {
|
||||||
@@ -592,13 +489,7 @@ export default {
|
|||||||
this.save_message_success = 'success';
|
this.save_message_success = 'success';
|
||||||
this.save_message_snack = true;
|
this.save_message_snack = true;
|
||||||
},
|
},
|
||||||
showError(message, options = {}) {
|
showError(message) {
|
||||||
if (options.inlineDialog) {
|
|
||||||
this.setAddServerDialogFeedback(
|
|
||||||
this.buildAddServerDialogErrorFeedback(message, options.errorData)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.save_message = message;
|
this.save_message = message;
|
||||||
this.save_message_success = 'error';
|
this.save_message_success = 'error';
|
||||||
this.save_message_snack = true;
|
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": {
|
"tips": {
|
||||||
"timeoutConfig": "Please configure tool call timeout separately in the configuration page"
|
"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": {
|
"serverDetail": {
|
||||||
|
|||||||
@@ -86,18 +86,6 @@
|
|||||||
},
|
},
|
||||||
"tips": {
|
"tips": {
|
||||||
"timeoutConfig": "工具调用的超时时间请前往配置页面单独配置"
|
"timeoutConfig": "工具调用的超时时间请前往配置页面单独配置"
|
||||||
},
|
|
||||||
"feedback": {
|
|
||||||
"successTitle": "连接测试通过",
|
|
||||||
"errorTitle": "连接测试失败",
|
|
||||||
"genericDetail": "连接测试未能成功完成。",
|
|
||||||
"availableTools": "可用工具:{tools}",
|
|
||||||
"rawError": "原始错误:",
|
|
||||||
"stdioCommandNotFound": {
|
|
||||||
"title": "无法启动 MCP stdio 服务",
|
|
||||||
"reason": "未找到命令 “{command}”。",
|
|
||||||
"action": "请先安装该命令,或将 command 修改为本机可执行文件的完整路径,并确认它已加入 PATH。"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serverDetail": {
|
"serverDetail": {
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
mermaid: 'mermaid/dist/mermaid.js',
|
mermaid: 'mermaid/dist/mermaid.js',
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
'@dashboard-shared': fileURLToPath(
|
|
||||||
new URL('../astrbot/dashboard/shared', import.meta.url)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
|
|||||||
+1
-1
@@ -116,7 +116,7 @@ allow-direct-references = true
|
|||||||
|
|
||||||
# Include bundled dashboard dist even though it is not tracked by VCS.
|
# Include bundled dashboard dist even though it is not tracked by VCS.
|
||||||
[tool.hatch.build.targets.wheel]
|
[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.
|
# Custom build hook: builds the Vue dashboard and copies dist into the package.
|
||||||
[tool.hatch.build.hooks.custom]
|
[tool.hatch.build.hooks.custom]
|
||||||
|
|||||||
+1
-86
@@ -3,7 +3,6 @@ import io
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import zipfile
|
import zipfile
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from types import SimpleNamespace
|
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.db.sqlite import SQLiteDatabase
|
||||||
from astrbot.core.star.star import star_registry
|
from astrbot.core.star.star import star_registry
|
||||||
from astrbot.core.star.star_handler import star_handlers_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.routes.plugin import PluginRoute
|
||||||
from astrbot.dashboard.server import AstrBotDashboard
|
from astrbot.dashboard.server import AstrBotDashboard
|
||||||
from tests.fixtures.helpers import (
|
from tests.fixtures.helpers import (
|
||||||
@@ -276,89 +270,10 @@ async def test_commands_api(app: Quart, authenticated_header: dict):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
data = await response.get_json()
|
data = await response.get_json()
|
||||||
assert data["status"] == "ok"
|
assert data["status"] == "ok"
|
||||||
|
# conflicts is a list
|
||||||
assert isinstance(data["data"], 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
|
@pytest.mark.asyncio
|
||||||
async def test_check_update(
|
async def test_check_update(
|
||||||
app: Quart,
|
app: Quart,
|
||||||
|
|||||||
Reference in New Issue
Block a user