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>
337 lines
15 KiB
Markdown
337 lines
15 KiB
Markdown
# דף הגדרות 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
|