mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-08 23:33:16 +08:00
Prefer report tools without hard-hiding CLI, keep soft CLI budgets, store compact+result_ref for the model, and clear frozen wire after MCP sync. Co-authored-by: Cursor <cursoragent@cursor.com>
180 lines
6.5 KiB
Python
180 lines
6.5 KiB
Python
"""Sync MCP tools/list into SQLite without wire-penalty / health gating.
|
|
|
|
Health is recorded when available but must not block persisting a successful tools/list.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
def is_bailian_webparser_remote(*, entry_command: str, entry_args: list[str]) -> bool:
|
|
cmd = str(entry_command or "").strip().lower()
|
|
if cmd not in {"npx", "npx.cmd", "node"}:
|
|
return False
|
|
joined = " ".join(str(x or "").strip().lower() for x in (entry_args or []))
|
|
return "mcp-remote" in joined and "/api/v1/mcps/webparser/sse" in joined
|
|
|
|
|
|
def bailian_webparser_virtual_tools() -> list[dict[str, Any]]:
|
|
return [
|
|
{
|
|
"tool_name": "bailian_webparser_parse",
|
|
"description": "Parse webpage via DashScope WebParser compatibility mode. Requires `url` (http/https).",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"url": {"type": "string", "description": "Target webpage URL (required). Example: https://example.com"},
|
|
"timeout": {"type": "integer", "default": 35, "minimum": 8, "maximum": 90},
|
|
},
|
|
"required": ["url"],
|
|
"additionalProperties": False,
|
|
},
|
|
}
|
|
]
|
|
|
|
|
|
def _normalize_tools_list(items: list[Any]) -> list[dict[str, Any]]:
|
|
norm: list[dict[str, Any]] = []
|
|
for it in items:
|
|
if not isinstance(it, dict):
|
|
continue
|
|
tool_name = str(it.get("tool_name") or it.get("name") or "").strip()
|
|
if not tool_name:
|
|
continue
|
|
params = it.get("parameters") if isinstance(it.get("parameters"), dict) else it.get("inputSchema")
|
|
if not isinstance(params, dict):
|
|
params = {}
|
|
norm.append(
|
|
{
|
|
"tool_name": tool_name,
|
|
"description": str(it.get("description") or ""),
|
|
"parameters": params,
|
|
}
|
|
)
|
|
return norm
|
|
|
|
|
|
def sync_mcp_server_tools(store: Any, row: dict[str, Any]) -> dict[str, Any] | None:
|
|
"""Run tools/list (+ optional health) and persist tools.
|
|
|
|
Unlike the old health-gated path, a failed initialize/health no longer skips sync:
|
|
we always attempt tools/list. Health status is best-effort metadata only.
|
|
"""
|
|
from runtime.tools.mcp.env_config import mcp_runtime_for_row
|
|
|
|
sid = str(row.get("server_id") or "").strip()
|
|
cmd = str(row.get("entry_command") or "").strip()
|
|
args = [str(x) for x in (row.get("entry_args") or []) if str(x).strip()]
|
|
if not sid:
|
|
return None
|
|
if not cmd:
|
|
item: dict[str, Any] = {
|
|
"server_id": sid,
|
|
"ok": False,
|
|
"error_code": "mcp_entry_missing",
|
|
"error": "entry_command_missing",
|
|
"health": {"ok": False, "error_code": "mcp_entry_missing", "error": "entry_command_missing"},
|
|
"tools_synced": 0,
|
|
}
|
|
store.set_mcp_server_health(server_id=sid, status="error", detail=item["health"])
|
|
return item
|
|
|
|
if is_bailian_webparser_remote(entry_command=cmd, entry_args=args):
|
|
tools = bailian_webparser_virtual_tools()
|
|
store.replace_mcp_server_tools(server_id=sid, tools=tools)
|
|
detail = {"synced_tools": len(tools), "compat_mode": "bailian_webparser"}
|
|
store.set_mcp_server_health(server_id=sid, status="ok", detail=detail)
|
|
try:
|
|
from runtime.direct_loop import invalidate_tool_wire_cache
|
|
|
|
invalidate_tool_wire_cache(reason=f"mcp_sync:{sid}")
|
|
except Exception:
|
|
pass
|
|
return {"server_id": sid, "ok": True, "health": detail, "tools_synced": len(tools)}
|
|
|
|
rt = mcp_runtime_for_row(row, store=store)
|
|
health: dict[str, Any] = {}
|
|
try:
|
|
try:
|
|
health = rt.health() if hasattr(rt, "health") else {}
|
|
if not isinstance(health, dict):
|
|
health = {}
|
|
except Exception as exc:
|
|
health = {"ok": False, "error_code": "mcp_healthcheck_failed", "error": f"{type(exc).__name__}: {exc}"}
|
|
|
|
tools_res = rt.tools_list()
|
|
if not bool(tools_res.get("ok")):
|
|
# Persist health note but keep previous tools; do not clear catalog on transient list failure.
|
|
detail = {
|
|
"error_code": str(tools_res.get("error_code") or "mcp_tools_list_invalid"),
|
|
"error": str(tools_res.get("error") or "tools_list_failed"),
|
|
"health": health,
|
|
}
|
|
store.set_mcp_server_health(server_id=sid, status="error", detail=detail)
|
|
return {
|
|
"server_id": sid,
|
|
"ok": False,
|
|
"error_code": detail["error_code"],
|
|
"error": detail["error"],
|
|
"health": health,
|
|
"tools_synced": 0,
|
|
}
|
|
|
|
tools = tools_res.get("tools") if isinstance(tools_res.get("tools"), list) else []
|
|
norm = _normalize_tools_list(tools if isinstance(tools, list) else [])
|
|
store.replace_mcp_server_tools(server_id=sid, tools=norm)
|
|
health_ok = bool(health.get("ok")) if health else True
|
|
store.set_mcp_server_health(
|
|
server_id=sid,
|
|
status="ok" if health_ok else "degraded",
|
|
detail={
|
|
"synced_tools": len(norm),
|
|
"health_ok": health_ok,
|
|
"health": health,
|
|
},
|
|
)
|
|
try:
|
|
from runtime.direct_loop import invalidate_tool_wire_cache
|
|
|
|
invalidate_tool_wire_cache(reason=f"mcp_sync:{sid}")
|
|
except Exception:
|
|
pass
|
|
return {
|
|
"server_id": sid,
|
|
"ok": True,
|
|
"health": health,
|
|
"tools_synced": len(norm),
|
|
"health_ok": health_ok,
|
|
}
|
|
finally:
|
|
try:
|
|
rt.stop()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def sync_enabled_mcp_servers(store: Any) -> dict[str, Any]:
|
|
"""Sync tools for all enabled MCP servers (used by runtime prewarm)."""
|
|
rows = store.list_mcp_servers(enabled_only=True) if store else []
|
|
items: list[dict[str, Any]] = []
|
|
for row in rows or []:
|
|
item = sync_mcp_server_tools(store, row)
|
|
if item is not None:
|
|
items.append(item)
|
|
ok_count = len([x for x in items if bool(x.get("ok"))])
|
|
return {
|
|
"ok": True,
|
|
"total": len(items),
|
|
"ok_count": ok_count,
|
|
"error_count": len(items) - ok_count,
|
|
"items": items,
|
|
}
|
|
|
|
|
|
__all__ = [
|
|
"bailian_webparser_virtual_tools",
|
|
"is_bailian_webparser_remote",
|
|
"sync_enabled_mcp_servers",
|
|
"sync_mcp_server_tools",
|
|
]
|