From 70052b0133a6c70ba69bd9a3f8ccfc52b24d29df Mon Sep 17 00:00:00 2001 From: Chaim Date: Mon, 4 May 2026 05:44:31 +0000 Subject: [PATCH] docs(specs): add design for MCP settings page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings page extension to view and edit MCP server config (env vars, tools, client registrations) — hybrid edit model: non-secrets editable through Infisical, secrets read-only with drift detection vs container. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-04-mcp-settings-page-design.md | 336 ++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-04-mcp-settings-page-design.md diff --git a/docs/superpowers/specs/2026-05-04-mcp-settings-page-design.md b/docs/superpowers/specs/2026-05-04-mcp-settings-page-design.md new file mode 100644 index 0000000..42e9b55 --- /dev/null +++ b/docs/superpowers/specs/2026-05-04-mcp-settings-page-design.md @@ -0,0 +1,336 @@ +# דף הגדרות MCP — איפיון + +**תאריך:** 2026-05-04 +**מצב:** Draft → ממתין לאישור משתמש +**הקשר:** הרחבת `/settings` ב-web-ui עם מידע על MCP server של legal-ai (env vars, tools, registrations). + +--- + +## 1. מטרה + +לתת ליו"ר/מנהל המערכת מקום מרכזי לראות (ולערוך כשבטוח) את כל מצב התצורה של ה-MCP server, בלי לעבור בין Infisical UI, Coolify UI, וקבצי קונפיגורציה מקומיים. + +## 2. גבולות (Scope) + +**בתוך הסקופ:** +- תצוגה + עריכה של env vars לא-סודיים, שמירה ל-Infisical, redeploy ידני של Coolify. +- תצוגה (read-only) של env vars סודיים, עם indicator של drift בין Infisical לקונטיינר. +- תצוגה (read-only) של רשימת tools שה-MCP server חושף (introspection דינמי). +- תצוגה (read-only) של רישומי MCP בקבצי הקונפיגורציה של Claude Code ו-Paperclip. + +**מחוץ לסקופ (אולי בעתיד):** +- Enable/disable של tools בודדים. +- עריכת `~/.claude.json` או `~/.paperclip/...` מ-UI. +- Auth/RBAC חדש (משתמש ב-auth קיים של הדף — אין כרגע). +- ניהול secrets — נשאר ב-Infisical UI. +- Auto-redeploy אחרי שמירה (משתמש לוחץ Redeploy ידנית). + +## 3. ארכיטקטורה + +### 3.1 מבנה דף (Frontend) + +`/settings` הופך לדף מבוסס-טאבים (`shadcn/Tabs`): + +| Tab | תוכן | מצב | +|---|---|---| +| Paperclip | התוכן הקיים: Tag mappings + Companies | קיים, ללא שינוי לוגי | +| Environment | env vars של MCP server, Infisical / Container | חדש, עריכה | +| Tools | רשימת tools של ה-MCP server | חדש, read-only | +| Registrations | רישומי MCP ב-Claude Code ו-Paperclip | חדש, read-only | + +טאב ברירת מחדל: `Paperclip`. + +### 3.2 שכבת Backend (FastAPI ב-`web/app.py`) + +#### Endpoints חדשים + +| Path | Method | תיאור | +|---|---|---| +| `/api/settings/mcp/env` | GET | מחזיר רשימת env vars מאוחדת | +| `/api/settings/mcp/env/{key}` | PATCH | מעדכן ערך ב-Infisical (רק לא-סודיים) | +| `/api/settings/mcp/env/redeploy` | POST | מפעיל Coolify redeploy | +| `/api/settings/mcp/tools` | GET | מחזיר רשימת tools של MCP server | +| `/api/settings/mcp/registrations` | GET | מחזיר רישומי MCP מ-`/host/.claude.json` ומ-`/host/.paperclip/instances/*/mcp.json` | + +#### Catalog של env vars + +קובץ חדש: `web/mcp_env_catalog.py` + +```python +from dataclasses import dataclass +from typing import Literal, Any + +EnvType = Literal["bool", "int", "float", "string", "enum"] +EnvCategory = Literal["multimodal", "rerank", "halacha", "credentials", "connection", "general"] + +@dataclass(frozen=True) +class EnvSpec: + key: str + category: EnvCategory + type: EnvType + description: str + is_secret: bool + is_editable: bool + default: Any = None + min: float | None = None + max: float | None = None + enum_values: list[str] | None = None + +ENV_CATALOG: dict[str, EnvSpec] = { + # multimodal + "MULTIMODAL_ENABLED": EnvSpec("MULTIMODAL_ENABLED", "multimodal", "bool", + "הפעלת page-image embeddings", False, True, default=False), + "MULTIMODAL_MODEL": EnvSpec("MULTIMODAL_MODEL", "multimodal", "string", + "מודל multimodal של Voyage", False, True, default="voyage-multimodal-3"), + "MULTIMODAL_DPI": EnvSpec("MULTIMODAL_DPI", "multimodal", "int", + "DPI ל-rendering של עמוד למודל", False, True, default=144, min=72, max=300), + "MULTIMODAL_THUMB_DPI": EnvSpec("MULTIMODAL_THUMB_DPI", "multimodal", "int", + "DPI ל-thumbnail בתצוגה", False, True, default=96, min=72, max=200), + "MULTIMODAL_TEXT_WEIGHT": EnvSpec("MULTIMODAL_TEXT_WEIGHT", "multimodal", "float", + "משקל text vs image ב-RRF", False, True, default=0.5, min=0.0, max=1.0), + "MULTIMODAL_RRF_K": EnvSpec("MULTIMODAL_RRF_K", "multimodal", "int", + "RRF damping constant", False, True, default=60, min=1, max=200), + # rerank + "VOYAGE_RERANK_ENABLED": EnvSpec("VOYAGE_RERANK_ENABLED", "rerank", "bool", + "הפעלת cross-encoder rerank", False, True, default=False), + "VOYAGE_RERANK_MODEL": EnvSpec("VOYAGE_RERANK_MODEL", "rerank", "string", + "מודל rerank", False, True, default="rerank-2"), + "VOYAGE_RERANK_FETCH_K": EnvSpec("VOYAGE_RERANK_FETCH_K", "rerank", "int", + "מספר candidates לפני rerank", False, True, default=50, min=10, max=200), + # halacha + "HALACHA_AUTO_APPROVE_THRESHOLD": EnvSpec("HALACHA_AUTO_APPROVE_THRESHOLD", + "halacha", "float", "סף confidence ל-auto-approve", + False, True, default=0.80, min=0.0, max=1.0), + # general + "VOYAGE_MODEL": EnvSpec("VOYAGE_MODEL", "general", "string", + "מודל embedding ראשי", False, True, default="voyage-law-2"), + "AUDIT_ENABLED": EnvSpec("AUDIT_ENABLED", "general", "bool", + "הפעלת audit log", False, True, default=True), + # credentials (read-only, masked) + "VOYAGE_API_KEY": EnvSpec("VOYAGE_API_KEY", "credentials", "string", + "Voyage AI API key", True, False), + "GOOGLE_CLOUD_VISION_API_KEY": EnvSpec("GOOGLE_CLOUD_VISION_API_KEY", + "credentials", "string", "Google Cloud Vision API key", True, False), + "INFISICAL_TOKEN": EnvSpec("INFISICAL_TOKEN", "credentials", "string", + "Infisical SDK token", True, False), + # connection (read-only — מסוכן לשנות runtime) + "POSTGRES_URL": EnvSpec("POSTGRES_URL", "connection", "string", + "PostgreSQL connection URL", True, False), + "REDIS_URL": EnvSpec("REDIS_URL", "connection", "string", + "Redis connection URL", False, False), + "DATA_DIR": EnvSpec("DATA_DIR", "connection", "string", + "Data directory path", False, False), +} +``` + +המקור: `mcp-server/src/legal_mcp/config.py`. כל מפתח שלא ב-catalog לא מוצג (whitelist policy). + +#### Response shape של `GET /api/settings/mcp/env` + +```json +{ + "vars": [ + { + "key": "MULTIMODAL_ENABLED", + "category": "multimodal", + "type": "bool", + "description": "הפעלת page-image embeddings", + "is_secret": false, + "is_editable": true, + "default": false, + "infisical_value": "true", + "container_value": "true", + "drift": false, + "min": null, "max": null, "enum_values": null + }, + { + "key": "VOYAGE_API_KEY", + "category": "credentials", + "type": "string", + "description": "Voyage AI API key", + "is_secret": true, + "is_editable": false, + "infisical_value": "****", + "container_value": "****", + "drift": false + } + ], + "infisical_environment": "dev", + "coolify_app_uuid": "gyjo0mtw2c42ej3xxvbz8zio", + "errors": [] +} +``` + +- `infisical_value`: דרך `InfisicalSDKClient.get_secret(...)`. אם יש שגיאה → `null` ועדכון `errors`. +- `container_value`: `os.environ.get(key)`. אם לא מוגדר → `null`. +- `drift`: `infisical_value != container_value` (אחרי normalization של bool/int/float; secrets לא משווים ערכים גולמיים — רק hash). +- ל-secret: שני הערכים מוחזרים מטושטשים (`"****" + last_4`); השוואת drift על ה-hash בלבד. + +#### Save flow ב-`PATCH /api/settings/mcp/env/{key}` + +1. ולידציה: הקיי קיים ב-catalog ו-`is_editable=true`. אם לא → 400. +2. ולידציה לפי type: int/float ב-טווח, bool מוסב מ-string, enum בערכים מותרים. +3. כתיבה ל-Infisical: + ```python + client.update_secret( + project_id=INFISICAL_PROJECT_ID, + environment_slug=INFISICAL_ENV, # "dev" כברירת מחדל + secret_path="/legal-ai", + secret_name=key, + secret_value=str(value), + ) + ``` +4. Audit log: `logger.info("mcp_env_update", extra={"key": key, "value": value if not is_secret else "[masked]"})`. +5. Response: `{"ok": true, "requires_redeploy": true, "message": "נשמר ב-Infisical. נדרש redeploy."}`. + +#### Redeploy flow ב-`POST /api/settings/mcp/env/redeploy` + +1. קריאה ל-Coolify API: `POST /api/v1/deploy?uuid=gyjo0mtw2c42ej3xxvbz8zio&force=false`. +2. אסימון: `COOLIFY_API_TOKEN` (מ-Infisical). +3. Polling: קריאה ל-`/api/v1/deployments/{deployment_uuid}` כל 5 שניות, עד `status="finished"` או `status="failed"` (max 10 דקות). +4. UI מציג סטטוס מתעדכן (פשוט: spinner + הודעת סטטוס; לא נדרש streaming). + +#### Tools introspection ב-`GET /api/settings/mcp/tools` + +```python +from legal_mcp.server import mcp # FastMCP instance + +async def api_mcp_tools(): + tools = await mcp.list_tools() # FastMCP API + return { + "tools": [ + { + "name": t.name, + "description": t.description, + "module": _module_for_tool(t.name), # מ-tools/__init__.py + "params_schema": t.inputSchema, + "source_location": _source_location(t), # f"{file}:{line}" + } + for t in tools + ] + } +``` + +`_module_for_tool` ו-`_source_location` נכתבים ב-`web/mcp_introspection.py` עם קריאת `inspect.getfile()` ו-`inspect.getsourcelines()`. + +#### Registrations ב-`GET /api/settings/mcp/registrations` + +קורא: +1. `/host/.claude.json` — תחת `mcpServers` או `projects..mcpServers`. +2. `/host/.paperclip/instances/*/mcp.json` — לכל instance בנפרד. + +לכל רישום: `{client, instance_name?, server_name, command, args, cwd, env_keys}`. +- `env_keys`: רק שמות, לא ערכים. +- אם command/args מכילים paths רגישים — מוצגים as-is (לא secrets). + +#### Coolify config — volume mounts נדרשים + +לפני שהפיצ'ר עולה לפרודקשן, יש לוודא ב-Coolify (UUID `gyjo0mtw2c42ej3xxvbz8zio`): + +```yaml +volumes: + - /home/chaim/.claude.json:/host/.claude.json:ro + - /home/chaim/.paperclip:/host/.paperclip:ro +``` + +המימוש כולל סקריפט/הוראה אופרטיבית להוסיף את ה-mounts (לא חלק מקוד הפרויקט — שינוי תצורה). + +### 3.3 שכבת Frontend + +#### קובץ קיים: `web-ui/src/lib/api/settings.ts` + +מורחב עם hooks חדשים: + +```ts +// קריאות חדשות +export function useMcpEnv() { /* GET /api/settings/mcp/env */ } +export function useUpdateMcpEnv() { /* PATCH /api/settings/mcp/env/{key} */ } +export function useMcpRedeploy() { /* POST /api/settings/mcp/env/redeploy */ } +export function useMcpTools() { /* GET /api/settings/mcp/tools */ } +export function useMcpRegistrations() { /* GET /api/settings/mcp/registrations */ } +``` + +#### קבצי components חדשים תחת `web-ui/src/app/settings/_components/` + +``` +_components/ +├── paperclip-tab.tsx ← העברת התוכן הקיים מ-page.tsx +├── environment-tab.tsx ← רשימת קבוצות + EnvVarRow +├── env-var-row.tsx ← שורה אחת של env var +├── env-var-editor.tsx ← input controls לפי type +├── tools-tab.tsx ← טבלה + drawer +├── tool-detail-drawer.tsx ← פרטי tool +├── registrations-tab.tsx ← כרטיסים לפי client +└── drift-badge.tsx ← badge ויזואלי +``` + +`page.tsx` הופך לאחראי רק על ה-Tabs ולעטיפה. + +#### חוויית עריכת env var + +לחיצה על שורה → התרחבות (accordion) → הצגת editor + שני ערכים (Infisical / Container) + כפתור "שמור". + +לחיצה על "שמור": +1. PATCH → toast הצלחה: "נשמר ב-Infisical. לחץ Redeploy כדי להחיל בקונטיינר." +2. השורה מסומנת כ-"pending redeploy" עד ה-redeploy הבא. +3. כפתור "Redeploy now" קבוע בתחתית הטאב, מודגש כשיש שינויים pending. + +#### חוויית Tools + +טבלה לפי module. שורה → drawer מימין עם schema + תיאור + מיקום בקוד. + +#### חוויית Registrations + +כרטיס לכל client (Claude Code, Paperclip) → פירוט הרישום: command/args/cwd/env_keys. + +## 4. טיפול בשגיאות + +| תרחיש | התנהגות | +|---|---| +| Infisical לא זמין | `errors: ["infisical_unreachable"]` ב-GET. ערך infisical = null. UI מציג `?` במקום הערך + tooltip | +| Coolify redeploy נכשל | toast עם פרטי השגיאה. ערך נשמר ב-Infisical, מסומן pending | +| volume mount חסר ב-Coolify | endpoint registrations מחזיר `{registrations: [], error: "host_path_unavailable"}`. UI מציג הודעה | +| ניסיון עריכה של secret | 400 עם הודעה ברורה | +| ערך לא חוקי לפי type | 400 עם הודעת ולידציה ספציפית | +| FastMCP introspection נכשלת | 500. לוג שגיאה. UI מציג fallback | + +## 5. בטיחות + +- **לא להציג ערכי secret** — ה-API מחזיר תמיד `****` עבור secrets. +- **Drift detection לא חושף** — השוואה על hash, לא על ערך גולמי. +- **PATCH על secret חסום ב-server** — לא רק ב-UI. +- **No raw `os.environ` dump** — ה-endpoint מחזיר רק keys ב-catalog. +- **Audit log** — כל PATCH מתועד ל-`logger.info` (key + ערך אם לא-סודי). + +## 6. שלבי מימוש (overview ל-plan) + +1. Catalog + endpoint `GET /api/settings/mcp/env` (ללא עריכה). +2. UI טאב Environment — read-only עם drift badges. +3. PATCH endpoint + UI editor. +4. Redeploy endpoint + UI button. +5. Tools introspection + UI. +6. Volume mounts הוראה (manual Coolify config) + Registrations endpoint + UI. +7. בדיקות ידניות end-to-end. + +## 7. שאלות פתוחות (להבהרה לפני plan) + +- **סביבת Infisical** — `dev`? `nautilus`? להחליט סופית. ברירת מחדל ב-spec: `dev`. ייתכן ויהיה ניתן לקבוע ב-env var (`INFISICAL_ENV`). +- **Path ב-Infisical** — `/legal-ai`? `/legal-ai/mcp`? להחליט לפי `_GUIDELINES/SAVE_SECRET_RULES`. +- **Auth** — אין כרגע על `/settings`. להוסיף לפחות "are you sure" dialog לפני PATCH של ערך משמעותי? + +## 8. בדיקות + +**ידני (אין test suite ל-frontend):** +- ✓ פתיחת `/settings` — Paperclip tab עובד כקודם. +- ✓ Environment tab — מציג env vars מקבץ catalog בלבד. +- ✓ Drift detection — שינוי ידני של env בקונטיינר → drift badge מופיע. +- ✓ עריכת `MULTIMODAL_TEXT_WEIGHT` ל-`0.7` → נשמר ב-Infisical. +- ✓ Redeploy → ערך חדש נכנס לתוקף בקונטיינר. +- ✓ ניסיון עריכת `VOYAGE_API_KEY` → חסום + הודעה. +- ✓ Tools tab — מציג את כל ה-tools של legal_mcp. +- ✓ Registrations tab — מציג את `~/.claude.json` ו-Paperclip instances. + +**Backend tests** ב-`web/tests/` (אם קיימים — אחרת לדלג): +- catalog rejects unknown key +- PATCH על secret נחסם +- ולידציה של min/max