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>
15 KiB
דף הגדרות 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
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
{
"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}
- ולידציה: הקיי קיים ב-catalog ו-
is_editable=true. אם לא → 400. - ולידציה לפי type: int/float ב-טווח, bool מוסב מ-string, enum בערכים מותרים.
- כתיבה ל-Infisical:
client.update_secret( project_id=INFISICAL_PROJECT_ID, environment_slug=INFISICAL_ENV, # "dev" כברירת מחדל secret_path="/legal-ai", secret_name=key, secret_value=str(value), ) - Audit log:
logger.info("mcp_env_update", extra={"key": key, "value": value if not is_secret else "[masked]"}). - Response:
{"ok": true, "requires_redeploy": true, "message": "נשמר ב-Infisical. נדרש redeploy."}.
Redeploy flow ב-POST /api/settings/mcp/env/redeploy
- קריאה ל-Coolify API:
POST /api/v1/deploy?uuid=gyjo0mtw2c42ej3xxvbz8zio&force=false. - אסימון:
COOLIFY_API_TOKEN(מ-Infisical). - Polling: קריאה ל-
/api/v1/deployments/{deployment_uuid}כל 5 שניות, עדstatus="finished"אוstatus="failed"(max 10 דקות). - UI מציג סטטוס מתעדכן (פשוט: spinner + הודעת סטטוס; לא נדרש streaming).
Tools introspection ב-GET /api/settings/mcp/tools
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
קורא:
/host/.claude.json— תחתmcpServersאוprojects.<path>.mcpServers./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):
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 חדשים:
// קריאות חדשות
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) + כפתור "שמור".
לחיצה על "שמור":
- PATCH → toast הצלחה: "נשמר ב-Infisical. לחץ Redeploy כדי להחיל בקונטיינר."
- השורה מסומנת כ-"pending redeploy" עד ה-redeploy הבא.
- כפתור "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.environdump — ה-endpoint מחזיר רק keys ב-catalog. - Audit log — כל PATCH מתועד ל-
logger.info(key + ערך אם לא-סודי).
6. שלבי מימוש (overview ל-plan)
- Catalog + endpoint
GET /api/settings/mcp/env(ללא עריכה). - UI טאב Environment — read-only עם drift badges.
- PATCH endpoint + UI editor.
- Redeploy endpoint + UI button.
- Tools introspection + UI.
- Volume mounts הוראה (manual Coolify config) + Registrations endpoint + UI.
- בדיקות ידניות 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