Slim MCP sync Admin status and stop page-wide horizontal overflow.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-12 22:53:35 +08:00
parent 02f2c69deb
commit e095901f61
4 changed files with 102 additions and 22 deletions

View file

@ -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()

View file

@ -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();
},

View file

@ -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 {

View file

@ -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()