v0.1.4-p2

main
mula.liu 2026-03-15 17:28:58 +08:00
parent 89ed5f7107
commit dab172cb2a
5 changed files with 3 additions and 502 deletions

View File

@ -4,7 +4,6 @@ import threading
import time import time
import codecs import codecs
import base64 import base64
import uuid
from typing import Any, Callable, Dict, List, Optional, Tuple from typing import Any, Callable, Dict, List, Optional, Tuple
import json import json
@ -26,87 +25,6 @@ class BotDockerManager:
self.active_monitors = {} self.active_monitors = {}
self._last_delivery_error: Dict[str, str] = {} self._last_delivery_error: Dict[str, str] = {}
@staticmethod
def _build_http_probe_payload_b64(
url: str,
method: str = "GET",
headers: Optional[Dict[str, str]] = None,
body_json: Optional[Dict[str, Any]] = None,
timeout_seconds: int = 10,
) -> str:
safe_method = str(method or "GET").strip().upper()
if safe_method not in {"GET", "POST"}:
safe_method = "GET"
timeout = max(1, min(int(timeout_seconds or 10), 30))
payload = {
"url": str(url or "").strip(),
"method": safe_method,
"headers": headers or {},
"body_json": body_json if isinstance(body_json, dict) else None,
"timeout": timeout,
}
return base64.b64encode(json.dumps(payload, ensure_ascii=False).encode("utf-8")).decode("ascii")
@staticmethod
def _http_probe_python_script() -> str:
return (
"import base64, json, os, urllib.request, urllib.error\n"
"cfg = json.loads(base64.b64decode(os.environ['DASHBOARD_HTTP_PROBE_B64']).decode('utf-8'))\n"
"url = str(cfg.get('url') or '').strip()\n"
"method = str(cfg.get('method') or 'GET').upper()\n"
"headers = cfg.get('headers') or {}\n"
"timeout = int(cfg.get('timeout') or 10)\n"
"data = None\n"
"if method == 'POST':\n"
" body = cfg.get('body_json')\n"
" if not isinstance(body, dict):\n"
" body = {}\n"
" data = json.dumps(body, ensure_ascii=False).encode('utf-8')\n"
" if 'Content-Type' not in headers:\n"
" headers['Content-Type'] = 'application/json'\n"
"req = urllib.request.Request(url, data=data, headers=headers, method=method)\n"
"result = {'ok': False, 'status_code': None, 'content_type': '', 'body_preview': '', 'message': ''}\n"
"try:\n"
" with urllib.request.urlopen(req, timeout=timeout) as resp:\n"
" body = resp.read(1024).decode('utf-8', 'ignore')\n"
" result.update({'ok': True, 'status_code': int(getattr(resp, 'status', 200) or 200), 'content_type': str(resp.headers.get('content-type') or ''), 'body_preview': body[:512], 'message': 'ok'})\n"
"except urllib.error.HTTPError as e:\n"
" body = ''\n"
" try:\n"
" body = e.read(1024).decode('utf-8', 'ignore')\n"
" except Exception:\n"
" body = ''\n"
" result.update({'ok': False, 'status_code': int(e.code or 0), 'content_type': str((e.headers or {}).get('content-type') or ''), 'body_preview': body[:512], 'message': f'HTTPError: {e.code}'})\n"
"except Exception as e:\n"
" result.update({'ok': False, 'status_code': None, 'content_type': '', 'body_preview': '', 'message': f'{type(e).__name__}: {e}'})\n"
"print(json.dumps(result, ensure_ascii=False))\n"
)
def _run_http_probe_exec(self, container, payload_b64: str) -> Dict[str, Any]:
py_script = self._http_probe_python_script()
py_bins = ["python3", "python"]
last_error = ""
for py_bin in py_bins:
try:
exec_result = container.exec_run(
[py_bin, "-c", py_script],
environment={"DASHBOARD_HTTP_PROBE_B64": payload_b64},
)
except Exception as e:
last_error = f"exec {py_bin} failed: {e}"
continue
output = exec_result.output.decode("utf-8", errors="ignore") if isinstance(exec_result.output, (bytes, bytearray)) else str(exec_result.output)
if exec_result.exit_code != 0:
last_error = f"exec {py_bin} exit={exec_result.exit_code}: {output[:300]}"
continue
try:
parsed = json.loads(output.strip() or "{}")
if isinstance(parsed, dict):
return parsed
except Exception:
last_error = f"exec {py_bin} returned non-json: {output[:300]}"
return {"ok": False, "message": last_error or "Failed to run probe in container"}
@staticmethod @staticmethod
def _normalize_resource_limits( def _normalize_resource_limits(
cpu_cores: Optional[float], cpu_cores: Optional[float],
@ -293,77 +211,6 @@ class BotDockerManager:
print(f"[DockerManager] Error stopping bot {bot_id}: {e}") print(f"[DockerManager] Error stopping bot {bot_id}: {e}")
return False return False
def probe_http_from_container(
self,
bot_id: str,
url: str,
method: str = "GET",
headers: Optional[Dict[str, str]] = None,
body_json: Optional[Dict[str, Any]] = None,
timeout_seconds: int = 10,
) -> Dict[str, Any]:
if not self.client:
return {"ok": False, "message": "Docker client is not available"}
try:
container = self.client.containers.get(f"worker_{bot_id}")
container.reload()
if container.status != "running":
return {"ok": False, "message": f"Bot container is {container.status}"}
except docker.errors.NotFound:
return {"ok": False, "message": "Bot container not found"}
except Exception as e:
return {"ok": False, "message": f"Failed to inspect bot container: {e}"}
payload_b64 = self._build_http_probe_payload_b64(
url=url,
method=method,
headers=headers,
body_json=body_json,
timeout_seconds=timeout_seconds,
)
return self._run_http_probe_exec(container, payload_b64)
def probe_http_via_temporary_container(
self,
image_tag: str,
url: str,
method: str = "GET",
headers: Optional[Dict[str, str]] = None,
body_json: Optional[Dict[str, Any]] = None,
timeout_seconds: int = 10,
) -> Dict[str, Any]:
if not self.client:
return {"ok": False, "message": "Docker client is not available"}
image = str(image_tag or self.base_image).strip() or self.base_image
payload_b64 = self._build_http_probe_payload_b64(
url=url,
method=method,
headers=headers,
body_json=body_json,
timeout_seconds=timeout_seconds,
)
container = None
try:
container = self.client.containers.run(
image=image,
name=f"dashboard_probe_{uuid.uuid4().hex[:10]}",
command=["sh", "-c", "sleep 45"],
detach=True,
tty=False,
stdin_open=False,
network_mode="bridge",
)
return self._run_http_probe_exec(container, payload_b64)
except docker.errors.ImageNotFound:
return {"ok": False, "message": f"Probe image not found: {image}"}
except Exception as e:
return {"ok": False, "message": f"Failed to run temporary probe container: {e}"}
finally:
if container is not None:
try:
container.remove(force=True)
except Exception:
pass
def send_command(self, bot_id: str, command: str, media: Optional[List[str]] = None) -> bool: def send_command(self, bot_id: str, command: str, media: Optional[List[str]] = None) -> bool:
"""Send a command to dashboard channel with robust container-local delivery.""" """Send a command to dashboard channel with robust container-local delivery."""
if not self.client: if not self.client:

View File

@ -163,13 +163,6 @@ class BotMcpConfigUpdateRequest(BaseModel):
mcp_servers: Optional[Dict[str, Any]] = None mcp_servers: Optional[Dict[str, Any]] = None
class BotMcpConfigTestRequest(BaseModel):
type: Optional[str] = None
url: Optional[str] = None
headers: Optional[Dict[str, str]] = None
tool_timeout: Optional[int] = None
class BotEnvParamsUpdateRequest(BaseModel): class BotEnvParamsUpdateRequest(BaseModel):
env_params: Optional[Dict[str, str]] = None env_params: Optional[Dict[str, str]] = None
@ -1112,215 +1105,6 @@ def _sanitize_mcp_servers_in_config_data(config_data: Dict[str, Any]) -> Dict[st
return merged return merged
def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict[str, Any]:
transport_type = str(cfg.get("type") or "streamableHttp").strip()
if transport_type not in {"streamableHttp", "sse"}:
transport_type = "streamableHttp"
url = str(cfg.get("url") or "").strip()
headers_raw = cfg.get("headers")
headers: Dict[str, str] = {}
if isinstance(headers_raw, dict):
for k, v in headers_raw.items():
key = str(k or "").strip()
if key:
headers[key] = str(v or "").strip()
timeout_raw = cfg.get("toolTimeout", 10)
try:
timeout_s = max(1, min(int(timeout_raw), 30))
except Exception:
timeout_s = 10
if not url:
return {
"ok": False,
"transport": transport_type,
"status_code": None,
"message": "MCP url is required",
"probe_from": "validation",
}
def _with_body_preview(message: str, preview: Any) -> str:
text = str(message or "").strip()
body = " ".join(str(preview or "").strip().split())
if not body:
return text
body = body[:240]
return f"{text}: {body}" if text else body
if bot_id:
if transport_type == "sse":
probe_headers = dict(headers)
probe_headers.setdefault("Accept", "text/event-stream")
probe = docker_manager.probe_http_from_container(
bot_id=bot_id,
url=url,
method="GET",
headers=probe_headers,
body_json=None,
timeout_seconds=timeout_s,
)
status_code = probe.get("status_code")
content_type = str(probe.get("content_type") or "")
message = str(probe.get("message") or "").strip()
body_preview = probe.get("body_preview")
if status_code in {401, 403}:
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "Auth failed for MCP SSE endpoint", "content_type": content_type, "probe_from": "bot-container"}
if status_code == 404:
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "MCP SSE endpoint not found", "content_type": content_type, "probe_from": "bot-container"}
if isinstance(status_code, int) and status_code >= 500:
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview("MCP SSE endpoint server error", body_preview), "content_type": content_type, "probe_from": "bot-container"}
if not probe.get("ok"):
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview(message or "Failed to connect MCP SSE endpoint from bot container", body_preview), "content_type": content_type, "probe_from": "bot-container"}
if "text/event-stream" not in content_type.lower():
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview("Endpoint reachable, but content-type is not text/event-stream", body_preview), "content_type": content_type, "probe_from": "bot-container"}
return {"ok": True, "transport": transport_type, "status_code": status_code, "message": "MCP SSE endpoint is reachable", "content_type": content_type, "probe_from": "bot-container"}
probe_headers = dict(headers)
probe_headers.setdefault("Accept", "application/json, text/event-stream")
probe = docker_manager.probe_http_from_container(
bot_id=bot_id,
url=url,
method="GET",
headers=probe_headers,
body_json=None,
timeout_seconds=timeout_s,
)
status_code = probe.get("status_code")
message = str(probe.get("message") or "").strip()
body_preview = probe.get("body_preview")
if status_code in {401, 403}:
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "Auth failed for MCP endpoint", "probe_from": "bot-container"}
if status_code == 404:
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "MCP endpoint not found", "probe_from": "bot-container"}
if isinstance(status_code, int) and status_code >= 500:
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview("MCP endpoint server error", body_preview), "probe_from": "bot-container"}
if probe.get("ok") and status_code in {200, 201, 202, 204, 400, 401, 403, 405, 406, 415, 422}:
reachability_msg = "MCP endpoint is reachable" if status_code in {200, 201, 202, 204} else "MCP endpoint is reachable (HTTP endpoint responded as expected)"
return {"ok": True, "transport": transport_type, "status_code": status_code, "message": reachability_msg, "probe_from": "bot-container"}
return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview(message or "Unexpected response from MCP endpoint", body_preview), "probe_from": "bot-container"}
try:
with httpx.Client(timeout=httpx.Timeout(timeout_s), follow_redirects=True) as client:
if transport_type == "sse":
req_headers = dict(headers)
req_headers.setdefault("Accept", "text/event-stream")
resp = client.get(url, headers=req_headers)
content_type = str(resp.headers.get("content-type") or "")
body_preview = resp.text[:512]
if resp.status_code in {401, 403}:
return {
"ok": False,
"transport": transport_type,
"status_code": resp.status_code,
"message": "Auth failed for MCP SSE endpoint",
"content_type": content_type,
"probe_from": "backend-host",
}
if resp.status_code == 404:
return {
"ok": False,
"transport": transport_type,
"status_code": resp.status_code,
"message": "MCP SSE endpoint not found",
"content_type": content_type,
"probe_from": "backend-host",
}
if resp.status_code >= 500:
return {
"ok": False,
"transport": transport_type,
"status_code": resp.status_code,
"message": _with_body_preview("MCP SSE endpoint server error", body_preview),
"content_type": content_type,
"probe_from": "backend-host",
}
if "text/event-stream" not in content_type.lower():
return {
"ok": False,
"transport": transport_type,
"status_code": resp.status_code,
"message": _with_body_preview("Endpoint reachable, but content-type is not text/event-stream", body_preview),
"content_type": content_type,
"probe_from": "backend-host",
}
return {
"ok": True,
"transport": transport_type,
"status_code": resp.status_code,
"message": "MCP SSE endpoint is reachable",
"content_type": content_type,
"probe_from": "backend-host",
}
req_headers = dict(headers)
req_headers.setdefault("Accept", "application/json, text/event-stream")
resp = client.get(url, headers=req_headers)
body_preview = resp.text[:512]
if resp.status_code in {401, 403}:
return {
"ok": False,
"transport": transport_type,
"status_code": resp.status_code,
"message": "Auth failed for MCP endpoint",
"probe_from": "backend-host",
}
if resp.status_code == 404:
return {
"ok": False,
"transport": transport_type,
"status_code": resp.status_code,
"message": "MCP endpoint not found",
"probe_from": "backend-host",
}
if resp.status_code >= 500:
return {
"ok": False,
"transport": transport_type,
"status_code": resp.status_code,
"message": _with_body_preview("MCP endpoint server error", body_preview),
"probe_from": "backend-host",
}
if resp.status_code in {200, 201, 202, 204}:
return {
"ok": True,
"transport": transport_type,
"status_code": resp.status_code,
"message": "MCP endpoint is reachable",
"probe_from": "backend-host",
}
if resp.status_code in {400, 401, 403, 405, 406, 415, 422}:
return {
"ok": True,
"transport": transport_type,
"status_code": resp.status_code,
"message": "MCP endpoint is reachable (HTTP endpoint responded as expected)",
"probe_from": "backend-host",
}
return {
"ok": False,
"transport": transport_type,
"status_code": resp.status_code,
"message": _with_body_preview("Unexpected response from MCP endpoint", body_preview),
"probe_from": "backend-host",
}
except httpx.TimeoutException:
return {
"ok": False,
"transport": transport_type,
"status_code": None,
"message": "MCP endpoint timeout",
"probe_from": "backend-host",
}
except Exception as exc:
return {
"ok": False,
"transport": transport_type,
"status_code": None,
"message": f"MCP probe failed: {type(exc).__name__}: {exc}",
"probe_from": "backend-host",
}
def _parse_env_params(raw: Any) -> Dict[str, str]: def _parse_env_params(raw: Any) -> Dict[str, str]:
return _normalize_env_params(raw) return _normalize_env_params(raw)
@ -2491,31 +2275,6 @@ def update_bot_mcp_config(bot_id: str, payload: BotMcpConfigUpdateRequest, sessi
} }
@app.post("/api/bots/{bot_id}/mcp-config/test")
def test_bot_mcp_config(bot_id: str, payload: BotMcpConfigTestRequest, session: Session = Depends(get_session)):
bot = session.get(BotInstance, bot_id)
if not bot:
raise HTTPException(status_code=404, detail="Bot not found")
cfg = {
"type": str(payload.type or "streamableHttp").strip(),
"url": str(payload.url or "").strip(),
"headers": payload.headers or {},
"toolTimeout": payload.tool_timeout if payload.tool_timeout is not None else 10,
}
result = _probe_mcp_server(cfg, bot_id=bot_id)
if not result.get("ok"):
logger.error(
"mcp probe failed bot_id=%s transport=%s url=%s probe_from=%s status_code=%s message=%s",
bot_id,
result.get("transport"),
cfg.get("url"),
result.get("probe_from"),
result.get("status_code"),
result.get("message"),
)
return result
@app.get("/api/bots/{bot_id}/env-params") @app.get("/api/bots/{bot_id}/env-params")
def get_bot_env_params(bot_id: str, session: Session = Depends(get_session)): def get_bot_env_params(bot_id: str, session: Session = Depends(get_session)):
bot = session.get(BotInstance, bot_id) bot = session.get(BotInstance, bot_id)

View File

@ -195,12 +195,6 @@ export const dashboardEn = {
mcpBotIdPlaceholder: 'e.g. mula_bot_b02', mcpBotIdPlaceholder: 'e.g. mula_bot_b02',
mcpBotSecretPlaceholder: 'Secret for this bot identity', mcpBotSecretPlaceholder: 'Secret for this bot identity',
mcpToolTimeout: 'Tool Timeout (seconds)', mcpToolTimeout: 'Tool Timeout (seconds)',
mcpTest: 'Test Connectivity',
mcpTesting: 'Testing connectivity...',
mcpTestPass: 'Connectivity test passed.',
mcpTestFail: 'Connectivity test failed.',
mcpTestNeedUrl: 'Please provide MCP URL first.',
mcpTestBlockSave: 'MCP connectivity test failed. Save is blocked.',
mcpDraftRequired: 'MCP server name and URL are required.', mcpDraftRequired: 'MCP server name and URL are required.',
mcpDraftAdded: 'Added to the MCP list. Save config to apply.', mcpDraftAdded: 'Added to the MCP list. Save config to apply.',
addMcpServer: 'Add MCP Server', addMcpServer: 'Add MCP Server',

View File

@ -195,12 +195,6 @@ export const dashboardZhCn = {
mcpBotIdPlaceholder: '如 mula_bot_b02', mcpBotIdPlaceholder: '如 mula_bot_b02',
mcpBotSecretPlaceholder: '输入该 Bot 对应的密钥', mcpBotSecretPlaceholder: '输入该 Bot 对应的密钥',
mcpToolTimeout: 'Tool Timeout', mcpToolTimeout: 'Tool Timeout',
mcpTest: '测试连通性',
mcpTesting: '连通性测试中...',
mcpTestPass: '连通性测试通过。',
mcpTestFail: '连通性测试失败。',
mcpTestNeedUrl: '请先填写 MCP URL。',
mcpTestBlockSave: '存在未通过的 MCP 连通性测试,已阻止保存。',
mcpDraftRequired: '请先填写 MCP 服务名称和 URL。', mcpDraftRequired: '请先填写 MCP 服务名称和 URL。',
mcpDraftAdded: '已加入 MCP 列表,记得保存配置。', mcpDraftAdded: '已加入 MCP 列表,记得保存配置。',
addMcpServer: '新增 MCP Server', addMcpServer: '新增 MCP Server',

View File

@ -134,19 +134,6 @@ interface MCPConfigResponse {
status?: string; status?: string;
} }
interface MCPTestResponse {
ok: boolean;
transport?: string;
status_code?: number | null;
message?: string;
probe_from?: string;
}
interface MCPTestState {
status: 'idle' | 'testing' | 'pass' | 'fail';
message: string;
}
interface MCPServerDraft { interface MCPServerDraft {
name: string; name: string;
type: 'streamableHttp' | 'sse'; type: 'streamableHttp' | 'sse';
@ -1051,7 +1038,6 @@ export function BotDashboardModule({
originName: '', originName: '',
}); });
const [expandedMcpByKey, setExpandedMcpByKey] = useState<Record<string, boolean>>({}); const [expandedMcpByKey, setExpandedMcpByKey] = useState<Record<string, boolean>>({});
const [mcpTestByIndex, setMcpTestByIndex] = useState<Record<number, MCPTestState>>({});
const [envDraftKey, setEnvDraftKey] = useState(''); const [envDraftKey, setEnvDraftKey] = useState('');
const [envDraftValue, setEnvDraftValue] = useState(''); const [envDraftValue, setEnvDraftValue] = useState('');
const [envDraftVisible, setEnvDraftVisible] = useState(false); const [envDraftVisible, setEnvDraftVisible] = useState(false);
@ -2419,7 +2405,6 @@ export function BotDashboardModule({
}); });
return next; return next;
}); });
setMcpTestByIndex({});
return drafts; return drafts;
}; };
@ -2913,7 +2898,6 @@ export function BotDashboardModule({
const updateMcpServer = (index: number, patch: Partial<MCPServerDraft>) => { const updateMcpServer = (index: number, patch: Partial<MCPServerDraft>) => {
setMcpServers((prev) => prev.map((row, i) => (i === index ? { ...row, ...patch } : row))); setMcpServers((prev) => prev.map((row, i) => (i === index ? { ...row, ...patch } : row)));
setMcpTestByIndex((prev) => ({ ...prev, [index]: { status: 'idle', message: '' } }));
}; };
const canRemoveMcpServer = (row?: MCPServerDraft | null) => { const canRemoveMcpServer = (row?: MCPServerDraft | null) => {
@ -2937,16 +2921,7 @@ export function BotDashboardModule({
}); });
return next; return next;
}); });
setMcpTestByIndex((prev) => { await saveBotMcpConfig(nextRows);
const next: Record<number, MCPTestState> = {};
Object.entries(prev).forEach(([key, val]) => {
const idx = Number(key);
if (idx < index) next[idx] = val;
if (idx > index) next[idx - 1] = val;
});
return next;
});
await saveBotMcpConfig(nextRows, { skipConnectivityTest: true });
}; };
const buildMcpHeaders = (row: MCPServerDraft): Record<string, string> => { const buildMcpHeaders = (row: MCPServerDraft): Record<string, string> => {
@ -2969,62 +2944,17 @@ export function BotDashboardModule({
return headers; return headers;
}; };
const testSingleMcpServer = async (row: MCPServerDraft, index: number): Promise<boolean> => {
if (!selectedBot) return false;
const url = String(row.url || '').trim();
if (!url) {
setMcpTestByIndex((prev) => ({
...prev,
[index]: { status: 'fail', message: t.mcpTestNeedUrl },
}));
return false;
}
const timeout = Math.max(1, Math.min(600, Number(row.toolTimeout || 60) || 60));
setMcpTestByIndex((prev) => ({
...prev,
[index]: { status: 'testing', message: t.mcpTesting },
}));
try {
const res = await axios.post<MCPTestResponse>(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/mcp-config/test`, {
type: row.type === 'sse' ? 'sse' : 'streamableHttp',
url,
headers: buildMcpHeaders(row),
tool_timeout: timeout,
});
const ok = Boolean(res.data?.ok);
const baseMsg = String(res.data?.message || '').trim() || (ok ? t.mcpTestPass : t.mcpTestFail);
const probeFrom = String(res.data?.probe_from || '').trim();
const msg = probeFrom ? `${baseMsg} (${probeFrom})` : baseMsg;
setMcpTestByIndex((prev) => ({
...prev,
[index]: { status: ok ? 'pass' : 'fail', message: msg },
}));
return ok;
} catch (error: any) {
const msg = error?.response?.data?.detail || t.mcpTestFail;
setMcpTestByIndex((prev) => ({
...prev,
[index]: { status: 'fail', message: String(msg) },
}));
return false;
}
};
const saveBotMcpConfig = async ( const saveBotMcpConfig = async (
rows: MCPServerDraft[] = mcpServers, rows: MCPServerDraft[] = mcpServers,
options?: { closeDraft?: boolean; expandedKey?: string; skipConnectivityTest?: boolean }, options?: { closeDraft?: boolean; expandedKey?: string },
) => { ) => {
if (!selectedBot) return; if (!selectedBot) return;
const mcp_servers: Record<string, MCPServerConfig> = {}; const mcp_servers: Record<string, MCPServerConfig> = {};
const testQueue: Array<{ index: number; row: MCPServerDraft }> = []; for (const row of rows) {
for (const [index, row] of rows.entries()) {
const name = String(row.name || '').trim(); const name = String(row.name || '').trim();
const url = String(row.url || '').trim(); const url = String(row.url || '').trim();
if (!name || !url) continue; if (!name || !url) continue;
const timeout = Math.max(1, Math.min(600, Number(row.toolTimeout || 60) || 60)); const timeout = Math.max(1, Math.min(600, Number(row.toolTimeout || 60) || 60));
if (!row.locked) {
testQueue.push({ index, row });
}
mcp_servers[name] = { mcp_servers[name] = {
type: row.type === 'sse' ? 'sse' : 'streamableHttp', type: row.type === 'sse' ? 'sse' : 'streamableHttp',
url, url,
@ -3034,16 +2964,6 @@ export function BotDashboardModule({
} }
setIsSavingMcp(true); setIsSavingMcp(true);
try { try {
if (!options?.skipConnectivityTest) {
for (const item of testQueue) {
const ok = await testSingleMcpServer(item.row, item.index);
if (!ok) {
notify(t.mcpTestBlockSave, { tone: 'error' });
setIsSavingMcp(false);
return;
}
}
}
await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/mcp-config`, { mcp_servers }); await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/mcp-config`, { mcp_servers });
if (options?.expandedKey) { if (options?.expandedKey) {
setExpandedMcpByKey({ [options.expandedKey]: true }); setExpandedMcpByKey({ [options.expandedKey]: true });
@ -6496,14 +6416,6 @@ export function BotDashboardModule({
<div className="ops-config-collapsed-meta">{summary}</div> <div className="ops-config-collapsed-meta">{summary}</div>
</div> </div>
<div className="ops-config-card-actions"> <div className="ops-config-card-actions">
<button
className="btn btn-secondary btn-sm"
onClick={() => void testSingleMcpServer(row, idx)}
disabled={mcpTestByIndex[idx]?.status === 'testing' || isSavingMcp}
>
{mcpTestByIndex[idx]?.status === 'testing' ? <RefreshCw size={14} className="animate-spin" /> : <Check size={14} />}
<span style={{ marginLeft: 6 }}>{t.mcpTest}</span>
</button>
<LucentIconButton <LucentIconButton
className="btn btn-danger btn-sm wizard-icon-btn" className="btn btn-danger btn-sm wizard-icon-btn"
disabled={isSavingMcp || !canRemoveMcpServer(row)} disabled={isSavingMcp || !canRemoveMcpServer(row)}
@ -6602,11 +6514,6 @@ export function BotDashboardModule({
) : null} ) : null}
</> </>
) : null} ) : null}
{mcpTestByIndex[idx]?.status !== 'idle' ? (
<div className="field-label" style={{ color: mcpTestByIndex[idx]?.status === 'pass' ? 'var(--ok)' : mcpTestByIndex[idx]?.status === 'fail' ? 'var(--err)' : 'var(--muted)' }}>
{mcpTestByIndex[idx]?.message}
</div>
) : null}
</div> </div>
); );
}) })