Ship Cursor-only MCP admin and modular Admin/Chat UI.

Plugins/Skills install and edit via mcpServers JSON, with clearer row actions, soft reloads, and instant loading placeholders; also drop MCP market and harden related scheduler/logging/exec guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-12 19:38:26 +08:00
parent 2aff012b48
commit f6f129931f
61 changed files with 18057 additions and 16327 deletions

View file

@ -1,16 +1,25 @@
from .manifest import McpServerManifest
from .installer import McpInstallResult, install_mcp_server
from .runtime import McpProcessRuntime
from __future__ import annotations
from .adapter import materialize_mcp_tools
from .cursor_config import (
build_cursor_mcp_export,
parse_cursor_mcp_document,
registry_row_to_cursor_server,
)
from .installer import McpInstallResult, install_mcp_server, uninstall_mcp_server
from .manifest import McpServerManifest
from .registry import McpRegistry
from .market import search_mcp_market
from .runtime import McpProcessRuntime
__all__ = [
"McpServerManifest",
"McpInstallResult",
"McpProcessRuntime",
"McpRegistry",
"McpServerManifest",
"build_cursor_mcp_export",
"install_mcp_server",
"materialize_mcp_tools",
"McpRegistry",
"search_mcp_market",
"parse_cursor_mcp_document",
"registry_row_to_cursor_server",
"uninstall_mcp_server",
]

View file

@ -150,6 +150,13 @@ class _McpBoundTool:
def _handler(args: dict[str, Any]) -> dict[str, Any]:
call_args = dict(args or {})
from runtime.chat.exec_managed_ne_guard import (
is_exec_managed_ne_tool,
normalize_exec_managed_ne_args,
)
if is_exec_managed_ne_tool(tool_name):
call_args = normalize_exec_managed_ne_args(call_args)
cache_ttl = _mcp_list_cache_ttl(tool_name)
cache_key = ""
if cache_ttl is not None:
@ -180,7 +187,7 @@ class _McpBoundTool:
res["hint"] = (
f"Cache {tool_name} results briefly; reuse ids/rows instead of listing again in the same turn."
)
if tool_name == "execManagedNe" and res.get("ok") is False:
if is_exec_managed_ne_tool(tool_name) and res.get("ok") is False:
from runtime.tools.tool_error_hints import enrich_exec_managed_ne_error
res = enrich_exec_managed_ne_error(res)

View file

@ -0,0 +1,313 @@
# -*- coding: utf-8 -*-
"""Cursor-style ``mcpServers`` JSON ↔ oclaw MCP registry payloads."""
from __future__ import annotations
import json
import re
from typing import Any
_ENV_VAR_RE = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
_REMOTE_TYPES = {
"streamablehttp",
"streamable_http",
"streamable-http",
"http",
"sse",
}
def _safe_server_id(seed: str) -> str:
v = re.sub(r"[^a-zA-Z0-9._-]+", "-", str(seed or "").strip().lower()).strip("-")
return v or "mcp-server"
def _env_schema_from_cursor_env(env: dict[str, Any] | None) -> dict[str, Any]:
out: dict[str, Any] = {}
if not isinstance(env, dict):
return out
for ek, ev in env.items():
name = str(ek or "").strip()
if not name:
continue
out[name] = {
"type": "string",
"default": "" if ev is None else str(ev),
"description": "From mcpServers env",
}
return out
def _env_schema_from_header_placeholders(headers: dict[str, Any]) -> dict[str, Any]:
out: dict[str, Any] = {}
for hk, hv in headers.items():
for m in _ENV_VAR_RE.finditer(str(hv or "")):
env_name = str(m.group(1) or "").strip()
if not env_name or env_name in out:
continue
out[env_name] = {
"required": True,
"type": "string",
"description": f"Auto-detected from header {str(hk or '').strip()}",
}
return out
def _cursor_env_from_schema(env_schema: dict[str, Any] | None) -> dict[str, str]:
out: dict[str, str] = {}
if not isinstance(env_schema, dict):
return out
for ek, spec in env_schema.items():
name = str(ek or "").strip()
if not name:
continue
if isinstance(spec, dict):
default = spec.get("default")
out[name] = "" if default is None else str(default)
else:
out[name] = str(spec)
return out
def _is_remote_server(server: dict[str, Any]) -> bool:
transport = str(server.get("type") or "").strip().lower().replace(" ", "")
url = str(server.get("url") or server.get("baseUrl") or "").strip()
if transport in _REMOTE_TYPES:
return True
# Cursor often omits type and only sets url for remote servers.
if url and not str(server.get("command") or server.get("entry_command") or "").strip():
return True
return False
def cursor_server_to_install_payload(server_id: str, server: dict[str, Any] | None) -> dict[str, Any]:
"""Convert one Cursor mcpServers entry into an oclaw install/upsert payload."""
sid = _safe_server_id(server_id)
s = server if isinstance(server, dict) else {}
headers = s.get("headers") if isinstance(s.get("headers"), dict) else {}
env_schema = _env_schema_from_header_placeholders(headers)
enabled = bool(s["isActive"]) if "isActive" in s else True
if "enabled" in s:
enabled = bool(s.get("enabled"))
timeout_s = float(s.get("timeout_s") or 30.0)
if _is_remote_server(s):
base_url = str(s.get("url") or s.get("baseUrl") or "").strip()
if not base_url:
raise ValueError(f"remote_mcp_missing_url:{sid}")
entry_args = ["-y", "mcp-remote", base_url]
for hk, hv in headers.items():
hn = str(hk or "").strip()
hvs = str(hv or "").strip()
if not hn or not hvs:
continue
entry_args.extend(["--header", f"{hn}: {hvs}"])
return {
"server_id": sid,
"source_type": "npm",
"source_ref": "mcp-remote",
"version": "",
"entry_command": "npx",
"entry_args": entry_args,
"env_schema": env_schema,
"required_permissions": [],
"risk_level": "high",
"enabled": enabled,
"timeout_s": timeout_s,
}
cmd = str(s.get("command") or s.get("entry_command") or "").strip()
if not cmd:
raise ValueError(f"stdio_mcp_missing_command:{sid}")
args_raw = s.get("args") if isinstance(s.get("args"), list) else s.get("entry_args")
args = [str(x) for x in (args_raw or [])]
env_from_cursor = _env_schema_from_cursor_env(s.get("env") if isinstance(s.get("env"), dict) else {})
merged = {**env_from_cursor, **env_schema}
if isinstance(s.get("env_schema"), dict):
merged.update(s["env_schema"])
return {
"server_id": sid,
"source_type": "local",
"source_ref": sid,
"version": "",
"entry_command": cmd,
"entry_args": args,
"env_schema": merged,
"required_permissions": [],
"risk_level": "high",
"enabled": enabled,
"timeout_s": timeout_s,
}
def parse_cursor_mcp_document(doc: Any) -> list[dict[str, Any]]:
"""Parse Cursor ``{ mcpServers: {...} }`` (or bare mcpServers object) into install payloads."""
if not isinstance(doc, dict):
raise ValueError("mcp_document_must_be_object")
servers_obj = doc.get("mcpServers")
if servers_obj is None and all(isinstance(v, dict) for v in doc.values()) and doc:
# Allow pasting the inner map directly if every value looks like a server config.
if any(k in next(iter(doc.values()), {}) for k in ("command", "url", "args", "type")):
servers_obj = doc
if not isinstance(servers_obj, dict) or not servers_obj:
raise ValueError("mcpServers_required")
out: list[dict[str, Any]] = []
for raw_key, raw_server in servers_obj.items():
key = str(raw_key or "").strip()
if not key:
continue
out.append(cursor_server_to_install_payload(key, raw_server if isinstance(raw_server, dict) else {}))
if not out:
raise ValueError("mcpServers_empty")
return out
def _try_parse_mcp_remote(entry_command: str, entry_args: list[str]) -> dict[str, Any] | None:
cmd = str(entry_command or "").strip().lower()
args = [str(x) for x in (entry_args or [])]
if cmd not in {"npx", "npx.cmd"} and not cmd.endswith("npx") and not cmd.endswith("npx.cmd"):
# Still allow if args contain mcp-remote (custom launcher).
if "mcp-remote" not in args:
return None
if "mcp-remote" not in args:
return None
url = ""
headers: dict[str, str] = {}
i = 0
while i < len(args):
a = args[i]
if a == "mcp-remote":
i += 1
continue
if a in {"-y", "--yes"}:
i += 1
continue
if a == "--header" and i + 1 < len(args):
hv = args[i + 1]
if ":" in hv:
hk, hval = hv.split(":", 1)
headers[hk.strip()] = hval.strip()
i += 2
continue
if not url and not a.startswith("-"):
url = a
i += 1
continue
i += 1
if not url:
return None
out: dict[str, Any] = {"url": url}
if headers:
out["headers"] = headers
return out
def registry_row_to_cursor_server(row: dict[str, Any]) -> dict[str, Any]:
"""Convert one registry row into a Cursor mcpServers value."""
entry_command = str(row.get("entry_command") or "").strip()
raw_args = row.get("entry_args")
entry_args = [str(x) for x in raw_args] if isinstance(raw_args, list) else []
remote = _try_parse_mcp_remote(entry_command, entry_args)
env = _cursor_env_from_schema(row.get("env_schema") if isinstance(row.get("env_schema"), dict) else {})
if remote is not None:
if env:
# Keep env for header placeholders / runtime.
remote = dict(remote)
remote["env"] = env
return remote
out: dict[str, Any] = {"command": entry_command or "npx", "args": entry_args}
if env:
out["env"] = env
return out
def build_cursor_mcp_export(rows: list[dict[str, Any]]) -> dict[str, Any]:
servers: dict[str, Any] = {}
for row in rows:
if not isinstance(row, dict):
continue
sid = str(row.get("server_id") or "").strip()
if not sid:
continue
servers[sid] = registry_row_to_cursor_server(row)
return {"mcpServers": servers}
def config_payload_to_upsert_fields(payload: dict[str, Any], *, existing: dict[str, Any]) -> dict[str, Any]:
"""Merge Admin edit payload (Cursor-ish) onto an existing registry row."""
base = dict(existing)
sid = str(payload.get("server_id") or base.get("server_id") or "").strip()
if not sid:
raise ValueError("server_id_required")
# Prefer a nested cursor server object when provided.
cursor_server = payload.get("server") if isinstance(payload.get("server"), dict) else None
if cursor_server is None and (
"command" in payload or "url" in payload or "args" in payload or "env" in payload or "headers" in payload
):
cursor_server = {
k: payload.get(k)
for k in ("command", "args", "env", "url", "baseUrl", "headers", "type", "isActive", "enabled", "timeout_s")
if k in payload
}
if cursor_server is not None:
converted = cursor_server_to_install_payload(sid, cursor_server)
if "enabled" in payload:
converted["enabled"] = bool(payload.get("enabled"))
if "timeout_s" in payload:
converted["timeout_s"] = float(payload.get("timeout_s") or 30.0)
return converted
# Field-level updates without full Cursor object.
entry_command = str(payload.get("entry_command") if "entry_command" in payload else base.get("entry_command") or "").strip()
if "entry_args" in payload:
entry_args = [str(x) for x in (payload.get("entry_args") or [])] if isinstance(payload.get("entry_args"), list) else []
else:
raw = base.get("entry_args")
entry_args = [str(x) for x in raw] if isinstance(raw, list) else []
if "env" in payload and isinstance(payload.get("env"), dict):
env_schema = _env_schema_from_cursor_env(payload.get("env"))
elif "env_schema" in payload and isinstance(payload.get("env_schema"), dict):
env_schema = dict(payload.get("env_schema") or {})
else:
env_schema = dict(base.get("env_schema") or {}) if isinstance(base.get("env_schema"), dict) else {}
enabled = bool(payload.get("enabled")) if "enabled" in payload else bool(base.get("enabled"))
timeout_s = float(payload.get("timeout_s") if "timeout_s" in payload else (base.get("timeout_s") or 30.0))
source_type = str(base.get("source_type") or "local")
source_ref = str(base.get("source_ref") or sid)
# If editing looks like mcp-remote, keep npm/mcp-remote markers.
if "mcp-remote" in entry_args or source_ref == "mcp-remote":
source_type = "npm"
source_ref = "mcp-remote"
elif source_type not in {"github", "npm", "pypi", "local"}:
source_type = "local"
source_ref = sid
return {
"server_id": sid,
"source_type": source_type,
"source_ref": source_ref,
"version": str(base.get("version") or ""),
"entry_command": entry_command,
"entry_args": entry_args,
"env_schema": env_schema,
"required_permissions": list(base.get("required_permissions") or [])
if isinstance(base.get("required_permissions"), list)
else [],
"risk_level": str(base.get("risk_level") or "high"),
"enabled": enabled,
"timeout_s": timeout_s,
}
def dumps_cursor_export(rows: list[dict[str, Any]]) -> str:
return json.dumps(build_cursor_mcp_export(rows), ensure_ascii=False, indent=2) + "\n"
__all__ = [
"build_cursor_mcp_export",
"config_payload_to_upsert_fields",
"cursor_server_to_install_payload",
"dumps_cursor_export",
"parse_cursor_mcp_document",
"registry_row_to_cursor_server",
]

View file

@ -1,153 +0,0 @@
from __future__ import annotations
from typing import Any
import time
import httpx
_TRENDING_CACHE: dict[str, Any] = {"ts": 0.0, "items": []}
_TRENDING_TTL_S = 1800
def _safe_get_json(url: str, *, params: dict[str, Any] | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
try:
with httpx.Client(timeout=8.0, follow_redirects=True) as c:
r = c.get(url, params=params or {}, headers=headers or {})
if r.status_code != 200:
return {}
obj = r.json()
return obj if isinstance(obj, dict) else {}
except Exception:
return {}
def search_github_repos(query: str, *, limit: int = 8) -> list[dict[str, Any]]:
q = str(query or "").strip()
if not q:
return []
blob = _safe_get_json(
"https://api.github.com/search/repositories",
params={"q": f"{q} mcp server", "sort": "stars", "order": "desc", "per_page": max(1, min(limit, 20))},
headers={"Accept": "application/vnd.github+json"},
)
items = blob.get("items") if isinstance(blob.get("items"), list) else []
out: list[dict[str, Any]] = []
for it in items[:limit]:
if not isinstance(it, dict):
continue
out.append(
{
"source_type": "github",
"name": str(it.get("full_name") or ""),
"source_ref": str(it.get("clone_url") or it.get("html_url") or ""),
"description": str(it.get("description") or ""),
"version": "",
"homepage": str(it.get("html_url") or ""),
"stars": int(it.get("stargazers_count") or 0),
"install_template": infer_install_template("github", str(it.get("clone_url") or it.get("html_url") or "")),
}
)
return out
def search_npm_packages(query: str, *, limit: int = 8) -> list[dict[str, Any]]:
q = str(query or "").strip()
if not q:
return []
blob = _safe_get_json(
"https://registry.npmjs.org/-/v1/search",
params={"text": f"{q} mcp", "size": max(1, min(limit, 20))},
)
items = blob.get("objects") if isinstance(blob.get("objects"), list) else []
out: list[dict[str, Any]] = []
for it in items[:limit]:
pkg = it.get("package") if isinstance(it, dict) else None
if not isinstance(pkg, dict):
continue
out.append(
{
"source_type": "npm",
"name": str(pkg.get("name") or ""),
"source_ref": str(pkg.get("name") or ""),
"description": str(pkg.get("description") or ""),
"version": str(pkg.get("version") or ""),
"homepage": str(pkg.get("links", {}).get("npm") if isinstance(pkg.get("links"), dict) else ""),
"stars": 0,
"install_template": infer_install_template("npm", str(pkg.get("name") or "")),
}
)
return out
def search_pypi_packages(query: str, *, limit: int = 8) -> list[dict[str, Any]]:
q = str(query or "").strip()
if not q:
return []
blob = _safe_get_json(
"https://pypi.org/search/",
params={"q": f"{q} mcp"},
headers={"Accept": "application/json"},
)
# PyPI JSON search API is not officially stable; keep best-effort.
projects = blob.get("projects") if isinstance(blob.get("projects"), list) else []
out: list[dict[str, Any]] = []
for it in projects[:limit]:
if not isinstance(it, dict):
continue
name = str(it.get("name") or "")
out.append(
{
"source_type": "pypi",
"name": name,
"source_ref": name,
"description": str(it.get("description") or ""),
"version": str(it.get("version") or ""),
"homepage": f"https://pypi.org/project/{name}/" if name else "",
"stars": 0,
"install_template": infer_install_template("pypi", name),
}
)
return out
def search_mcp_market(query: str, *, per_source_limit: int = 6) -> list[dict[str, Any]]:
lim = max(1, min(int(per_source_limit or 6), 20))
out: list[dict[str, Any]] = []
out.extend(search_github_repos(query, limit=lim))
out.extend(search_npm_packages(query, limit=lim))
out.extend(search_pypi_packages(query, limit=lim))
return out
def infer_install_template(source_type: str, source_ref: str) -> dict[str, Any]:
st = str(source_type or "").strip().lower()
sr = str(source_ref or "").strip()
if st == "npm":
pkg = sr.split("/")[-1] if sr else ""
return {"entry_command": "npx", "entry_args": [pkg] if pkg else []}
if st == "pypi":
pkg = sr.replace("-", "_")
return {"entry_command": "python", "entry_args": ["-m", pkg] if pkg else []}
return {"entry_command": "python", "entry_args": []}
def trending_mcp_market(*, force_refresh: bool = False, per_source_limit: int = 5) -> list[dict[str, Any]]:
now = time.time()
if not force_refresh and _TRENDING_CACHE["items"] and (now - float(_TRENDING_CACHE["ts"] or 0.0) < _TRENDING_TTL_S):
return list(_TRENDING_CACHE["items"])
items = search_mcp_market("mcp", per_source_limit=per_source_limit)
items = sorted(items, key=lambda x: int(x.get("stars") or 0), reverse=True)
_TRENDING_CACHE["ts"] = now
_TRENDING_CACHE["items"] = list(items)
return items
__all__ = [
"search_mcp_market",
"search_github_repos",
"search_npm_packages",
"search_pypi_packages",
"infer_install_template",
"trending_mcp_market",
]