feat(settings): add MCP registrations endpoint + Coolify volume runbook
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
38
docs/runbooks/coolify-mcp-settings-volumes.md
Normal file
38
docs/runbooks/coolify-mcp-settings-volumes.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<!-- docs/runbooks/coolify-mcp-settings-volumes.md -->
|
||||||
|
# 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),
|
||||||
|
ולא מאפשר לעדכן את הקבצים.
|
||||||
@@ -2818,6 +2818,13 @@ async def api_mcp_tools():
|
|||||||
return {"tools": tools, "count": len(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 ──────────────────────────────
|
# ── Settings: Tag → Company Mappings ──────────────────────────────
|
||||||
|
|
||||||
@app.get("/api/settings/paperclip-companies")
|
@app.get("/api/settings/paperclip-companies")
|
||||||
|
|||||||
95
web/mcp_registrations.py
Normal file
95
web/mcp_registrations.py
Normal file
@@ -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,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user