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

* fix: clarify missing MCP stdio command errors

* refactor: tighten MCP error presentation helpers

* fix: improve MCP test connection feedback

* fix: structure MCP test connection errors

* refactor: share MCP test error codes
This commit is contained in:
エイカク
2026-03-10 23:05:50 +09:00
committed by GitHub
parent ec21cb13d3
commit 0c771e4a77
13 changed files with 349 additions and 27 deletions
+53 -11
View File
@@ -15,6 +15,7 @@ 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
@@ -36,6 +37,44 @@ 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"):
@@ -218,17 +257,20 @@ class MCPClient:
# Handle MCP service error logs
self.server_errlogs.append(msg)
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
),
)
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
# Create a new client session
self.session = await self.exit_stack.enter_async_context(
+2 -1
View File
@@ -45,9 +45,10 @@ class Response:
message: str | None = None
data: dict | list | None = None
def error(self, message: str):
def error(self, message: str, data: dict | list | None = None):
self.status = "error"
self.message = message
self.data = data
return self
def ok(self, data: dict | list | None = None, message: str | None = None):
+33 -2
View File
@@ -3,13 +3,18 @@ import traceback
from quart import request
from astrbot.core import logger
from astrbot.core.agent.mcp_client import MCPTool
from astrbot.core.agent.mcp_client import MCPStdioCommandNotFoundError, 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):
@@ -70,6 +75,32 @@ 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()
@@ -423,7 +454,7 @@ class ToolsRoute(Route):
except Exception as e:
logger.error(traceback.format_exc())
return Response().error(f"Failed to test MCP connection: {e!s}").__dict__
return self._build_mcp_test_error_response(e)
async def get_tool_list(self):
"""Get all registered tools."""
+9
View File
@@ -0,0 +1,9 @@
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",
]
@@ -0,0 +1,4 @@
{
"MCP_STDIO_COMMAND_NOT_FOUND": "mcp_stdio_command_not_found",
"MCP_TEST_CONNECTION_FAILED": "mcp_test_connection_failed"
}
@@ -0,0 +1,8 @@
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"]