From 3f48dc0e11ce8475481b563e121521c8bb8524ef Mon Sep 17 00:00:00 2001 From: Chaim Date: Wed, 5 Aug 2026 08:19:45 +0000 Subject: [PATCH] =?UTF-8?q?feat(ops):=20=D7=A9=D7=99=D7=A8=D7=95=D7=AA=20p?= =?UTF-8?q?m2=20legal-mcp-http=20=E2=80=94=20=D7=A9=D7=A8=D7=AA=20=D7=94-M?= =?UTF-8?q?CP=20=D7=97=D7=A9=D7=95=D7=A3=20=D7=91-HTTP=20=D7=9E=D7=90?= =?UTF-8?q?=D7=97=D7=95=D7=A8=D7=99=20=D7=A9=D7=A2=D7=A8=20Bearer=20(#231.?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit סוכנים המונעים דרך Agent Client Protocol מקבלים את שרתי-ה-MCP שלהם מהלקוח בפתיחת הסשן, והערוץ הזה נושא שרתי HTTP בלבד. ל-stdio אין מסלול לשם, ומכאן שהסוכנים נותרו בלי 108 הכלים. זהו הקצה ה-HTTP שאפשר להפנות אותם אליו. **אינו מחליף את stdio.** כל סשן אינטראקטיבי ממשיך להגיע לאותו שרת דרך הרשומה legal-ai ב-~/.claude.json. אותו קוד, אותו מרשם-כלים, שתי תחבורות (G2) — דלת שנייה, לא שרת שני. אבטחה — שתי הגנות, שתיהן נדרשות: 1. bind ל-127.0.0.1. צר יותר מ-10.0.1.1 של legal-chat-service בכוונה: שום קונטיינר לא צריך לקרוא ל-MCP. 2. Bearer מ-MCP_HTTP_SHARED_SECRET. השרת מסרב לעלות בלי טוקן (services/http_auth.py), ולכן תקלת-הגדרה לא יכולה לייצר בשקט מאזין לא-מאומת — pm2 יציג crash במקום להגיש תעבורה פתוחה. הסוד: מקור-אמת ב-Infisical (All Infrastructure / main / /apps/legal-ai, תג credentials). ~/.legal-mcp-http.env הוא עותק-ריצה chmod 600, אותו סידור כמו legal-chat-service. max_restarts נמוך במתכוון: טוקן חסר מפיל את התהליך מיד, ועדיף ש-pm2 יפסיק לנסות ויותיר רשומת errored גלויה מאשר ילולאה על טעות-הגדרה. אומת על השירות החי: POST בלי Authorization → 401 POST עם טוקן שגוי → 401 POST עם הטוקן הנכון → 200 אחרי pm2 restart → 200 pm2 save → ✓ invariants: G2 — מקיים (תחבורה שנייה לאותו שרת, לא מימוש מקביל). G12 — לא נגוע. INV-AG3 — לא נגוע. Co-Authored-By: Claude Opus 5 --- scripts/SCRIPTS.md | 1 + scripts/legal-mcp-http.config.cjs | 96 +++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 scripts/legal-mcp-http.config.cjs diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index 2e8f6eb..6782dbb 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -26,6 +26,7 @@ | `test_retrieval_by_name.py` | python | בדיקת אחזור-לפי-שם (#52/RC-A) — מאמת ש`search_precedent_library`/`search_internal_decisions` מדרגים את ההחלטה עצמה (אגסי) מעל מי שמצטט אותה, + רגרסיות לשאילתות מהותיות. הרצה: `DOTENV_PATH=/home/chaim/.env DATA_DIR=.../data mcp-server/.venv/bin/python scripts/test_retrieval_by_name.py` (exit 0 = עבר). | ידני אחרי שינוי שכבת חיפוש | | `eval_gold_bootstrap.py` | python | **FU-5 (GAP-11) — bootstrap ל-gold-set** של הערכת-אחזור ל-`data/eval/gold-set.jsonl`. שני מקורות: `--source citations` (cited==relevant מ-`search_relevance_feedback`; ריק עד שייצברו ציטוטים) ו-`--source known_item` (query=שם-תיק → relevant=עצמו; אות אמיתי היום). Idempotent — שומר שורות `source=chair`, מחדש `bootstrap_*`. דורש POSTGRES. | לפני eval; חוזר כשנצבר ground-truth | | `eval_retrieval.py` | python | **FU-5 (GAP-11, INV-RET4/G8) — harness הערכת-אחזור** — מריץ את מסלול-האחזור בייצור (`search_library`/`search_internal`) על ה-gold-set, מחשב precision@k/recall@k/MRR/nDCG@k (k=5,10), מצרף overall+per-corpus+per-PA ל-`data/eval/eval-report-.{json,md}` + delta מול `data/eval/baseline.json` (מתעד retrieval_config). `--self-test` בודק את המטריקות offline; `--update-baseline` מאמץ snapshot. **שער-CI במשמעת:** הרץ לפני/אחרי כל שינוי בשכבת-האחזור באותו קונפיג. דורש POSTGRES+VOYAGE_API_KEY. | לפני/אחרי שינוי RRF/k/embedder/rerank | +| `legal-mcp-http.config.cjs` | pm2/js | **שרת ה-MCP חשוף ב-streamable-http** (#231) — `python -m legal_mcp.server` עם `MCP_TRANSPORT=streamable-http`, bound **`127.0.0.1:8790`**, Bearer `MCP_HTTP_SHARED_SECRET` מ-`~/.legal-mcp-http.env` (מקור-אמת: Infisical → All Infrastructure / `main` / `/apps/legal-ai`, תג `credentials`). **למה:** סוכנים המונעים דרך Agent Client Protocol מקבלים את שרתי-ה-MCP שלהם מהלקוח בפתיחת הסשן, והערוץ הזה נושא שרתי HTTP בלבד — ל-stdio אין מסלול לשם, ומכאן שהסוכנים נותרו בלי 108 הכלים. **אינו מחליף את stdio:** כל סשן אינטראקטיבי ממשיך דרך `~/.claude.json`; אותו קוד, אותו מרשם-כלים, שתי תחבורות (G2). **אבטחה:** loopback בלבד (צר יותר מ-`10.0.1.1` של legal-chat-service — שום קונטיינר לא צריך MCP), והשרת **מסרב לעלות בלי טוקן** (`services/http_auth.py`), כך שתקלת-הגדרה לא יכולה לייצר מאזין לא-מאומת. מראָה לדפוס `legal-chat-service.config.cjs`. התקנה: `pm2 start scripts/legal-mcp-http.config.cjs && pm2 save`. בדיקה: POST ל-`/mcp` → 401 בלי טוקן, 200 עם. | pm2 (host-side) | | `legal-court-fetch-service.config.cjs` | pm2/js | **שירות-מארח Tier-1 לאחזור פסקי-דין מנט המשפט (X13)** — 2 apps: (א) `legal-court-fetch-xvfb` (Xvfb :99, צג-וירטואלי ל-Camoufox); (ב) `legal-court-fetch-service` (`python -m legal_mcp.court_fetch_service.server`, bound `10.0.1.1:8771`, Bearer `COURT_FETCH_SHARED_SECRET` מ-`~/.legal-court-fetch-service.env`, `DISPLAY=:99`). מריץ Camoufox דרך חבילת-הפייתון (in-process) כי הקונטיינר לא יכול דפדפן. תלות: `pip install -e "mcp-server[court-fetch]" && python -m camoufox fetch`. אחזור = ניווט→צופה→`GetImages`(X-Requested-With)→PDF, ללא CAPTCHA; כשל→`ok:false`→orchestrator מסלים ל-fallback אנושי. **אומת על עת"מ 46111-12-22 (34 עמ').** מראָה לדפוס `legal-chat-service.config.cjs`. ספ: `docs/spec/X13-court-fetch.md`. התקנה: `pm2 start scripts/legal-court-fetch-service.config.cjs && pm2 save`. בריאות: `curl http://10.0.1.1:8771/health`. | pm2 (host-side) | | `drain_court_fetch.py` | python | **ריקון תור-אחזור הפסיקה (X13)** — קורא ל-`court_fetch_orchestrator.drain_pending(limit)` שמוריד+קולט כל job ממתין שהיומונים מילאו, וקושר חזרה ליומון. מקומי בלבד (ingest = claude CLI). no-op מהיר כשהתור ריק. הרצה ידנית: `mcp-server/.venv/bin/python scripts/drain_court_fetch.py [limit]`. | דרך `legal-court-fetch-drain.config.cjs` (pm2 cron) | | `legal-court-fetch-drain.config.cjs` | pm2/js | **תזמון שעתי של `drain_court_fetch.py`** (cron `17 * * * *`, `COURT_FETCH_DRAIN_CRON` לעקיפה) — הופך את לולאת יומון→אחזור→קליטה ל-fully-autonomous. `autorestart:false` (one-shot per tick). דורש `legal-court-fetch-service` רץ. התקנה: `pm2 start scripts/legal-court-fetch-drain.config.cjs && pm2 save`. | pm2 cron (host-side) | diff --git a/scripts/legal-mcp-http.config.cjs b/scripts/legal-mcp-http.config.cjs new file mode 100644 index 0000000..ed34ff5 --- /dev/null +++ b/scripts/legal-mcp-http.config.cjs @@ -0,0 +1,96 @@ +/** + * pm2 ecosystem entry for legal-mcp-http — the legal-ai MCP server exposed over + * streamable-http (TaskMaster #231). + * + * Why it exists + * Agents driven over the Agent Client Protocol get their MCP servers from the + * *client* at session start, and that channel carries HTTP servers only. A + * stdio server has no path into such a session, which is how platform-driven + * agents ended up with none of the 108 tools. This service is the HTTP end + * they can actually be pointed at. + * + * It does NOT replace the stdio path. Every interactive Claude Code session + * still reaches the same server through the `legal-ai` entry in + * ~/.claude.json, spawned per session. Same code, same tool registry, two + * transports (G2) — this is a second *door*, not a second server. + * + * Security + * The registry includes case_delete, precedent_library_delete, document_upload + * and every block-writing tool, so an open port here is a delete-any-case + * endpoint. Two defences, both required: + * 1. Bind 127.0.0.1 — the platform runs on this host, so loopback suffices. + * Deliberately narrower than legal-chat-service's 10.0.1.1: nothing in a + * container needs to call MCP. + * 2. Bearer token — MCP_HTTP_SHARED_SECRET, loaded below. The server + * REFUSES TO START without it (services/http_auth.py), so a + * misconfiguration cannot silently produce an unauthenticated listener. + * + * Secret + * Source of truth: Infisical, project "All Infrastructure", env `main`, + * /apps/legal-ai/MCP_HTTP_SHARED_SECRET (tag: credentials). The file read + * below is a chmod-600 runtime copy, same arrangement as + * legal-chat-service.config.cjs. Rotate in Infisical first, then refresh the + * file and `pm2 restart legal-mcp-http`. + * + * Install (once): + * pm2 start /home/chaim/legal-ai/scripts/legal-mcp-http.config.cjs + * pm2 save + * + * Smoke test — expect 401 without the token, 200 with it: + * curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:8790/mcp \ + * -H 'Content-Type: application/json' \ + * -H 'Accept: application/json, text/event-stream' \ + * -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' + * + * Update: pm2 restart legal-mcp-http --update-env + * Stop: pm2 stop legal-mcp-http + */ +const fs = require("fs"); + +const ENV_FILE = "/home/chaim/.legal-mcp-http.env"; +const env = { + HOME: "/home/chaim", + PATH: "/home/chaim/.local/bin:/usr/local/bin:/usr/bin:/bin", + PYTHONUNBUFFERED: "1", + // Same DB/data wiring the stdio server gets from ~/.claude.json, so both + // transports read exactly the same corpus. + DOTENV_PATH: "/home/chaim/.env", + DATA_DIR: "/home/chaim/legal-ai/data", + MCP_TRANSPORT: "streamable-http", + MCP_HTTP_HOST: "127.0.0.1", + MCP_HTTP_PORT: "8790", +}; + +try { + const text = fs.readFileSync(ENV_FILE, "utf8"); + for (const line of text.split("\n")) { + if (!line || line.trim().startsWith("#")) continue; + const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/); + if (m) env[m[1]] = m[2]; + } +} catch (e) { + // Warn, but do not fabricate a token. The server's own gate turns a missing + // secret into a refusal to boot, which is the outcome we want — pm2 will + // surface it as a crash loop rather than serve unauthenticated traffic. + console.error(`legal-mcp-http: failed to load ${ENV_FILE}: ${e.message}`); + console.error("Service will refuse to start without MCP_HTTP_SHARED_SECRET."); +} + +module.exports = { + apps: [ + { + name: "legal-mcp-http", + cwd: "/home/chaim/legal-ai/mcp-server", + script: "/home/chaim/legal-ai/mcp-server/.venv/bin/python", + args: "-m legal_mcp.server", + env, + restart_delay: 5000, + // Low ceiling on purpose: if the token is missing the process exits + // immediately, and we want pm2 to stop retrying and leave an obvious + // errored entry rather than loop forever on a config mistake. + max_restarts: 10, + autorestart: true, + max_memory_restart: "800M", + }, + ], +}; -- 2.49.1