Trigger async runtime prewarm immediately after Admin MCP tools sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-12 22:59:17 +08:00
parent e095901f61
commit 91c5a1a754
3 changed files with 31 additions and 5 deletions

View file

@ -129,6 +129,23 @@ def _bailian_webparser_virtual_tools() -> list[dict[str, Any]]:
return bailian_webparser_virtual_tools() 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]: def _http_get_json(url: str, *, timeout: float = 8.0) -> dict[str, Any]:
req = urllib_request.Request( req = urllib_request.Request(
url, url,
@ -3884,12 +3901,14 @@ 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
prewarm = _spawn_async_runtime_prewarm(reason=f"mcp_admin_sync:{server_id}")
return { return {
"ok": True, "ok": True,
"server_id": server_id, "server_id": server_id,
"synced_tools": len(norm), "synced_tools": len(norm),
"tool_names": [str(t.get("tool_name") or "") for t in norm if str(t.get("tool_name") or "").strip()], "tool_names": [str(t.get("tool_name") or "") for t in norm if str(t.get("tool_name") or "").strip()],
"compat_mode": "bailian_webparser", "compat_mode": "bailian_webparser",
"prewarm": prewarm,
} }
rt = mcp_runtime_for_row(row, store=store) rt = mcp_runtime_for_row(row, store=store)
try: try:
@ -3919,11 +3938,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
prewarm = _spawn_async_runtime_prewarm(reason=f"mcp_admin_sync:{server_id}")
return { return {
"ok": True, "ok": True,
"server_id": server_id, "server_id": server_id,
"synced_tools": len(norm), "synced_tools": len(norm),
"tool_names": [str(t.get("tool_name") or "") for t in norm if str(t.get("tool_name") or "").strip()], "tool_names": [str(t.get("tool_name") or "") for t in norm if str(t.get("tool_name") or "").strip()],
"prewarm": prewarm,
} }
finally: finally:
rt.stop() rt.stop()

View file

@ -11,6 +11,8 @@ function summarizeApiStatus(prefix, r) {
const err = String(r.error_code || r.error || r.detail || "error").slice(0, 120); const err = String(r.error_code || r.error || r.detail || "error").slice(0, 120);
return `${p} fail: ${err}`; return `${p} fail: ${err}`;
} }
const prewarmTail =
r.prewarm && typeof r.prewarm === "object" && r.prewarm.accepted ? " · prewarm=async" : "";
if (Array.isArray(r.tools)) { if (Array.isArray(r.tools)) {
const names = r.tools const names = r.tools
.map((x) => String((x && (x.tool_name || x.name)) || "").trim()) .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 shown = names.slice(0, 10).join(", ");
const more = names.length > 10 ? ` (+${names.length - 10})` : ""; const more = names.length > 10 ? ` (+${names.length - 10})` : "";
const sid = r.server_id ? ` server=${r.server_id}` : ""; 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)) { if (Array.isArray(r.tool_names)) {
const names = r.tool_names.map((x) => String(x || "").trim()).filter(Boolean); 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 more = names.length > 10 ? ` (+${names.length - 10})` : "";
const n = Number(r.synced_tools || names.length || 0); const n = Number(r.synced_tools || names.length || 0);
const sid = r.server_id ? ` server=${r.server_id}` : ""; 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)) { 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)) { if (Array.isArray(r.results)) {
const okN = r.results.filter((x) => x && x.ok !== false).length; 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) { if (r.status != null) {
const sid = r.server_id ? ` server=${r.server_id}` : ""; const sid = r.server_id ? ` server=${r.server_id}` : "";
return `${p} ${String(r.status)}${sid}`; return `${p} ${String(r.status)}${sid}${prewarmTail}`;
} }
const bits = []; const bits = [];
for (const k of ["ok", "server_id", "enabled", "deleted", "synced_tools", "compat_mode"]) { 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)}`); bits.push(`${k}=${String(r[k]).slice(0, 40)}`);
if (bits.length >= 5) break; if (bits.length >= 5) break;
} }
if (prewarmTail) bits.push("prewarm=async");
return bits.length ? `${p} ${bits.join(" · ")}` : `${p} ok`; return bits.length ? `${p} ${bits.join(" · ")}` : `${p} ok`;
} }

View file

@ -196,6 +196,7 @@ class McpAdminApiTests(unittest.TestCase):
names = body.get("tool_names") or [] names = body.get("tool_names") or []
self.assertIn("ping", names) self.assertIn("ping", names)
self.assertEqual(int(body.get("synced_tools") or 0), len(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") stored = store.list_mcp_server_tools(server_id="dummy")
self.assertTrue(any(str(t.get("tool_name") or "") == "ping" for t in stored)) 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) self.assertNotIn("tools", body)
names = body.get("tool_names") or [] names = body.get("tool_names") or []
self.assertIn("bailian_webparser_parse", names) self.assertIn("bailian_webparser_parse", names)
self.assertTrue((body.get("prewarm") or {}).get("accepted"))
stored = store.list_mcp_server_tools(server_id="webparser-compat") 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)) self.assertTrue(any(str(t.get("tool_name") or "") == "bailian_webparser_parse" for t in stored))