feat(ops): שירות pm2 legal-mcp-http — שרת ה-MCP חשוף ב-HTTP מאחורי שער Bearer (#231.3)
סוכנים המונעים דרך 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 = עבר). | ידני אחרי שינוי שכבת חיפוש |
|
| `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_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-<ts>.{json,md}` + delta מול `data/eval/baseline.json` (מתעד retrieval_config). `--self-test` בודק את המטריקות offline; `--update-baseline` מאמץ snapshot. **שער-CI במשמעת:** הרץ לפני/אחרי כל שינוי בשכבת-האחזור באותו קונפיג. דורש POSTGRES+VOYAGE_API_KEY. | לפני/אחרי שינוי RRF/k/embedder/rerank |
|
| `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-<ts>.{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) |
|
| `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) |
|
| `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) |
|
| `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) |
|
||||||
|
|||||||
96
scripts/legal-mcp-http.config.cjs
Normal file
96
scripts/legal-mcp-http.config.cjs
Normal file
@@ -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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user