Merge pull request 'infra(deploy): serve MCP on multiple domains + SKIP_BUILD' (#4) from infra/deploy-multidomain into main
This commit is contained in:
commit
2f421d7439
2 changed files with 37 additions and 6 deletions
|
|
@ -26,6 +26,7 @@ Env:
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextvars
|
||||||
import hmac
|
import hmac
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
@ -65,6 +66,12 @@ def _get_logger(name: str) -> logging.Logger:
|
||||||
|
|
||||||
log = _get_logger("server")
|
log = _get_logger("server")
|
||||||
|
|
||||||
|
# Per-request caller name, set by BearerAuth from the matched token so the tools can
|
||||||
|
# attribute each query to an analyst in the logs. A ContextVar (not a tool arg) because
|
||||||
|
# FastMCP tools never receive the HTTP request; anyio propagates the context into the
|
||||||
|
# worker thread that runs each sync tool. Defaults to "?" if auth ever didn't run.
|
||||||
|
_caller_var: contextvars.ContextVar[str] = contextvars.ContextVar("caller", default="?")
|
||||||
|
|
||||||
DATABASE_URL = os.environ["DATABASE_URL"] # analytics_ro DSN (set by deploy)
|
DATABASE_URL = os.environ["DATABASE_URL"] # analytics_ro DSN (set by deploy)
|
||||||
MAX_ROWS_CEIL = int(os.getenv("MCP_MAX_ROWS", "10000"))
|
MAX_ROWS_CEIL = int(os.getenv("MCP_MAX_ROWS", "10000"))
|
||||||
# Schemas the introspection helpers (list_tables/describe_table/sample_table) expose.
|
# Schemas the introspection helpers (list_tables/describe_table/sample_table) expose.
|
||||||
|
|
@ -284,7 +291,10 @@ def query(sql: str, max_rows: int = 1000) -> dict:
|
||||||
truncated = len(rows) > cap
|
truncated = len(rows) > cap
|
||||||
rows = rows[:cap]
|
rows = rows[:cap]
|
||||||
dur_ms = int((time.monotonic() - t0) * 1000)
|
dur_ms = int((time.monotonic() - t0) * 1000)
|
||||||
log.info("query rows=%d trunc=%s %dms :: %s", len(rows), truncated, dur_ms, sql[:200])
|
log.info(
|
||||||
|
"query caller=%s rows=%d trunc=%s %dms :: %s",
|
||||||
|
_caller_var.get(), len(rows), truncated, dur_ms, sql[:200],
|
||||||
|
)
|
||||||
return {"row_count": len(rows), "truncated": truncated, "rows": rows}
|
return {"row_count": len(rows), "truncated": truncated, "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -393,6 +403,7 @@ class BearerAuth(BaseHTTPMiddleware):
|
||||||
if caller is None:
|
if caller is None:
|
||||||
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
||||||
request.state.caller = caller
|
request.state.caller = caller
|
||||||
|
_caller_var.set(caller) # so the tools can attribute the query in the logs
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
30
deploy.sh
30
deploy.sh
|
|
@ -24,6 +24,20 @@ set -euo pipefail
|
||||||
NAME=analytics_mcp
|
NAME=analytics_mcp
|
||||||
PORT=8892
|
PORT=8892
|
||||||
HOST_DOMAIN="${HOST_DOMAIN:-fleetmcp.fivetitude.com}" # prod: fleetmcp.rahamafresh.com
|
HOST_DOMAIN="${HOST_DOMAIN:-fleetmcp.fivetitude.com}" # prod: fleetmcp.rahamafresh.com
|
||||||
|
# Comma-separated list of every domain this service answers on (defaults to
|
||||||
|
# HOST_DOMAIN). All are folded into ONE Traefik router rule so a single cert
|
||||||
|
# covers them and connectors on either domain keep working.
|
||||||
|
HOST_DOMAINS="${HOST_DOMAINS:-$HOST_DOMAIN}"
|
||||||
|
BT='`'
|
||||||
|
RULE=""
|
||||||
|
IFS=',' read -ra _DOMS <<< "$HOST_DOMAINS"
|
||||||
|
for _d in "${_DOMS[@]}"; do
|
||||||
|
_d="${_d// /}"
|
||||||
|
if [ -n "$_d" ]; then
|
||||||
|
seg="Host(${BT}${_d}${BT})"
|
||||||
|
if [ -z "$RULE" ]; then RULE="$seg"; else RULE="$RULE || $seg"; fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
IMAGE="fleetanalytics-mcp:latest"
|
IMAGE="fleetanalytics-mcp:latest"
|
||||||
ENV_FILE="$(pwd)/.deploy.env"
|
ENV_FILE="$(pwd)/.deploy.env"
|
||||||
|
|
||||||
|
|
@ -53,9 +67,15 @@ RO_PW=$(cat "${ANALYTICS_RO_PW_FILE:-$HOME/.analytics_ro.pw}" 2>/dev/null || tru
|
||||||
HOSTPART="${SRC_DB_URL#*@}" # host:port/dbname[?params]
|
HOSTPART="${SRC_DB_URL#*@}" # host:port/dbname[?params]
|
||||||
RO_DB_URL="postgresql://analytics_ro:${RO_PW}@${HOSTPART}"
|
RO_DB_URL="postgresql://analytics_ro:${RO_PW}@${HOSTPART}"
|
||||||
|
|
||||||
# Build the image from this repo.
|
# Build the image from this repo (SKIP_BUILD=1 reuses the existing image for a
|
||||||
echo "Building $IMAGE ..."
|
# labels/env-only change — no new code is pulled in).
|
||||||
docker build -t "$IMAGE" .
|
if [ "${SKIP_BUILD:-0}" = "1" ]; then
|
||||||
|
echo "SKIP_BUILD=1 — reusing existing $IMAGE (no rebuild)."
|
||||||
|
docker image inspect "$IMAGE" >/dev/null 2>&1 || { echo "ERROR: $IMAGE not present"; exit 1; }
|
||||||
|
else
|
||||||
|
echo "Building $IMAGE ..."
|
||||||
|
docker build -t "$IMAGE" .
|
||||||
|
fi
|
||||||
|
|
||||||
# Minimal env (read-only DSN + auth only — no Tracksolid ingestion secrets).
|
# 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"
|
{ echo "DATABASE_URL=${RO_DB_URL}"; echo "MCP_AUTH_TOKENS=${MCP_AUTH_TOKENS}"; } > "$ENV_FILE"
|
||||||
|
|
@ -73,9 +93,9 @@ docker run -d --name "$NAME" --restart unless-stopped \
|
||||||
--label 'traefik.http.middlewares.fleetmcp-ratelimit.ratelimit.burst=60' \
|
--label 'traefik.http.middlewares.fleetmcp-ratelimit.ratelimit.burst=60' \
|
||||||
--label "traefik.http.routers.http-0-fleetmcp.entryPoints=http" \
|
--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.middlewares=redirect-to-https" \
|
||||||
--label "traefik.http.routers.http-0-fleetmcp.rule=Host(\`${HOST_DOMAIN}\`)" \
|
--label "traefik.http.routers.http-0-fleetmcp.rule=${RULE}" \
|
||||||
--label "traefik.http.routers.https-0-fleetmcp.entryPoints=https" \
|
--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.rule=${RULE}" \
|
||||||
--label "traefik.http.routers.https-0-fleetmcp.middlewares=fleetmcp-ratelimit" \
|
--label "traefik.http.routers.https-0-fleetmcp.middlewares=fleetmcp-ratelimit" \
|
||||||
--label "traefik.http.routers.https-0-fleetmcp.tls=true" \
|
--label "traefik.http.routers.https-0-fleetmcp.tls=true" \
|
||||||
--label "traefik.http.routers.https-0-fleetmcp.tls.certresolver=letsencrypt" \
|
--label "traefik.http.routers.https-0-fleetmcp.tls.certresolver=letsencrypt" \
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue