Skip to content

brain-mcp hosting

brain-mcp is the HTTP transport of the same MCP server described in MCP server — the untouched dispatch() from mcp_server.py, plus the write tools registered by mcp_write_tools.py, now reachable over the network instead of stdio. It's the write surface the hourly Claude-driven brain routine calls (Option B; see 2026-08-30-brain-routine-option-b-design.md). It runs as its own container in prod, behind the Cloudflare tunnel, and is never reachable directly — TLS terminates at Cloudflare.

Endpoint contract

  • GET /healthz — unauthenticated, always 200 ok (used by the container healthcheck and by cloudflared/uptime checks).
  • POST /mcp (also accepted at /) — the JSON-RPC 2.0 endpoint. Every request must carry Authorization: Bearer <JARVIS_BRAIN_MCP_TOKEN>; a missing, malformed, or wrong token gets 401 with no body. A valid tools/list call returns all 20 tools (6 read + 14 write).
  • Everything else (GET on any other path, wrong method, oversized or malformed body) is rejected before dispatch() ever runs.

The process refuses to start if JARVIS_BRAIN_MCP_TOKEN is unset or shorter than 32 characters — there is no way to accidentally expose the write surface with a weak or empty token.

Local run

Point it at the local dev stack (the docker compose up stack published on :8091; see Local run recipe):

JARVIS_BRAIN_MCP_TOKEN=$(openssl rand -hex 24) \
  PYTHONPATH="agent-runtime/src:modules/conversation/agent" \
  python3 -m conversation_agent.agent serve-mcp-http

This binds 0.0.0.0:8090 by default (JARVIS_BRAIN_MCP_HOST / JARVIS_BRAIN_MCP_PORT override it). Point POCKETBASE_URL at the local stack's PocketBase and set DEV_USER_EMAIL / DEV_USER_PASSWORD the same way you would for serve-chat or serve-mcp.

Prod deployment

brain-mcp ships as a service in compose.prod.yaml: same jarvis-embed:prod image as chat-worker (for the fastembed model cache), same vault / chat_data / model_cache volumes, no published host port. It's added to REQUIRED_CONTAINERS in ops/jarvis-deploy.sh, so a deploy fails loudly if the container isn't healthy afterward — including if the owner forgot to set the token (the process exits immediately, the healthcheck never turns green, and the deploy gate catches it).

Four steps are owner-only — none of them run from CI, and none of them run automatically as part of a deploy. Run each of these yourself, in order, before merging the PR that ships this service:

1. Add the token to the server's .env (idempotent — skips if a token is already set, rather than appending a second, shadowed value):

ssh mailserver 'umask 077; grep -q "^JARVIS_BRAIN_MCP_TOKEN=" /opt/jarvis/.env 2>/dev/null && echo "JARVIS_BRAIN_MCP_TOKEN already set" || echo "JARVIS_BRAIN_MCP_TOKEN=$(openssl rand -hex 24)" >> /opt/jarvis/.env'

Then print the current value back, so you can copy it into the cloud credential store the routine will read from (PR ③):

ssh mailserver "grep JARVIS_BRAIN_MCP_TOKEN /opt/jarvis/.env"

2. Cloudflare dashboard — add the public hostname. The tunnel's hostname→service mapping is managed remotely in the Cloudflare dashboard, not in a repo config file. Add a public hostname rule, placed above any catch-all rule:

  • Public hostname: brain-mcp.richarddepierre.com
  • Service: http://brain-mcp:8090
  • Same tunnel that already serves jarvis.richarddepierre.com

3. Refresh the forced-command deploy script. The active copy the SSH forced command runs is /usr/local/bin/jarvis-deploy.sh, deliberately outside /opt/jarvis so a deploy payload's rsync can never touch it (see Backend auto-deploy). It has to be refreshed by hand — this must happen before merging this PR, because the very next deploy will otherwise run the old script, which doesn't know about jarvis-brain-mcp and won't gate on it:

ssh mailserver "cp /opt/jarvis/ops/jarvis-deploy.sh /usr/local/bin/jarvis-deploy.sh && chmod +x /usr/local/bin/jarvis-deploy.sh"

4. After the next deploy ships, verify read-only:

curl -s -o /dev/null -w '%{http_code}\n' https://brain-mcp.richarddepierre.com/healthz
# expect: 200

curl -s -o /dev/null -w '%{http_code}\n' -X POST https://brain-mcp.richarddepierre.com/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# expect: 401 (no token)

curl -s https://brain-mcp.richarddepierre.com/mcp \
  -H "Authorization: Bearer $JARVIS_BRAIN_MCP_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | python3 -m json.tool
# expect: a result with 20 tools