From 394b971856a0c2e4324b2d2b3ce099c0ec563109 Mon Sep 17 00:00:00 2001 From: Chaim Date: Mon, 4 May 2026 06:38:47 +0000 Subject: [PATCH] feat(settings): add MCP registrations endpoint + Coolify volume runbook Co-Authored-By: Claude Sonnet 4.6 --- docs/runbooks/coolify-mcp-settings-volumes.md | 38 ++++++++ web/app.py | 7 ++ web/mcp_registrations.py | 95 +++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 docs/runbooks/coolify-mcp-settings-volumes.md create mode 100644 web/mcp_registrations.py diff --git a/docs/runbooks/coolify-mcp-settings-volumes.md b/docs/runbooks/coolify-mcp-settings-volumes.md new file mode 100644 index 0000000..3e1c04a --- /dev/null +++ b/docs/runbooks/coolify-mcp-settings-volumes.md @@ -0,0 +1,38 @@ + +# Coolify Volume Mounts ל-MCP Settings Page + +## רקע + +טאב **Registrations** בדף `/settings` קורא רישומי MCP מתוך: +- `~/.claude.json` (host) +- `~/.paperclip/instances/*/mcp.json` (host) + +הקונטיינר של legal-ai חייב גישת קריאה לקבצים אלה דרך volume mounts. +בלי המאונט, ה-endpoint יחזיר `error: "host_path_unavailable"` והטאב יציג הודעת אי-זמינות. + +## הוראות + +1. פתח Coolify UI: `http://158.178.131.193:8000`. +2. נווט לאפליקציה: legal-ai (UUID `gyjo0mtw2c42ej3xxvbz8zio`). +3. לשונית **Storages** → **Add Storage**. +4. הוסף שני mounts: + + | Source path (host) | Destination path (container) | Mode | + |---|---|---| + | `/home/chaim/.claude.json` | `/host/.claude.json` | `ro` | + | `/home/chaim/.paperclip` | `/host/.paperclip` | `ro` | + +5. שמור ולחץ **Redeploy**. + +## אימות + +אחרי ה-redeploy: +```bash +curl -s https://legal-ai.nautilus.marcusgroup.org/api/settings/mcp/registrations | jq +``` +צריך להחזיר `"error": null` ורשימת רישומים. + +## הערה אבטחה + +המאונטים הם read-only. ה-endpoint לא מחזיר ערכי env (רק שמות keys), +ולא מאפשר לעדכן את הקבצים. diff --git a/web/app.py b/web/app.py index 8888c66..2a3212b 100644 --- a/web/app.py +++ b/web/app.py @@ -2818,6 +2818,13 @@ async def api_mcp_tools(): return {"tools": tools, "count": len(tools)} +@app.get("/api/settings/mcp/registrations") +async def api_mcp_registrations(): + """List MCP server registrations from host config files.""" + from web.mcp_registrations import list_registrations + return list_registrations() + + # ── Settings: Tag → Company Mappings ────────────────────────────── @app.get("/api/settings/paperclip-companies") diff --git a/web/mcp_registrations.py b/web/mcp_registrations.py new file mode 100644 index 0000000..30d204c --- /dev/null +++ b/web/mcp_registrations.py @@ -0,0 +1,95 @@ +# web/mcp_registrations.py +"""Read MCP server registrations from host config files mounted in /host.""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + +HOST_DIR = Path("/host") + + +def _redact_env_keys(env: dict[str, Any] | None) -> list[str]: + if not env or not isinstance(env, dict): + return [] + return sorted(env.keys()) + + +def _read_claude_registrations() -> list[dict[str, Any]]: + """Read MCP registrations from /host/.claude.json.""" + path = HOST_DIR / ".claude.json" + if not path.exists(): + return [] + try: + data = json.loads(path.read_text(encoding="utf-8")) + except Exception as e: + logger.warning("claude_json_parse_failed: %s", e) + return [] + out: list[dict[str, Any]] = [] + # Top-level mcpServers + for name, cfg in (data.get("mcpServers") or {}).items(): + out.append(_normalize("Claude Code (global)", name, cfg)) + # Per-project mcpServers + for project_path, project_cfg in (data.get("projects") or {}).items(): + for name, cfg in (project_cfg.get("mcpServers") or {}).items(): + out.append(_normalize(f"Claude Code ({project_path})", name, cfg)) + return out + + +def _read_paperclip_registrations() -> list[dict[str, Any]]: + """Read MCP registrations from /host/.paperclip/instances/*/mcp.json.""" + base = HOST_DIR / ".paperclip" / "instances" + if not base.exists(): + return [] + out: list[dict[str, Any]] = [] + for instance_dir in sorted(base.iterdir()): + if not instance_dir.is_dir(): + continue + mcp_json = instance_dir / "mcp.json" + if not mcp_json.exists(): + continue + try: + data = json.loads(mcp_json.read_text(encoding="utf-8")) + except Exception as e: + logger.warning( + "paperclip_mcp_json_parse_failed: %s %s", + instance_dir.name, e, + ) + continue + for name, cfg in (data.get("mcpServers") or data or {}).items(): + if not isinstance(cfg, dict): + continue + out.append(_normalize(f"Paperclip ({instance_dir.name})", name, cfg)) + return out + + +def _normalize(client: str, server_name: str, cfg: dict[str, Any]) -> dict[str, Any]: + return { + "client": client, + "server_name": server_name, + "command": cfg.get("command", ""), + "args": cfg.get("args") or [], + "cwd": cfg.get("cwd") or cfg.get("workingDirectory") or "", + "env_keys": _redact_env_keys(cfg.get("env")), + "transport": cfg.get("transport") or "stdio", + } + + +def list_registrations() -> dict[str, Any]: + """Return all MCP registrations + status.""" + if not HOST_DIR.exists(): + return { + "registrations": [], + "error": "host_path_unavailable", + "message": "תיקיית /host לא mounted. ראה runbook להגדרת volumes ב-Coolify.", + } + return { + "registrations": ( + _read_claude_registrations() + _read_paperclip_registrations() + ), + "error": None, + }