הקונטיינר והשרת ה-HTTP הם שתי דלתות לאותו קורפוס, ולכן חייבים לדרג אותו. הם לא עשו זאת: הקונטיינר רץ `MULTIMODAL_ENABLED=true`, בעוד שרת ה-MCP נפל לברירת-המחדל `false` — `~/.env` אינו מגדיר את הדגל כלל. סוכן ו-UI יכלו לענות על אותה שאלה מתוך קבוצות-תוצאות שונות, בלי שאיש ידע (G2 — מסלול מקביל שמתפצל בשקט). מה שונה - `MULTIMODAL_ENABLED: "true"` בקונפיג ה-pm2 של legal-mcp-http. - תיעוד השירות ב-operations-runbook (פורט, שער-Bearer, ו**האזהרה שהחשובה מכולן**: קוד ב-`mcp-server/` לא נכנס לתוקף עד restart — זהו תהליך ארוך-חיים, בניגוד ל-stdio שנטען בכל סשן. שלושה מיזוגים היום רצו על קוד ישן עד שהבחנתי). מה **לא** שונה, ולמה `STORAGE_BACKEND` נשאר לא-מוגדר. הקונטיינר רץ `s3`, השרת נופל ל-`filesystem`, וזה דריפט אמיתי — סוכן שכותב דרך MCP כותב לדיסק בעוד ה-web קורא מ-S3. אבל הפיכת הדגל דורשת `MINIO_ENDPOINT`/`ACCESS_KEY`/ `SECRET_KEY`, ואלה **אינם ב-Infisical** — רק ב-env של Coolify. בלעדיהם `MINIO_ENDPOINT` נפתר לברירת-המחדל `http://minio:9000` (שם-רשת פנימי של דוקר, לא נגיש מהמארח) וכל פעולת-בלוב נשברת. יצירת סוד חדש מחייבת אישור חיים לשם/תיקייה/תגית, ולכן הדריפט מתועד בבירור ב-runbook עם בדיוק מה שצריך כדי לסגור אותו — במקום להישאר ידיעה שבעל-פה. MinIO **כן** נגיש מהמארח: `https://s3.nautilus.marcusgroup.org` מחזיר 200 ב-`/minio/health/live` (אומת), כך שהחסם היחיד הוא הסודות. invariants: G2 (שתי דלתות, דירוג אחד) · §6 (הדריפט הנותר מתועד, לא נבלע)
111 lines
5.0 KiB
JavaScript
111 lines
5.0 KiB
JavaScript
/**
|
|
* 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",
|
|
// Retrieval flags must match the container's, or the two doors into the same
|
|
// corpus rank differently (G2 — a parallel path that drifts). The container
|
|
// runs MULTIMODAL_ENABLED=true; without this the MCP door skipped the
|
|
// page-image merge entirely, so an agent and the web UI could answer the same
|
|
// question from different result sets. ~/.env sets neither, so both fell to
|
|
// the code defaults (false) — the drift was silent.
|
|
//
|
|
// NOT set here: STORAGE_BACKEND. The container runs `s3`, this service falls
|
|
// back to `filesystem`, and closing that gap needs MinIO credentials that do
|
|
// not exist in Infisical yet (only in Coolify's env). Flipping the flag
|
|
// without them would resolve MINIO_ENDPOINT to the in-cluster default
|
|
// `http://minio:9000` — unreachable from the host — and break every blob
|
|
// read. Tracked separately; see the note in docs/operations-runbook.md.
|
|
MULTIMODAL_ENABLED: "true",
|
|
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",
|
|
},
|
|
],
|
|
};
|