97 lines
3.7 KiB
Bash
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
|