fleetanalytics_mcp/deploy.sh
kiania f83f67e73f feat(access): expose tickets + fuel schemas to analytics_ro (read-only)
The analytics_ro role only had USAGE/SELECT on reporting + tracksolid, so
the tickets schema (INC/CRQ, 8 tables + 1 view + 7 fns) and fuel schema
were invisible to the MCP server — queries failed with permission denied.

- analytics_ro_role.sql: GRANT USAGE/SELECT/EXECUTE on tickets + fuel.
  Default privileges for these are keyed to postgres (their owner), not
  tracksolid_owner, so future objects auto-grant correctly.
- analytics_mcp.py: READABLE_SCHEMAS now includes tickets + fuel and is
  overridable via MCP_READABLE_SCHEMAS, so the introspection helpers
  (list_tables/describe_table/sample_table) work for them too.
- deploy.sh: reuse existing analyst tokens from the running container when
  MCP_AUTH_TOKENS is unset, so a code-only redeploy needs no secret.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:37:25 +03:00

86 lines
5.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# deploy.sh — manual host deploy for the read-only Fleet Analytics MCP server.
# ─────────────────────────────────────────────────────────────────────────────
# Use this if you are NOT letting Coolify build the Dockerfile (see README §Deploy
# for the Coolify-managed path, which is the recommended default). This script
# builds the image from this repo ON THE HOST and runs it as a standalone
# Traefik-labelled bridge — the same proven pattern as the dashboard_api bridges:
# it joins the network that can reach timescale_db, derives a READ-ONLY DATABASE_URL
# for the analytics_ro role, and exposes the MCP over HTTPS with Bearer auth.
#
# Prereqs ON THE HOST:
# * the analytics_ro role exists -> scripts/bootstrap_analytics_ro.sh (writes ~/.analytics_ro.pw)
# * this repo is checked out (e.g. ~/fleetanalytics_mcp) — run this script from inside it
#
# Run:
# cd ~/fleetanalytics_mcp && git pull
# MCP_AUTH_TOKENS="alice:$(openssl rand -hex 16)" bash deploy.sh
#
# A token/env change needs a container RECREATE — this script does that. Record
# each analyst's token securely; it is only shown once (when you generate it).
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
NAME=analytics_mcp
PORT=8892
HOST_DOMAIN="${HOST_DOMAIN:-fleetmcp.fivetitude.com}" # prod: fleetmcp.rahamafresh.com
IMAGE="fleetanalytics-mcp:latest"
ENV_FILE="$(pwd)/.deploy.env"
# Per-analyst Bearer tokens. For a CODE-ONLY redeploy you can omit MCP_AUTH_TOKENS:
# we reuse the tokens from the currently running container so existing analysts keep
# working and no secret has to be re-typed or printed. Only set MCP_AUTH_TOKENS when
# you are adding/rotating/revoking a token.
if [ -z "${MCP_AUTH_TOKENS:-}" ]; then
MCP_AUTH_TOKENS="$(docker inspect "$NAME" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | sed -n 's/^MCP_AUTH_TOKENS=//p' | head -1)"
[ -n "$MCP_AUTH_TOKENS" ] && echo "Reusing existing analyst tokens from running $NAME container."
fi
: "${MCP_AUTH_TOKENS:?set MCP_AUTH_TOKENS=name:token[,name:token...] before running (per-analyst Bearer tokens)}"
# Resolve the network + DB DSN from the running webhook_receiver (it sits on the
# same internal network as timescale_db and holds a DATABASE_URL we can reuse the
# host:port/dbname from). This avoids hardcoding the internal DB hostname.
WH=$(docker ps --filter name=webhook_receiver --format "{{.Names}}" | head -1)
[ -n "$WH" ] || { echo "ERROR: webhook_receiver container not found (need the tracksolid stack running)"; exit 1; }
APPNET=$(docker inspect "$WH" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}')
SRC_DB_URL=$(docker inspect "$WH" --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^DATABASE_URL=//p' | head -1)
[ -n "$SRC_DB_URL" ] || { echo "ERROR: DATABASE_URL not found in $WH env"; exit 1; }
echo "Reusing network $APPNET (from $WH)"
# Build a READ-ONLY DATABASE_URL: app DB host:port/dbname + analytics_ro creds.
RO_PW=$(cat "${ANALYTICS_RO_PW_FILE:-$HOME/.analytics_ro.pw}" 2>/dev/null || true)
[ -n "$RO_PW" ] || { echo "ERROR: ~/.analytics_ro.pw missing — run scripts/bootstrap_analytics_ro.sh first"; exit 1; }
HOSTPART="${SRC_DB_URL#*@}" # host:port/dbname[?params]
RO_DB_URL="postgresql://analytics_ro:${RO_PW}@${HOSTPART}"
# Build the image from this repo.
echo "Building $IMAGE ..."
docker build -t "$IMAGE" .
# Minimal env (read-only DSN + auth only — no Tracksolid ingestion secrets).
{ echo "DATABASE_URL=${RO_DB_URL}"; echo "MCP_AUTH_TOKENS=${MCP_AUTH_TOKENS}"; } > "$ENV_FILE"
chmod 600 "$ENV_FILE"
docker rm -f "$NAME" 2>/dev/null || true
docker run -d --name "$NAME" --restart unless-stopped \
--network "$APPNET" \
--env-file "$ENV_FILE" \
--label 'traefik.enable=true' \
--label 'traefik.docker.network=coolify' \
--label 'traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https' \
--label "traefik.http.routers.http-0-fleetmcp.entryPoints=http" \
--label "traefik.http.routers.http-0-fleetmcp.middlewares=redirect-to-https" \
--label "traefik.http.routers.http-0-fleetmcp.rule=Host(\`${HOST_DOMAIN}\`)" \
--label "traefik.http.routers.https-0-fleetmcp.entryPoints=https" \
--label "traefik.http.routers.https-0-fleetmcp.rule=Host(\`${HOST_DOMAIN}\`)" \
--label "traefik.http.routers.https-0-fleetmcp.tls=true" \
--label "traefik.http.routers.https-0-fleetmcp.tls.certresolver=letsencrypt" \
--label "traefik.http.services.fleetmcp.loadbalancer.server.port=${PORT}" \
"$IMAGE"
docker network connect coolify "$NAME" 2>/dev/null || true
rm -f "$ENV_FILE"
sleep 5
echo "== container =="; docker ps --filter name="$NAME" --format "{{.Names}} | {{.Status}}"
echo "== DB role (expect analytics_ro) =="; docker exec "$NAME" sh -lc 'printenv DATABASE_URL | sed -E "s#://([^:]+):[^@]+@#://\1:<pw>@#"'
echo "== health =="; docker exec "$NAME" python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:${PORT}/healthz').read().decode())" 2>&1 | head