mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-30 06:07:14 +00:00
feat(mcp): friendly UI message when stdio MCP refused by Box state
Previously the MCP detail dialog dumped the raw RuntimeError text from
``_init_stdio_python_server`` — English-only, prefixed with "Failed
after 4 attempts", and exposing internal config names. The retry
wrapper also kept retrying a refusal that is deterministically going
to fail again, polluting logs.
Replace the raw text with a structured signal:
- New ``MCPSessionErrorPhase.BOX_UNAVAILABLE`` enum value. The stdio
refusal path sets it before raising and uses a short opaque
discriminator (``box_disabled_in_config`` / ``box_unavailable``) as
the message body — never user-facing
- ``_lifecycle_loop_with_retry`` short-circuits on
``BOX_UNAVAILABLE``: surfaces the error immediately, no retries,
no "Failed after N attempts" prefix. Silences the warning storm
seen during smoke-testing
- ``MCPServerRuntimeInfo`` (TS type) now declares ``error_phase``,
``retry_count``, ``box_session_id``, ``box_enabled`` to match what
the backend already returns in get_runtime_info_dict()
- Both MCP detail forms (``mcp/components/mcp-form/MCPForm.tsx`` and
``plugins/mcp-server/mcp-form/MCPFormDialog.tsx``) detect
``error_phase === 'box_unavailable'`` and render a two-line
localized notice: state line ("Box disabled / unreachable") plus
remediation line ("enable Box or switch to http/sse")
- 8 locale files (en/zh-Hans/zh-Hant/ja/ru/vi/th/es) get
``mcp.boxDisabledStdioRefused``, ``mcp.boxUnavailableStdioRefused``,
``mcp.boxStdioRefusedSuggestion``
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,19 +94,18 @@ class RuntimeMCPSession:
|
|||||||
# (disabled by config or connection failed). Refuse stdio MCP rather
|
# (disabled by config or connection failed). Refuse stdio MCP rather
|
||||||
# than silently falling through to host-stdio — the operator asked
|
# than silently falling through to host-stdio — the operator asked
|
||||||
# for the sandbox and the failure mode should be visible.
|
# for the sandbox and the failure mode should be visible.
|
||||||
|
#
|
||||||
|
# Set ``error_phase = BOX_UNAVAILABLE`` BEFORE raising so the retry
|
||||||
|
# wrapper can short-circuit (retrying is pointless when Box is
|
||||||
|
# deliberately off) and the frontend can render a localized,
|
||||||
|
# actionable message instead of this raw RuntimeError. Keep the
|
||||||
|
# message itself short — the frontend ignores it for this phase.
|
||||||
box_service = getattr(self.ap, 'box_service', None)
|
box_service = getattr(self.ap, 'box_service', None)
|
||||||
if box_service is not None and not getattr(box_service, 'available', False):
|
if box_service is not None and not getattr(box_service, 'available', False):
|
||||||
connector_error = getattr(box_service, '_connector_error', '') or 'currently unavailable'
|
self.error_phase = MCPSessionErrorPhase.BOX_UNAVAILABLE
|
||||||
if not getattr(box_service, 'enabled', True):
|
if not getattr(box_service, 'enabled', True):
|
||||||
reason = 'disabled in config (box.enabled = false)'
|
raise RuntimeError('box_disabled_in_config')
|
||||||
else:
|
raise RuntimeError('box_unavailable')
|
||||||
reason = f'unavailable: {connector_error}'
|
|
||||||
raise RuntimeError(
|
|
||||||
f'Stdio MCP server "{self.server_name}" requires the Box runtime, '
|
|
||||||
f'which is {reason}. Either enable Box in config.yaml '
|
|
||||||
f'(box.enabled = true) and ensure the runtime is healthy, '
|
|
||||||
f'or switch this MCP server to http/sse transport.'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Legacy: no box_service installed at all (pre-Box dev mode). Fall
|
# Legacy: no box_service installed at all (pre-Box dev mode). Fall
|
||||||
# through to host-stdio for backward compatibility.
|
# through to host-stdio for backward compatibility.
|
||||||
@@ -231,6 +230,14 @@ class RuntimeMCPSession:
|
|||||||
self.retry_count = attempt + 1
|
self.retry_count = attempt + 1
|
||||||
if self._shutdown_event.is_set():
|
if self._shutdown_event.is_set():
|
||||||
return # Shutdown requested, don't retry
|
return # Shutdown requested, don't retry
|
||||||
|
# BOX_UNAVAILABLE is a deliberate refusal, not a transient
|
||||||
|
# failure — retrying produces log spam and a misleading
|
||||||
|
# "Failed after N attempts" message. Surface it immediately.
|
||||||
|
if self.error_phase == MCPSessionErrorPhase.BOX_UNAVAILABLE:
|
||||||
|
self.status = MCPSessionStatus.ERROR
|
||||||
|
self.error_message = str(e)
|
||||||
|
self._ready_event.set()
|
||||||
|
return
|
||||||
if attempt >= self._MAX_RETRIES:
|
if attempt >= self._MAX_RETRIES:
|
||||||
self.status = MCPSessionStatus.ERROR
|
self.status = MCPSessionStatus.ERROR
|
||||||
self.error_message = f'Failed after {self._MAX_RETRIES + 1} attempts: {e}'
|
self.error_message = f'Failed after {self._MAX_RETRIES + 1} attempts: {e}'
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ class MCPSessionErrorPhase(enum.Enum):
|
|||||||
MCP_INIT = 'mcp_init'
|
MCP_INIT = 'mcp_init'
|
||||||
RUNTIME = 'runtime'
|
RUNTIME = 'runtime'
|
||||||
TOOL_CALL = 'tool_call'
|
TOOL_CALL = 'tool_call'
|
||||||
|
# Stdio MCP refused because Box is disabled in config or currently
|
||||||
|
# unavailable. Not transient — retries would be pointless. The frontend
|
||||||
|
# uses this phase to render a localized actionable message instead of
|
||||||
|
# the raw RuntimeError text.
|
||||||
|
BOX_UNAVAILABLE = 'box_unavailable'
|
||||||
|
|
||||||
|
|
||||||
class MCPServerBoxConfig(pydantic.BaseModel):
|
class MCPServerBoxConfig(pydantic.BaseModel):
|
||||||
|
|||||||
@@ -78,6 +78,32 @@ function StatusDisplay({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stdio MCP refused because Box is disabled / unreachable. The backend
|
||||||
|
// marks the phase so we can show a localized, actionable message instead
|
||||||
|
// of the raw "box_disabled_in_config" / "box_unavailable" marker.
|
||||||
|
if (runtimeInfo.error_phase === 'box_unavailable') {
|
||||||
|
const isDisabledByConfig =
|
||||||
|
runtimeInfo.error_message === 'box_disabled_in_config';
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 text-red-600">
|
||||||
|
<XCircle className="size-5" />
|
||||||
|
<span className="font-medium">{t('mcp.connectionFailed')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="pl-7 text-sm text-red-500 space-y-0.5">
|
||||||
|
<div>
|
||||||
|
{isDisabledByConfig
|
||||||
|
? t('mcp.boxDisabledStdioRefused')
|
||||||
|
: t('mcp.boxUnavailableStdioRefused')}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
{t('mcp.boxStdioRefusedSuggestion')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center gap-2 text-red-600">
|
<div className="flex items-center gap-2 text-red-600">
|
||||||
|
|||||||
@@ -77,6 +77,32 @@ function StatusDisplay({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stdio MCP refused because Box is disabled / unreachable. The backend
|
||||||
|
// marks the phase so we can show a localized, actionable message instead
|
||||||
|
// of the raw "box_disabled_in_config" / "box_unavailable" marker.
|
||||||
|
if (runtimeInfo.error_phase === 'box_unavailable') {
|
||||||
|
const isDisabledByConfig =
|
||||||
|
runtimeInfo.error_message === 'box_disabled_in_config';
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 text-red-600">
|
||||||
|
<XCircle className="w-5 h-5" />
|
||||||
|
<span className="font-medium">{t('mcp.connectionFailed')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-red-500 pl-7 space-y-0.5">
|
||||||
|
<div>
|
||||||
|
{isDisabledByConfig
|
||||||
|
? t('mcp.boxDisabledStdioRefused')
|
||||||
|
: t('mcp.boxUnavailableStdioRefused')}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
{t('mcp.boxStdioRefusedSuggestion')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 连接失败
|
// 连接失败
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|||||||
@@ -529,8 +529,18 @@ export enum MCPSessionStatus {
|
|||||||
export interface MCPServerRuntimeInfo {
|
export interface MCPServerRuntimeInfo {
|
||||||
status: MCPSessionStatus;
|
status: MCPSessionStatus;
|
||||||
error_message?: string;
|
error_message?: string;
|
||||||
|
/** Stage at which the session failed. Frontends key off this to render
|
||||||
|
* a localized actionable message instead of the raw ``error_message``.
|
||||||
|
* Notable values: ``box_unavailable`` (stdio MCP refused because Box is
|
||||||
|
* disabled / unreachable). See ``MCPSessionErrorPhase`` (backend). */
|
||||||
|
error_phase?: string;
|
||||||
|
retry_count?: number;
|
||||||
tool_count: number;
|
tool_count: number;
|
||||||
tools: MCPTool[];
|
tools: MCPTool[];
|
||||||
|
/** Optional ``box_session_id`` / ``box_enabled`` set when this stdio
|
||||||
|
* server runs inside Box. Absent when Box is unavailable. */
|
||||||
|
box_session_id?: string;
|
||||||
|
box_enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MCPServer =
|
export type MCPServer =
|
||||||
|
|||||||
@@ -738,6 +738,12 @@ const enUS = {
|
|||||||
connectionSuccess: 'Connection successful',
|
connectionSuccess: 'Connection successful',
|
||||||
connectionFailed: 'Connection failed, please check URL',
|
connectionFailed: 'Connection failed, please check URL',
|
||||||
connectionFailedStatus: 'Connection Failed',
|
connectionFailedStatus: 'Connection Failed',
|
||||||
|
boxDisabledStdioRefused:
|
||||||
|
'Stdio MCP servers require the Box sandbox, which is disabled in config (box.enabled = false).',
|
||||||
|
boxUnavailableStdioRefused:
|
||||||
|
'Stdio MCP servers require the Box sandbox, which is currently unreachable.',
|
||||||
|
boxStdioRefusedSuggestion:
|
||||||
|
'Enable Box (box.enabled = true) and ensure the runtime is healthy, or switch this server to http/sse mode.',
|
||||||
toolsFound: 'tools',
|
toolsFound: 'tools',
|
||||||
unknownError: 'Unknown error',
|
unknownError: 'Unknown error',
|
||||||
noToolsFound: 'No tools found',
|
noToolsFound: 'No tools found',
|
||||||
|
|||||||
@@ -752,6 +752,12 @@ const esES = {
|
|||||||
connectionSuccess: 'Conexión exitosa',
|
connectionSuccess: 'Conexión exitosa',
|
||||||
connectionFailed: 'Error de conexión, por favor verifica la URL',
|
connectionFailed: 'Error de conexión, por favor verifica la URL',
|
||||||
connectionFailedStatus: 'Conexión fallida',
|
connectionFailedStatus: 'Conexión fallida',
|
||||||
|
boxDisabledStdioRefused:
|
||||||
|
'Los servidores MCP en modo stdio requieren el sandbox de Box, desactivado en la configuración (box.enabled = false).',
|
||||||
|
boxUnavailableStdioRefused:
|
||||||
|
'Los servidores MCP en modo stdio requieren el sandbox de Box, actualmente no accesible.',
|
||||||
|
boxStdioRefusedSuggestion:
|
||||||
|
'Active Box (box.enabled = true) y asegúrese de que el runtime está conectado, o cambie este servidor a modo http/sse.',
|
||||||
toolsFound: 'herramientas',
|
toolsFound: 'herramientas',
|
||||||
unknownError: 'Error desconocido',
|
unknownError: 'Error desconocido',
|
||||||
noToolsFound: 'No se encontraron herramientas',
|
noToolsFound: 'No se encontraron herramientas',
|
||||||
|
|||||||
@@ -743,6 +743,12 @@ const jaJP = {
|
|||||||
connectionSuccess: '接続に成功しました',
|
connectionSuccess: '接続に成功しました',
|
||||||
connectionFailed: '接続に失敗しました,URLを確認してください',
|
connectionFailed: '接続に失敗しました,URLを確認してください',
|
||||||
connectionFailedStatus: '接続失敗',
|
connectionFailedStatus: '接続失敗',
|
||||||
|
boxDisabledStdioRefused:
|
||||||
|
'Stdio モードの MCP サーバーは Box サンドボックスを必要としますが、設定で無効化されています(box.enabled = false)。',
|
||||||
|
boxUnavailableStdioRefused:
|
||||||
|
'Stdio モードの MCP サーバーは Box サンドボックスを必要としますが、現在接続できません。',
|
||||||
|
boxStdioRefusedSuggestion:
|
||||||
|
'Box を有効化(box.enabled = true)してランタイムの接続を確認するか、このサーバーを http/sse モードに切り替えてください。',
|
||||||
toolsFound: '個のツール',
|
toolsFound: '個のツール',
|
||||||
unknownError: '不明なエラー',
|
unknownError: '不明なエラー',
|
||||||
noToolsFound: 'ツールが見つかりません',
|
noToolsFound: 'ツールが見つかりません',
|
||||||
|
|||||||
@@ -748,6 +748,12 @@ const ruRU = {
|
|||||||
connectionSuccess: 'Подключение успешно',
|
connectionSuccess: 'Подключение успешно',
|
||||||
connectionFailed: 'Не удалось подключиться, проверьте URL',
|
connectionFailed: 'Не удалось подключиться, проверьте URL',
|
||||||
connectionFailedStatus: 'Ошибка подключения',
|
connectionFailedStatus: 'Ошибка подключения',
|
||||||
|
boxDisabledStdioRefused:
|
||||||
|
'MCP-серверы в режиме stdio требуют песочницу Box, которая отключена в конфигурации (box.enabled = false).',
|
||||||
|
boxUnavailableStdioRefused:
|
||||||
|
'MCP-серверы в режиме stdio требуют песочницу Box, которая сейчас недоступна.',
|
||||||
|
boxStdioRefusedSuggestion:
|
||||||
|
'Включите Box (box.enabled = true) и убедитесь, что среда работает, либо переключите этот сервер в режим http/sse.',
|
||||||
toolsFound: 'инструментов',
|
toolsFound: 'инструментов',
|
||||||
unknownError: 'Неизвестная ошибка',
|
unknownError: 'Неизвестная ошибка',
|
||||||
noToolsFound: 'Инструменты не найдены',
|
noToolsFound: 'Инструменты не найдены',
|
||||||
|
|||||||
@@ -728,6 +728,12 @@ const thTH = {
|
|||||||
connectionSuccess: 'เชื่อมต่อสำเร็จ',
|
connectionSuccess: 'เชื่อมต่อสำเร็จ',
|
||||||
connectionFailed: 'เชื่อมต่อล้มเหลว กรุณาตรวจสอบ URL',
|
connectionFailed: 'เชื่อมต่อล้มเหลว กรุณาตรวจสอบ URL',
|
||||||
connectionFailedStatus: 'เชื่อมต่อล้มเหลว',
|
connectionFailedStatus: 'เชื่อมต่อล้มเหลว',
|
||||||
|
boxDisabledStdioRefused:
|
||||||
|
'MCP server แบบ stdio ต้องใช้ Sandbox Box ซึ่งถูกปิดใช้งานในการตั้งค่า (box.enabled = false)',
|
||||||
|
boxUnavailableStdioRefused:
|
||||||
|
'MCP server แบบ stdio ต้องใช้ Sandbox Box ซึ่งขณะนี้เชื่อมต่อไม่ได้',
|
||||||
|
boxStdioRefusedSuggestion:
|
||||||
|
'กรุณาเปิดใช้งาน Box (box.enabled = true) และตรวจสอบว่ารันไทม์ทำงานปกติ หรือเปลี่ยน MCP server เป็นโหมด http/sse',
|
||||||
toolsFound: 'เครื่องมือ',
|
toolsFound: 'เครื่องมือ',
|
||||||
unknownError: 'ข้อผิดพลาดที่ไม่ทราบสาเหตุ',
|
unknownError: 'ข้อผิดพลาดที่ไม่ทราบสาเหตุ',
|
||||||
noToolsFound: 'ไม่พบเครื่องมือ',
|
noToolsFound: 'ไม่พบเครื่องมือ',
|
||||||
|
|||||||
@@ -742,6 +742,12 @@ const viVN = {
|
|||||||
connectionSuccess: 'Kết nối thành công',
|
connectionSuccess: 'Kết nối thành công',
|
||||||
connectionFailed: 'Kết nối thất bại, vui lòng kiểm tra URL',
|
connectionFailed: 'Kết nối thất bại, vui lòng kiểm tra URL',
|
||||||
connectionFailedStatus: 'Kết nối thất bại',
|
connectionFailedStatus: 'Kết nối thất bại',
|
||||||
|
boxDisabledStdioRefused:
|
||||||
|
'MCP server ở chế độ stdio cần Sandbox Box, hiện đã bị tắt trong cấu hình (box.enabled = false).',
|
||||||
|
boxUnavailableStdioRefused:
|
||||||
|
'MCP server ở chế độ stdio cần Sandbox Box, hiện không thể kết nối.',
|
||||||
|
boxStdioRefusedSuggestion:
|
||||||
|
'Hãy bật Box (box.enabled = true) và đảm bảo runtime hoạt động, hoặc chuyển server này sang chế độ http/sse.',
|
||||||
toolsFound: 'công cụ',
|
toolsFound: 'công cụ',
|
||||||
unknownError: 'Lỗi không xác định',
|
unknownError: 'Lỗi không xác định',
|
||||||
noToolsFound: 'Không tìm thấy công cụ nào',
|
noToolsFound: 'Không tìm thấy công cụ nào',
|
||||||
|
|||||||
@@ -710,6 +710,12 @@ const zhHans = {
|
|||||||
connectionSuccess: '连接成功',
|
connectionSuccess: '连接成功',
|
||||||
connectionFailed: '连接失败,请检查URL',
|
connectionFailed: '连接失败,请检查URL',
|
||||||
connectionFailedStatus: '连接失败',
|
connectionFailedStatus: '连接失败',
|
||||||
|
boxDisabledStdioRefused:
|
||||||
|
'Stdio 模式的 MCP 服务器依赖 Box 沙箱,目前已在配置中禁用(box.enabled = false)。',
|
||||||
|
boxUnavailableStdioRefused:
|
||||||
|
'Stdio 模式的 MCP 服务器依赖 Box 沙箱,目前无法连接。',
|
||||||
|
boxStdioRefusedSuggestion:
|
||||||
|
'请启用 Box(box.enabled = true)并确认运行时连接正常,或将此服务器切换到 http/sse 模式。',
|
||||||
toolsFound: '个工具',
|
toolsFound: '个工具',
|
||||||
unknownError: '未知错误',
|
unknownError: '未知错误',
|
||||||
noToolsFound: '未找到任何工具',
|
noToolsFound: '未找到任何工具',
|
||||||
|
|||||||
@@ -709,6 +709,12 @@ const zhHant = {
|
|||||||
connectionSuccess: '連接成功',
|
connectionSuccess: '連接成功',
|
||||||
connectionFailed: '連接失敗,請檢查URL',
|
connectionFailed: '連接失敗,請檢查URL',
|
||||||
connectionFailedStatus: '連接失敗',
|
connectionFailedStatus: '連接失敗',
|
||||||
|
boxDisabledStdioRefused:
|
||||||
|
'Stdio 模式的 MCP 伺服器依賴 Box 沙箱,目前已在設定中停用(box.enabled = false)。',
|
||||||
|
boxUnavailableStdioRefused:
|
||||||
|
'Stdio 模式的 MCP 伺服器依賴 Box 沙箱,目前無法連線。',
|
||||||
|
boxStdioRefusedSuggestion:
|
||||||
|
'請啟用 Box(box.enabled = true)並確認執行時連線正常,或將此伺服器切換到 http/sse 模式。',
|
||||||
toolsFound: '個工具',
|
toolsFound: '個工具',
|
||||||
unknownError: '未知錯誤',
|
unknownError: '未知錯誤',
|
||||||
noToolsFound: '未找到任何工具',
|
noToolsFound: '未找到任何工具',
|
||||||
|
|||||||
Reference in New Issue
Block a user