# דף הגדרות 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.<path>.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 מחזיר תמיד `****<last_4>` עבור 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
