docs(specs): add design for MCP settings page

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 05:44:31 +00:00
parent 2f05cdea2e
commit 70052b0133

View File

@@ -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.<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