Files
legal-ai/docs/superpowers/specs/2026-05-04-mcp-settings-page-design.md
Chaim 70052b0133 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>
2026-05-04 05:44:31 +00:00

15 KiB
Raw Permalink Blame History

דף הגדרות 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}

  1. ולידציה: הקיי קיים ב-catalog ו-is_editable=true. אם לא → 400.
  2. ולידציה לפי type: int/float ב-טווח, bool מוסב מ-string, enum בערכים מותרים.
  3. כתיבה ל-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),
    )
    
  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

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):

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) + כפתור "שמור".

לחיצה על "שמור":

  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)

  • סביבת Infisicaldev? 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