From e095901f612d0d439bb073bbf9d87419cb5e5472 Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 12 Aug 2026 22:53:35 +0800 Subject: [PATCH] Slim MCP sync Admin status and stop page-wide horizontal overflow. Co-authored-by: Cursor --- interfaces/admin/routes.py | 15 ++++- interfaces/admin/static/js/pages/plugins.js | 71 +++++++++++++++++---- interfaces/admin/static/styles.css | 17 ++++- tests/test_mcp_admin_api.py | 21 ++++-- 4 files changed, 102 insertions(+), 22 deletions(-) diff --git a/interfaces/admin/routes.py b/interfaces/admin/routes.py index 1520037f..6c446a6c 100644 --- a/interfaces/admin/routes.py +++ b/interfaces/admin/routes.py @@ -3884,7 +3884,13 @@ def build_admin_router() -> APIRouter: invalidate_tool_wire_cache(reason=f"mcp_admin_sync:{server_id}") except Exception: pass - return {"ok": True, "server_id": server_id, "tools": norm, "compat_mode": "bailian_webparser"} + return { + "ok": True, + "server_id": server_id, + "synced_tools": len(norm), + "tool_names": [str(t.get("tool_name") or "") for t in norm if str(t.get("tool_name") or "").strip()], + "compat_mode": "bailian_webparser", + } rt = mcp_runtime_for_row(row, store=store) try: response = rt.tools_list() @@ -3913,7 +3919,12 @@ def build_admin_router() -> APIRouter: invalidate_tool_wire_cache(reason=f"mcp_admin_sync:{server_id}") except Exception: pass - return {"ok": True, "server_id": server_id, "tools": norm} + return { + "ok": True, + "server_id": server_id, + "synced_tools": len(norm), + "tool_names": [str(t.get("tool_name") or "") for t in norm if str(t.get("tool_name") or "").strip()], + } finally: rt.stop() diff --git a/interfaces/admin/static/js/pages/plugins.js b/interfaces/admin/static/js/pages/plugins.js index a2850c05..1be78086 100644 --- a/interfaces/admin/static/js/pages/plugins.js +++ b/interfaces/admin/static/js/pages/plugins.js @@ -2,6 +2,52 @@ import { t, el, tdCell, apiGet, apiGetNoHang, apiPost, renderPageShell, markPrew const PLUGINS_PAGE_SIZE = 15; +/** Compact Admin status line — never dump full tools[] / schemas (blows page width + noise). */ +function summarizeApiStatus(prefix, r) { + const p = String(prefix || "").trim() || "[api]"; + if (r == null) return `${p} (empty)`; + if (typeof r !== "object") return `${p} ${String(r).slice(0, 160)}`; + if (r.ok === false) { + const err = String(r.error_code || r.error || r.detail || "error").slice(0, 120); + return `${p} fail: ${err}`; + } + if (Array.isArray(r.tools)) { + const names = r.tools + .map((x) => String((x && (x.tool_name || x.name)) || "").trim()) + .filter(Boolean); + const shown = names.slice(0, 10).join(", "); + const more = names.length > 10 ? ` (+${names.length - 10})` : ""; + const sid = r.server_id ? ` server=${r.server_id}` : ""; + return `${p} ok${sid} · ${names.length} tools${shown ? `: ${shown}${more}` : ""}`; + } + if (Array.isArray(r.tool_names)) { + const names = r.tool_names.map((x) => String(x || "").trim()).filter(Boolean); + const shown = names.slice(0, 10).join(", "); + const more = names.length > 10 ? ` (+${names.length - 10})` : ""; + const n = Number(r.synced_tools || names.length || 0); + const sid = r.server_id ? ` server=${r.server_id}` : ""; + return `${p} ok${sid} · ${n} tools${shown ? `: ${shown}${more}` : ""}`; + } + if (Array.isArray(r.items)) { + return `${p} ok · ${r.items.length} item(s)`; + } + if (Array.isArray(r.results)) { + const okN = r.results.filter((x) => x && x.ok !== false).length; + return `${p} ok · ${okN}/${r.results.length} result(s)`; + } + if (r.status != null) { + const sid = r.server_id ? ` server=${r.server_id}` : ""; + return `${p} ${String(r.status)}${sid}`; + } + const bits = []; + for (const k of ["ok", "server_id", "enabled", "deleted", "synced_tools", "compat_mode"]) { + if (r[k] == null || typeof r[k] === "object") continue; + bits.push(`${k}=${String(r[k]).slice(0, 40)}`); + if (bits.length >= 5) break; + } + return bits.length ? `${p} ${bits.join(" · ")}` : `${p} ok`; +} + function pluginsFold(summaryText, innerNodes) { const det = el("details", { class: "details plugins-fold" }); det.appendChild(el("summary", { text: summaryText })); @@ -135,9 +181,9 @@ async function renderPlugins() { repaintPlugins(); const pluginPager = pluginsPagerBar(pluginTotalHolder, pluginPageRef, repaintPlugins); - const bindingStatus = el("div", { class: "muted", text: "" }); - const installStatus = el("div", { class: "muted", text: "" }); - const toolPolicyStatus = el("div", { class: "muted", text: "" }); + const bindingStatus = el("div", { class: "muted plugins-status", text: "" }); + const installStatus = el("div", { class: "muted plugins-status", text: "" }); + const toolPolicyStatus = el("div", { class: "muted plugins-status", text: "" }); const turnMaxWorkersInput = el("input", { class: "input u-max-w-120", @@ -248,9 +294,10 @@ async function renderPlugins() { const unknownRetry = Array.isArray(r.unknown_retryable_error_codes) ? r.unknown_retryable_error_codes : []; if (unknownRetry.length) { toolPolicyStatus.textContent = - `[tool-policy] saved with warnings: unknown_retryable_error_codes=${unknownRetry.join(", ")} | ` + JSON.stringify(r); + summarizeApiStatus("[tool-policy]", r) + + ` · warn unknown_retryable_error_codes=${unknownRetry.join(", ")}`; } else { - toolPolicyStatus.textContent = `[tool-policy] ` + JSON.stringify(r); + toolPolicyStatus.textContent = summarizeApiStatus("[tool-policy]", r); } toolPolicyStatus.textContent += " | restart gateway/desktop to apply run_command toggle"; }, @@ -411,7 +458,7 @@ async function renderPlugins() { text: t("plugins.action.saveBinding"), onclick: async () => { const r = await apiPost("/admin/api/mcp/binding", { mapping: bindingDraft }); - bindingStatus.textContent = `[binding] ` + JSON.stringify(r); + bindingStatus.textContent = summarizeApiStatus("[binding]", r); markPrewarmReminder("mcp_binding_changed"); try { const fresh = await apiGet("/admin/api/mcp/binding"); @@ -462,7 +509,7 @@ async function renderPlugins() { installStatus.textContent = "[install] installing..."; try { const res = await apiPost("/admin/api/mcp/install", parsed); - installStatus.textContent = JSON.stringify(res); + installStatus.textContent = summarizeApiStatus("[install]", res); if (res && res.ok) { markPrewarmReminder("mcp_installed"); await softReloadMcpServers(); @@ -485,7 +532,7 @@ async function renderPlugins() { return; } if (!r || r.ok !== true || !r.document) { - installStatus.textContent = "[export] failed: " + JSON.stringify(r); + installStatus.textContent = "[export] failed: " + summarizeApiStatus("[export]", r); return; } const text = JSON.stringify(r.document, null, 2) + "\n"; @@ -594,7 +641,7 @@ async function renderPlugins() { enabled: !!editEnabledCb.checked, timeout_s: Number(editTimeoutInput.value || 30), }); - editStatus.textContent = JSON.stringify(r); + editStatus.textContent = summarizeApiStatus("[edit]", r); if (r && r.ok) { closeEditModal(); markPrewarmReminder("mcp_config_updated"); @@ -663,7 +710,7 @@ async function renderPlugins() { label: t("plugins.action.health"), onClick: async () => { const r = await apiPost("/admin/api/mcp/healthcheck", { server_id: sid }); - installStatus.textContent = `[health:${sid}] ` + JSON.stringify(r); + installStatus.textContent = summarizeApiStatus(`[health:${sid}]`, r); await softReloadMcpServers(); }, }, @@ -671,7 +718,7 @@ async function renderPlugins() { label: t("plugins.action.syncTools"), onClick: async () => { const r = await apiPost("/admin/api/mcp/tools/sync", { server_id: sid }); - installStatus.textContent = `[sync:${sid}] ` + JSON.stringify(r); + installStatus.textContent = summarizeApiStatus(`[sync:${sid}]`, r); await softReloadMcpServers(); }, }, @@ -681,7 +728,7 @@ async function renderPlugins() { onClick: async () => { if (!window.confirm(tf("plugins.confirm.delete", { sid }))) return; const r = await apiPost("/admin/api/mcp/delete", { server_id: sid }); - installStatus.textContent = `[delete:${sid}] ` + JSON.stringify(r); + installStatus.textContent = summarizeApiStatus(`[delete:${sid}]`, r); markPrewarmReminder("mcp_deleted"); await softReloadMcpServers(); }, diff --git a/interfaces/admin/static/styles.css b/interfaces/admin/static/styles.css index 930ee602..de3482ca 100644 --- a/interfaces/admin/static/styles.css +++ b/interfaces/admin/static/styles.css @@ -70,7 +70,7 @@ body { background: var(--ds-bg, #070b14); } -.layout { display: flex; height: 100vh; } +.layout { display: flex; height: 100vh; width: 100%; max-width: 100vw; overflow-x: hidden; } .sidebar { width: 260px; background: var(--ds-sidebar, #0c1220); @@ -295,7 +295,9 @@ body { } .content { padding: 20px 22px 32px; - overflow: auto; + overflow-x: hidden; + overflow-y: auto; + min-width: 0; color: var(--ds-text, #e2e8f0); background: radial-gradient(1200px 480px at 12% -10%, rgba(37, 99, 235, 0.12), transparent 55%), @@ -308,6 +310,8 @@ body { flex-direction: column; gap: 14px; max-width: 1280px; + min-width: 0; + width: 100%; } .page-shell__header { display: flex; @@ -1046,6 +1050,15 @@ body.col-resize-active { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--ds-border, rgba(148, 163, 184, 0.12)); + min-width: 0; + max-width: 100%; +} +.plugins-status { + max-width: 100%; + min-width: 0; + overflow-wrap: anywhere; + word-break: break-word; + white-space: pre-wrap; } .plugins-pager .btn.btn--small { min-width: 72px; } .badge { diff --git a/tests/test_mcp_admin_api.py b/tests/test_mcp_admin_api.py index aa44a4b5..da087d25 100644 --- a/tests/test_mcp_admin_api.py +++ b/tests/test_mcp_admin_api.py @@ -190,9 +190,14 @@ class McpAdminApiTests(unittest.TestCase): sync = self.client.post("/admin/api/mcp/tools/sync", json={"server_id": "dummy"}, headers=self._headers()) self.assertEqual(sync.status_code, 200) - self.assertTrue(sync.json().get("ok"), sync.json()) - tools = sync.json().get("tools") or [] - self.assertTrue(any(str(t.get("tool_name") or "") == "ping" for t in tools)) + body = sync.json() + self.assertTrue(body.get("ok"), body) + self.assertNotIn("tools", body) # avoid dumping full schemas into Admin UI + names = body.get("tool_names") or [] + self.assertIn("ping", names) + self.assertEqual(int(body.get("synced_tools") or 0), len(names)) + stored = store.list_mcp_server_tools(server_id="dummy") + self.assertTrue(any(str(t.get("tool_name") or "") == "ping" for t in stored)) def test_healthcheck_and_tools_sync_bailian_webparser_compat(self) -> None: store = get_assistant_store() @@ -217,9 +222,13 @@ class McpAdminApiTests(unittest.TestCase): sync = self.client.post("/admin/api/mcp/tools/sync", json={"server_id": "webparser-compat"}, headers=self._headers()) self.assertEqual(sync.status_code, 200) - self.assertTrue(sync.json().get("ok"), sync.json()) - tools = sync.json().get("tools") or [] - self.assertTrue(any(str(t.get("tool_name") or "") == "bailian_webparser_parse" for t in tools)) + body = sync.json() + self.assertTrue(body.get("ok"), body) + self.assertNotIn("tools", body) + names = body.get("tool_names") or [] + self.assertIn("bailian_webparser_parse", names) + stored = store.list_mcp_server_tools(server_id="webparser-compat") + self.assertTrue(any(str(t.get("tool_name") or "") == "bailian_webparser_parse" for t in stored)) def test_reinstall_from_saved_manifest(self) -> None: script = self._write_mcp_server()