Files
runtipi/apps/sap-mcp-bridge/build/entrypoint.sh

97 lines
3.7 KiB
Bash

#!/usr/bin/env bash
# sap-mcp-bridge entrypoint
#
# Lifecycle:
# 1. (as root) Ensure /workspace exists and is owned by the mcp user,
# then re-exec ourselves as mcp via gosu. This makes the mount
# writable even when Docker auto-creates the host path as root.
# 2. (as mcp) Install the signal trap BEFORE forking any children, so
# a SIGTERM arriving during start-up still tears the whole group
# down cleanly.
# 3. (as mcp) Launch one supergateway per SAP MCP server on
# 127.0.0.1:9001..9005. They speak Streamable HTTP at /mcp and
# expose /healthz.
# 4. (as mcp) Wait for every gateway's /healthz to answer (with a
# cap), then launch nginx in the foreground. This avoids the
# cold-start window where nginx 502s because upstreams haven't
# bound yet.
# 5. (as mcp) `wait -n` — exit as soon as ANY direct child (gateway
# or nginx) dies. Docker restarts the container, so a crashed
# gateway resets the whole group (acceptable for homelab use).
set -euo pipefail
WORKDIR_HOST="${WORKDIR_HOST:-/workspace}"
APP_UID=10001
APP_GID=65534 # nogroup
APP_USER="mcp"
GATEWAY_PORTS=(9001 9002 9003 9004 9005)
# --- Stage 1: privilege handling -----------------------------------
if [ "$(id -u)" = "0" ]; then
mkdir -p "$WORKDIR_HOST"
# Skip the chown if the dir is already owned correctly — recursive
# chown on a large mounted workspace is slow and runs every restart.
# The chown can fail on read-only / fuse / overlay mounts; we tolerate
# failure rather than crash (the mcp user just won't be able to write).
current_owner="$(stat -c '%u' "$WORKDIR_HOST" 2>/dev/null || echo "")"
if [ "$current_owner" != "$APP_UID" ]; then
chown -R "${APP_UID}:${APP_GID}" "$WORKDIR_HOST" 2>/dev/null || true
fi
exec gosu "${APP_USER}" "$0" "$@"
fi
# --- Stage 2: install trap BEFORE children -------------------------
# kill 0 sends SIGTERM to every process in our group; tini will reap
# them. Done now so a SIGTERM that lands during start-up still tears
# the whole group down cleanly.
trap 'echo "[sap-mcp-bridge] received signal, shutting down"; kill 0' SIGTERM SIGINT
# --- Stage 3: launch gateways --------------------------------------
cd "$WORKDIR_HOST" 2>/dev/null || cd /home/mcp
start() {
local name="$1" port="$2" cmd="$3"
echo "[sap-mcp-bridge] starting ${name} on 127.0.0.1:${port} -> ${cmd}"
# --stateful gives each MCP client its own subprocess (safer for the
# SAP servers, which keep auth state per-session).
supergateway \
--stdio "${cmd}" \
--outputTransport streamableHttp \
--port "${port}" \
--streamableHttpPath /mcp \
--healthEndpoint /healthz \
--cors \
--stateful \
--logLevel info \
&
}
start "sap-cap" 9001 "iflow-mcp_cap-js-mcp-server"
start "sap-abap-adt" 9002 "mcp-abap-adt"
start "sap-odata" 9003 "btp-sap-odata-to-mcp-server"
start "ui5-mcp-server" 9004 "ui5mcp"
start "fiori-mcp-server" 9005 "fiori-mcp"
# --- Stage 4: wait for upstreams, then nginx -----------------------
# Poll each gateway's /healthz for up to ~30s. Without this nginx can
# come up before the Node processes finish booting and serve 502s on
# the first requests (and to the Traefik healthcheck).
for port in "${GATEWAY_PORTS[@]}"; do
for _ in $(seq 30); do
if curl -fsS -m 1 "http://127.0.0.1:${port}/healthz" >/dev/null 2>&1; then
echo "[sap-mcp-bridge] upstream :${port} ready"
break
fi
sleep 1
done
done
echo "[sap-mcp-bridge] starting nginx reverse proxy on :8080"
nginx -g 'daemon off;' &
# --- Stage 5: shut down on first child death -----------------------
wait -n
echo "[sap-mcp-bridge] a child exited, taking the container down"
kill 0 2>/dev/null || true
exit 1