mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-11 05:20:44 +08:00
Slim MCP sync Admin status and stop page-wide horizontal overflow.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
02f2c69deb
commit
e095901f61
4 changed files with 102 additions and 22 deletions
|
|
@ -3884,7 +3884,13 @@ def build_admin_router() -> APIRouter:
|
||||||
invalidate_tool_wire_cache(reason=f"mcp_admin_sync:{server_id}")
|
invalidate_tool_wire_cache(reason=f"mcp_admin_sync:{server_id}")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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)
|
rt = mcp_runtime_for_row(row, store=store)
|
||||||
try:
|
try:
|
||||||
response = rt.tools_list()
|
response = rt.tools_list()
|
||||||
|
|
@ -3913,7 +3919,12 @@ def build_admin_router() -> APIRouter:
|
||||||
invalidate_tool_wire_cache(reason=f"mcp_admin_sync:{server_id}")
|
invalidate_tool_wire_cache(reason=f"mcp_admin_sync:{server_id}")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
finally:
|
||||||
rt.stop()
|
rt.stop()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,52 @@ import { t, el, tdCell, apiGet, apiGetNoHang, apiPost, renderPageShell, markPrew
|
||||||
|
|
||||||
const PLUGINS_PAGE_SIZE = 15;
|
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) {
|
function pluginsFold(summaryText, innerNodes) {
|
||||||
const det = el("details", { class: "details plugins-fold" });
|
const det = el("details", { class: "details plugins-fold" });
|
||||||
det.appendChild(el("summary", { text: summaryText }));
|
det.appendChild(el("summary", { text: summaryText }));
|
||||||
|
|
@ -135,9 +181,9 @@ async function renderPlugins() {
|
||||||
repaintPlugins();
|
repaintPlugins();
|
||||||
const pluginPager = pluginsPagerBar(pluginTotalHolder, pluginPageRef, repaintPlugins);
|
const pluginPager = pluginsPagerBar(pluginTotalHolder, pluginPageRef, repaintPlugins);
|
||||||
|
|
||||||
const bindingStatus = el("div", { class: "muted", text: "" });
|
const bindingStatus = el("div", { class: "muted plugins-status", text: "" });
|
||||||
const installStatus = el("div", { class: "muted", text: "" });
|
const installStatus = el("div", { class: "muted plugins-status", text: "" });
|
||||||
const toolPolicyStatus = el("div", { class: "muted", text: "" });
|
const toolPolicyStatus = el("div", { class: "muted plugins-status", text: "" });
|
||||||
|
|
||||||
const turnMaxWorkersInput = el("input", {
|
const turnMaxWorkersInput = el("input", {
|
||||||
class: "input u-max-w-120",
|
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 : [];
|
const unknownRetry = Array.isArray(r.unknown_retryable_error_codes) ? r.unknown_retryable_error_codes : [];
|
||||||
if (unknownRetry.length) {
|
if (unknownRetry.length) {
|
||||||
toolPolicyStatus.textContent =
|
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 {
|
} else {
|
||||||
toolPolicyStatus.textContent = `[tool-policy] ` + JSON.stringify(r);
|
toolPolicyStatus.textContent = summarizeApiStatus("[tool-policy]", r);
|
||||||
}
|
}
|
||||||
toolPolicyStatus.textContent += " | restart gateway/desktop to apply run_command toggle";
|
toolPolicyStatus.textContent += " | restart gateway/desktop to apply run_command toggle";
|
||||||
},
|
},
|
||||||
|
|
@ -411,7 +458,7 @@ async function renderPlugins() {
|
||||||
text: t("plugins.action.saveBinding"),
|
text: t("plugins.action.saveBinding"),
|
||||||
onclick: async () => {
|
onclick: async () => {
|
||||||
const r = await apiPost("/admin/api/mcp/binding", { mapping: bindingDraft });
|
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");
|
markPrewarmReminder("mcp_binding_changed");
|
||||||
try {
|
try {
|
||||||
const fresh = await apiGet("/admin/api/mcp/binding");
|
const fresh = await apiGet("/admin/api/mcp/binding");
|
||||||
|
|
@ -462,7 +509,7 @@ async function renderPlugins() {
|
||||||
installStatus.textContent = "[install] installing...";
|
installStatus.textContent = "[install] installing...";
|
||||||
try {
|
try {
|
||||||
const res = await apiPost("/admin/api/mcp/install", parsed);
|
const res = await apiPost("/admin/api/mcp/install", parsed);
|
||||||
installStatus.textContent = JSON.stringify(res);
|
installStatus.textContent = summarizeApiStatus("[install]", res);
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
markPrewarmReminder("mcp_installed");
|
markPrewarmReminder("mcp_installed");
|
||||||
await softReloadMcpServers();
|
await softReloadMcpServers();
|
||||||
|
|
@ -485,7 +532,7 @@ async function renderPlugins() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!r || r.ok !== true || !r.document) {
|
if (!r || r.ok !== true || !r.document) {
|
||||||
installStatus.textContent = "[export] failed: " + JSON.stringify(r);
|
installStatus.textContent = "[export] failed: " + summarizeApiStatus("[export]", r);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const text = JSON.stringify(r.document, null, 2) + "\n";
|
const text = JSON.stringify(r.document, null, 2) + "\n";
|
||||||
|
|
@ -594,7 +641,7 @@ async function renderPlugins() {
|
||||||
enabled: !!editEnabledCb.checked,
|
enabled: !!editEnabledCb.checked,
|
||||||
timeout_s: Number(editTimeoutInput.value || 30),
|
timeout_s: Number(editTimeoutInput.value || 30),
|
||||||
});
|
});
|
||||||
editStatus.textContent = JSON.stringify(r);
|
editStatus.textContent = summarizeApiStatus("[edit]", r);
|
||||||
if (r && r.ok) {
|
if (r && r.ok) {
|
||||||
closeEditModal();
|
closeEditModal();
|
||||||
markPrewarmReminder("mcp_config_updated");
|
markPrewarmReminder("mcp_config_updated");
|
||||||
|
|
@ -663,7 +710,7 @@ async function renderPlugins() {
|
||||||
label: t("plugins.action.health"),
|
label: t("plugins.action.health"),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const r = await apiPost("/admin/api/mcp/healthcheck", { server_id: sid });
|
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();
|
await softReloadMcpServers();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -671,7 +718,7 @@ async function renderPlugins() {
|
||||||
label: t("plugins.action.syncTools"),
|
label: t("plugins.action.syncTools"),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const r = await apiPost("/admin/api/mcp/tools/sync", { server_id: sid });
|
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();
|
await softReloadMcpServers();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -681,7 +728,7 @@ async function renderPlugins() {
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
if (!window.confirm(tf("plugins.confirm.delete", { sid }))) return;
|
if (!window.confirm(tf("plugins.confirm.delete", { sid }))) return;
|
||||||
const r = await apiPost("/admin/api/mcp/delete", { server_id: sid });
|
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");
|
markPrewarmReminder("mcp_deleted");
|
||||||
await softReloadMcpServers();
|
await softReloadMcpServers();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ body {
|
||||||
background: var(--ds-bg, #070b14);
|
background: var(--ds-bg, #070b14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout { display: flex; height: 100vh; }
|
.layout { display: flex; height: 100vh; width: 100%; max-width: 100vw; overflow-x: hidden; }
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 260px;
|
width: 260px;
|
||||||
background: var(--ds-sidebar, #0c1220);
|
background: var(--ds-sidebar, #0c1220);
|
||||||
|
|
@ -295,7 +295,9 @@ body {
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
padding: 20px 22px 32px;
|
padding: 20px 22px 32px;
|
||||||
overflow: auto;
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-width: 0;
|
||||||
color: var(--ds-text, #e2e8f0);
|
color: var(--ds-text, #e2e8f0);
|
||||||
background:
|
background:
|
||||||
radial-gradient(1200px 480px at 12% -10%, rgba(37, 99, 235, 0.12), transparent 55%),
|
radial-gradient(1200px 480px at 12% -10%, rgba(37, 99, 235, 0.12), transparent 55%),
|
||||||
|
|
@ -308,6 +310,8 @@ body {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.page-shell__header {
|
.page-shell__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1046,6 +1050,15 @@ body.col-resize-active {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
border-top: 1px solid var(--ds-border, rgba(148, 163, 184, 0.12));
|
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; }
|
.plugins-pager .btn.btn--small { min-width: 72px; }
|
||||||
.badge {
|
.badge {
|
||||||
|
|
|
||||||
|
|
@ -190,9 +190,14 @@ class McpAdminApiTests(unittest.TestCase):
|
||||||
|
|
||||||
sync = self.client.post("/admin/api/mcp/tools/sync", json={"server_id": "dummy"}, headers=self._headers())
|
sync = self.client.post("/admin/api/mcp/tools/sync", json={"server_id": "dummy"}, headers=self._headers())
|
||||||
self.assertEqual(sync.status_code, 200)
|
self.assertEqual(sync.status_code, 200)
|
||||||
self.assertTrue(sync.json().get("ok"), sync.json())
|
body = sync.json()
|
||||||
tools = sync.json().get("tools") or []
|
self.assertTrue(body.get("ok"), body)
|
||||||
self.assertTrue(any(str(t.get("tool_name") or "") == "ping" for t in tools))
|
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:
|
def test_healthcheck_and_tools_sync_bailian_webparser_compat(self) -> None:
|
||||||
store = get_assistant_store()
|
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())
|
sync = self.client.post("/admin/api/mcp/tools/sync", json={"server_id": "webparser-compat"}, headers=self._headers())
|
||||||
self.assertEqual(sync.status_code, 200)
|
self.assertEqual(sync.status_code, 200)
|
||||||
self.assertTrue(sync.json().get("ok"), sync.json())
|
body = sync.json()
|
||||||
tools = sync.json().get("tools") or []
|
self.assertTrue(body.get("ok"), body)
|
||||||
self.assertTrue(any(str(t.get("tool_name") or "") == "bailian_webparser_parse" for t in tools))
|
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:
|
def test_reinstall_from_saved_manifest(self) -> None:
|
||||||
script = self._write_mcp_server()
|
script = self._write_mcp_server()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue