From 91c5a1a75433cffd07a8e99cb099f864466f8f82 Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 12 Aug 2026 22:59:17 +0800 Subject: [PATCH] Trigger async runtime prewarm immediately after Admin MCP tools sync. Co-authored-by: Cursor --- interfaces/admin/routes.py | 21 +++++++++++++++++++++ interfaces/admin/static/js/pages/plugins.js | 13 ++++++++----- tests/test_mcp_admin_api.py | 2 ++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/interfaces/admin/routes.py b/interfaces/admin/routes.py index 6c446a6c..ea710fd4 100644 --- a/interfaces/admin/routes.py +++ b/interfaces/admin/routes.py @@ -129,6 +129,23 @@ def _bailian_webparser_virtual_tools() -> list[dict[str, Any]]: return bailian_webparser_virtual_tools() +def _spawn_async_runtime_prewarm(*, reason: str) -> dict[str, Any]: + """Fire-and-forget prewarm after Admin MCP catalog changes (does not block the HTTP response).""" + import threading + + reason_s = str(reason or "mcp_admin_sync").strip() or "mcp_admin_sync" + + def _run() -> None: + try: + run_runtime_prewarm(reason=reason_s, store=get_assistant_store()) + except Exception: + pass + + th = threading.Thread(target=_run, name="oclaw-mcp-sync-prewarm", daemon=True) + th.start() + return {"accepted": True, "mode": "async", "reason": reason_s} + + def _http_get_json(url: str, *, timeout: float = 8.0) -> dict[str, Any]: req = urllib_request.Request( url, @@ -3884,12 +3901,14 @@ def build_admin_router() -> APIRouter: invalidate_tool_wire_cache(reason=f"mcp_admin_sync:{server_id}") except Exception: pass + prewarm = _spawn_async_runtime_prewarm(reason=f"mcp_admin_sync:{server_id}") 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", + "prewarm": prewarm, } rt = mcp_runtime_for_row(row, store=store) try: @@ -3919,11 +3938,13 @@ def build_admin_router() -> APIRouter: invalidate_tool_wire_cache(reason=f"mcp_admin_sync:{server_id}") except Exception: pass + prewarm = _spawn_async_runtime_prewarm(reason=f"mcp_admin_sync:{server_id}") 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()], + "prewarm": prewarm, } finally: rt.stop() diff --git a/interfaces/admin/static/js/pages/plugins.js b/interfaces/admin/static/js/pages/plugins.js index 1be78086..5f479b8d 100644 --- a/interfaces/admin/static/js/pages/plugins.js +++ b/interfaces/admin/static/js/pages/plugins.js @@ -11,6 +11,8 @@ function summarizeApiStatus(prefix, r) { const err = String(r.error_code || r.error || r.detail || "error").slice(0, 120); return `${p} fail: ${err}`; } + const prewarmTail = + r.prewarm && typeof r.prewarm === "object" && r.prewarm.accepted ? " · prewarm=async" : ""; if (Array.isArray(r.tools)) { const names = r.tools .map((x) => String((x && (x.tool_name || x.name)) || "").trim()) @@ -18,7 +20,7 @@ function summarizeApiStatus(prefix, r) { 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}` : ""}`; + return `${p} ok${sid} · ${names.length} tools${shown ? `: ${shown}${more}` : ""}${prewarmTail}`; } if (Array.isArray(r.tool_names)) { const names = r.tool_names.map((x) => String(x || "").trim()).filter(Boolean); @@ -26,18 +28,18 @@ function summarizeApiStatus(prefix, r) { 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}` : ""}`; + return `${p} ok${sid} · ${n} tools${shown ? `: ${shown}${more}` : ""}${prewarmTail}`; } if (Array.isArray(r.items)) { - return `${p} ok · ${r.items.length} item(s)`; + return `${p} ok · ${r.items.length} item(s)${prewarmTail}`; } 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)`; + return `${p} ok · ${okN}/${r.results.length} result(s)${prewarmTail}`; } if (r.status != null) { const sid = r.server_id ? ` server=${r.server_id}` : ""; - return `${p} ${String(r.status)}${sid}`; + return `${p} ${String(r.status)}${sid}${prewarmTail}`; } const bits = []; for (const k of ["ok", "server_id", "enabled", "deleted", "synced_tools", "compat_mode"]) { @@ -45,6 +47,7 @@ function summarizeApiStatus(prefix, r) { bits.push(`${k}=${String(r[k]).slice(0, 40)}`); if (bits.length >= 5) break; } + if (prewarmTail) bits.push("prewarm=async"); return bits.length ? `${p} ${bits.join(" · ")}` : `${p} ok`; } diff --git a/tests/test_mcp_admin_api.py b/tests/test_mcp_admin_api.py index da087d25..247d89e8 100644 --- a/tests/test_mcp_admin_api.py +++ b/tests/test_mcp_admin_api.py @@ -196,6 +196,7 @@ class McpAdminApiTests(unittest.TestCase): names = body.get("tool_names") or [] self.assertIn("ping", names) self.assertEqual(int(body.get("synced_tools") or 0), len(names)) + self.assertTrue((body.get("prewarm") or {}).get("accepted")) stored = store.list_mcp_server_tools(server_id="dummy") self.assertTrue(any(str(t.get("tool_name") or "") == "ping" for t in stored)) @@ -227,6 +228,7 @@ class McpAdminApiTests(unittest.TestCase): self.assertNotIn("tools", body) names = body.get("tool_names") or [] self.assertIn("bailian_webparser_parse", names) + self.assertTrue((body.get("prewarm") or {}).get("accepted")) 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))