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:
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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"]
|
||||
Reference in New Issue
Block a user