# דף הגדרות 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