mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 00:40:45 +08:00
109 lines
4 KiB
Python
109 lines
4 KiB
Python
"""Build a reinstallable MCP JSON from SQLite and persist under ``oclaw/_local/`` for migration."""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import warnings
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from oclaw.platform.config.paths import PROJECT_ROOT
|
||
from oclaw.platform.persistence.sqlite_store import SqliteStore
|
||
from oclaw.runtime.tools.mcp.registry import McpRegistry
|
||
|
||
_EXPORT_FILENAME = "mcp_registry_migrated.json"
|
||
_LOCAL_DIR = (PROJECT_ROOT / "_local").resolve()
|
||
_EXPORT_PATH = (_LOCAL_DIR / _EXPORT_FILENAME).resolve()
|
||
|
||
|
||
def mcp_migrated_json_path() -> Path:
|
||
return _EXPORT_PATH
|
||
|
||
|
||
def _collapse_entry_arg(arg: str, root: Path) -> str:
|
||
"""Rewrite absolute / repo-relative paths to ``__REPO_ROOT__/...`` when they exist on disk under root.
|
||
|
||
Skips npx flags (``-y``) and tokens that are not an existing file or directory, so package names
|
||
(e.g. ``mcp-sqlite``, ``@upstash/foo``) are not mistaken for paths.
|
||
"""
|
||
t = (arg or "").strip()
|
||
if not t or t.startswith("__REPO_ROOT__") or t.startswith("-"):
|
||
return str(arg)
|
||
r = root.resolve()
|
||
a = Path(os.path.expanduser(t))
|
||
if not a.is_absolute():
|
||
a = (r / t).resolve()
|
||
else:
|
||
a = a.resolve()
|
||
if not a.exists():
|
||
return str(arg)
|
||
try:
|
||
rel = a.relative_to(r)
|
||
except ValueError:
|
||
return str(arg)
|
||
return "__REPO_ROOT__/" + rel.as_posix()
|
||
|
||
|
||
def _collapse_entry_args(args: list[str], root: Path) -> list[str]:
|
||
return [_collapse_entry_arg(x, root) for x in args]
|
||
|
||
|
||
def build_mcp_install_export_document(store: SqliteStore) -> dict[str, Any]:
|
||
rows = McpRegistry(store).list_servers(enabled_only=False)
|
||
root = PROJECT_ROOT.resolve()
|
||
servers: list[dict[str, Any]] = []
|
||
for r in rows:
|
||
if not isinstance(r, dict):
|
||
continue
|
||
raw_args = r.get("entry_args")
|
||
if isinstance(raw_args, list):
|
||
entry_args = _collapse_entry_args([str(x) for x in raw_args if str(x).strip()], root)
|
||
else:
|
||
entry_args = []
|
||
es = r.get("env_schema")
|
||
env_schema = es if isinstance(es, dict) else {}
|
||
perms = r.get("required_permissions")
|
||
if not isinstance(perms, list):
|
||
perms = []
|
||
servers.append(
|
||
{
|
||
"server_id": str(r.get("server_id") or ""),
|
||
"source_type": str(r.get("source_type") or ""),
|
||
"source_ref": str(r.get("source_ref") or ""),
|
||
"version": str(r.get("version") or ""),
|
||
"entry_command": str(r.get("entry_command") or ""),
|
||
"entry_args": entry_args,
|
||
"env_schema": env_schema,
|
||
"required_permissions": [str(x) for x in perms if str(x).strip()],
|
||
"risk_level": str(r.get("risk_level") or "high"),
|
||
"enabled": bool(r.get("enabled")),
|
||
"timeout_s": float(r.get("timeout_s") or 30.0),
|
||
"dry_run": False,
|
||
}
|
||
)
|
||
return {
|
||
"_comment": "管理台导出 / 新安装后自动落盘。可粘到「Install from JSON」;`__REPO_ROOT__/` 在管理台与 seed 中展开为仓库根。已存在路径会写成占位符,其余参数保持原样。",
|
||
"exported_at": datetime.now(timezone.utc).isoformat(),
|
||
"servers": servers,
|
||
}
|
||
|
||
|
||
def persist_mcp_migrated_file(store: SqliteStore) -> str | None:
|
||
"""Write ``oclaw/_local/mcp_registry_migrated.json``. Fails open (warn only). Returns path or None."""
|
||
try:
|
||
mcp_migrated_json_path().parent.mkdir(parents=True, exist_ok=True)
|
||
doc = build_mcp_install_export_document(store)
|
||
path = mcp_migrated_json_path()
|
||
path.write_text(json.dumps(doc, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||
return str(path)
|
||
except OSError as exc:
|
||
warnings.warn(f"mcp_registry_migrated write failed: {exc}", RuntimeWarning, stacklevel=1)
|
||
return None
|
||
|
||
|
||
__all__ = [
|
||
"build_mcp_install_export_document",
|
||
"mcp_migrated_json_path",
|
||
"persist_mcp_migrated_file",
|
||
]
|