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:
336
docs/superpowers/specs/2026-05-04-mcp-settings-page-design.md
Normal file
336
docs/superpowers/specs/2026-05-04-mcp-settings-page-design.md
Normal 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
|
||||
Reference in New Issue
Block a user