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