/** * 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", }, ], };