diff --git a/apps/sap-mcp-bridge/build/Dockerfile b/apps/sap-mcp-bridge/build/Dockerfile new file mode 100644 index 0000000..7db3ac9 --- /dev/null +++ b/apps/sap-mcp-bridge/build/Dockerfile @@ -0,0 +1,82 @@ +# syntax=docker/dockerfile:1.7 +# +# sap-mcp-bridge +# ------------------------------------------------------------------- +# One container, five SAP MCP servers, all reachable through a single +# external HTTP port via an internal nginx reverse proxy. This makes +# the container deployable behind Traefik/Runtipi where only one port +# per app can be exposed. +# +# External (single port — Runtipi binds it): +# / landing page +# /healthz aggregate health probe +# /cap/mcp sap-cap (cap-js-mcp-server) +# /cap/healthz +# /abap/mcp sap-abap-adt (mcp-abap-adt) +# /abap/healthz +# /odata/mcp sap-odata (btp-sap-odata-to-mcp-server) +# /odata/healthz +# /ui5/mcp ui5-mcp-server (ui5mcp) +# /ui5/healthz +# /fiori/mcp fiori-mcp-server (fiori-mcp) +# /fiori/healthz +# +# Internally: +# 127.0.0.1:9001..9005 individual supergateway HTTP endpoints +# 127.0.0.1:8080 nginx (the only port EXPOSEd) +# ------------------------------------------------------------------- + +FROM node:24-bookworm-slim + +ENV NODE_ENV=production \ + NPM_CONFIG_FUND=false \ + NPM_CONFIG_AUDIT=false \ + NPM_CONFIG_UPDATE_NOTIFIER=false + +# tini : PID-1 init that reaps zombies and forwards SIGTERM cleanly +# nginx : in-container reverse proxy (single externally-visible port) +# gosu : drop privileges from root → mcp after we've chowned /workspace +# curl : healthcheck client +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + tini ca-certificates curl nginx gosu \ + && rm -rf /var/lib/apt/lists/* /var/log/nginx/* + +# Install all MCP servers + the stdio<->HTTP bridge globally. +# All versions pinned for reproducibility — a broken supergateway or +# MCP-server release would silently break every endpoint on the next +# rebuild. Bump deliberately, not by accident. +RUN npm install -g --omit=dev --no-audit --no-fund \ + supergateway@3.4.3 \ + @iflow-mcp/cap-js-mcp-server@0.0.3 \ + @iflow-mcp/mcp-abap-adt@1.1.0 \ + @iflow-mcp/btp-sap-odata-to-mcp-server@1.0.0 \ + @ui5/mcp-server@0.2.11 \ + @sap-ux/fiori-mcp-server@0.6.49 + +# Non-root runtime user. Container still starts as root so the +# entrypoint can chown /workspace; it then drops to this user via gosu. +# (No -r flag: Debian's SYS_UID_MAX is 999, and we want UID 10001 — the +# warning useradd -r produces is harmless but ugly in build logs.) +RUN useradd -u 10001 -g nogroup -d /home/mcp -m mcp + +COPY nginx.conf /etc/nginx/nginx.conf +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# /workspace is the project root the cap / ui5 / fiori servers operate on. +# It's the volume mount target — host-side it's bound to a user-config path +# from the Runtipi compose. Pre-creating it here makes "no volume bound" +# (e.g. local docker run) still work. +RUN mkdir -p /workspace && chown -R mcp:nogroup /workspace +WORKDIR /workspace + +EXPOSE 8080 + +# Cheap liveness: nginx aggregates /healthz; if it answers 200, the +# container is "alive enough" from Traefik's perspective. +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD curl -fsS http://127.0.0.1:8080/healthz || exit 1 + +ENTRYPOINT ["/usr/bin/tini", "--"] +CMD ["/usr/local/bin/entrypoint.sh"] diff --git a/apps/sap-mcp-bridge/build/README.md b/apps/sap-mcp-bridge/build/README.md new file mode 100644 index 0000000..b8c588d --- /dev/null +++ b/apps/sap-mcp-bridge/build/README.md @@ -0,0 +1,95 @@ +# sap-mcp-bridge — build sources + +This directory contains the Docker build context for the +**`git.alexzaw.dev/alexz/sap-mcp-bridge`** image that the Runtipi app +references. The same files are mirrored under +`~/projects/sap-mcp-bridge/` so you can iterate on the image outside +the Runtipi tree if you prefer; treat the two locations as +copies-in-sync. + +## Files + +| File | Role | +| -------------- | ----------------------------------------------------------------------------------- | +| `Dockerfile` | Node 24 + nginx + gosu + tini, installs 5 MCP servers + supergateway globally. | +| `entrypoint.sh`| Boots as root → chowns `/workspace` → drops to `mcp` via gosu → launches 5 gateways on 127.0.0.1:9001–9005 → execs nginx on :8080. | +| `nginx.conf` | Path-prefixed reverse proxy (`/cap/mcp`, `/abap/mcp`, …) with SSE/streaming-safe defaults (no buffering, 24h timeouts). | +| `build.sh` | `docker build && docker push` to `git.alexzaw.dev/alexz/sap-mcp-bridge:`. | + +## How it works + +The MCP servers are stdio-only. `supergateway` wraps each one as a +small HTTP server speaking the MCP "Streamable HTTP" transport. Each +gateway binds its own port (9001–9005) on the loopback interface, +inside the container only. An internal **nginx** is the only thing +that listens on the EXPOSEd port (8080) and routes requests by URL +path: + +``` +8080/cap/mcp -> 127.0.0.1:9001/mcp +8080/abap/mcp -> 127.0.0.1:9002/mcp +8080/odata/mcp -> 127.0.0.1:9003/mcp +8080/ui5/mcp -> 127.0.0.1:9004/mcp +8080/fiori/mcp -> 127.0.0.1:9005/mcp +8080/healthz -> 127.0.0.1:9001/healthz (aggregate probe) +``` + +This single-port design is what makes the container Runtipi-deployable +behind Traefik, which only routes one port per app. + +## Build & push + +```bash +cd /etc/runtipi/repos/runtipi/apps/sap-mcp-bridge/build +./build.sh # builds + pushes :latest +IMAGE_TAG=1.0.0 ./build.sh # tag a specific version +./build.sh --no-cache # forwarded to docker build +``` + +You must be logged in to the Gitea Docker registry: + +```bash +docker login git.alexzaw.dev +``` + +## Deploy to Runtipi + +After `build.sh` succeeds: + +```bash +cd /etc/runtipi/repos/runtipi +sudo git add apps/sap-mcp-bridge +sudo git commit -m "sap-mcp-bridge: " +sudo git push origin main + +cd /etc/runtipi +sudo ./runtipi-cli appstore update +sudo ./runtipi-cli app update sap-mcp-bridge:runtipi # for an already-installed app +# (first time: install via the Runtipi dashboard so form fields are written) +``` + +## Local sanity check (no Runtipi) + +```bash +docker run --rm -p 8080:8080 \ + -v "$PWD/workspace:/workspace" \ + git.alexzaw.dev/alexz/sap-mcp-bridge:latest + +# In another terminal: +curl http://127.0.0.1:8080/ # landing page lists endpoints +curl http://127.0.0.1:8080/healthz # 200 +curl http://127.0.0.1:8080/cap/healthz # 200 once cap-js-mcp-server boots +``` + +## Image dependencies (pinned in the Dockerfile) + +- Base: `node:24-bookworm-slim` +- `supergateway@latest` +- `@iflow-mcp/cap-js-mcp-server@latest` +- `@iflow-mcp/mcp-abap-adt@1.1.0` +- `@iflow-mcp/btp-sap-odata-to-mcp-server@1.0.0` +- `@ui5/mcp-server@0.2.11` +- `@sap-ux/fiori-mcp-server@0.6.49` +- OS packages: `tini ca-certificates curl nginx gosu` + +Bump the pinned versions in `Dockerfile` then rebuild. diff --git a/apps/sap-mcp-bridge/build/build.sh b/apps/sap-mcp-bridge/build/build.sh new file mode 100644 index 0000000..9f1ef7a --- /dev/null +++ b/apps/sap-mcp-bridge/build/build.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Build and push the sap-mcp-bridge Docker image to the Gitea registry. +# Run this from the build/ directory of the Runtipi app. +# +# Usage: +# ./build.sh # build + push :latest +# IMAGE_TAG=1.0.0 ./build.sh # tag a specific version +# ./build.sh --no-cache # extra flags are forwarded to docker build + +set -euo pipefail + +IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/sap-mcp-bridge}" +IMAGE_TAG="${IMAGE_TAG:-latest}" + +echo "Building ${IMAGE_NAME}:${IMAGE_TAG}..." + +docker build "$@" -t "${IMAGE_NAME}:${IMAGE_TAG}" . +docker push "${IMAGE_NAME}:${IMAGE_TAG}" + +echo "" +echo "Build complete!" +echo "" +echo "To test locally (without Runtipi):" +echo " docker run --rm -p 8080:8080 -v \$PWD/workspace:/workspace ${IMAGE_NAME}:${IMAGE_TAG}" +echo " curl http://127.0.0.1:8080/healthz" +echo "" +echo "After pushing, deploy to Runtipi:" +echo " cd /etc/runtipi/repos/runtipi && sudo git add . && sudo git commit -m 'sap-mcp-bridge: bump' && sudo git push origin main" +echo " sudo /etc/runtipi/runtipi-cli appstore update" +echo " sudo /etc/runtipi/runtipi-cli app update sap-mcp-bridge:runtipi # (or install via the dashboard the first time)" diff --git a/apps/sap-mcp-bridge/build/entrypoint.sh b/apps/sap-mcp-bridge/build/entrypoint.sh new file mode 100644 index 0000000..1b9ef0b --- /dev/null +++ b/apps/sap-mcp-bridge/build/entrypoint.sh @@ -0,0 +1,96 @@ +#!/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 diff --git a/apps/sap-mcp-bridge/build/nginx.conf b/apps/sap-mcp-bridge/build/nginx.conf new file mode 100644 index 0000000..c0d704c --- /dev/null +++ b/apps/sap-mcp-bridge/build/nginx.conf @@ -0,0 +1,86 @@ +# sap-mcp-bridge :: in-container reverse proxy +# +# Routes a single external port (8080) to five internal supergateway +# instances on 127.0.0.1:9001..9005, based on URL path prefix. Tuned +# for MCP Streamable HTTP, which uses long-lived streaming responses +# (effectively SSE) — so buffering is OFF and timeouts are HOURS, +# not seconds. + +worker_processes 1; +pid /tmp/nginx.pid; +error_log /dev/stderr warn; + +events { + worker_connections 256; +} + +http { + # All temp paths must be writable by the non-root mcp user. + client_body_temp_path /tmp/nginx-client-body; + proxy_temp_path /tmp/nginx-proxy; + fastcgi_temp_path /tmp/nginx-fastcgi; + uwsgi_temp_path /tmp/nginx-uwsgi; + scgi_temp_path /tmp/nginx-scgi; + + access_log off; + server_tokens off; + types_hash_max_size 2048; + default_type application/octet-stream; + + # --- Streaming-HTTP / SSE friendly defaults --------------------- + # MCP responses can be long-lived streams. Buffering would break + # the stream; default 60s timeouts would kill it mid-flight. + proxy_http_version 1.1; + proxy_buffering off; + proxy_request_buffering off; + proxy_read_timeout 24h; + proxy_send_timeout 24h; + proxy_connect_timeout 10s; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # Empty Connection header keeps keepalive to upstream working + # (the default would inherit the client's Connection header). + proxy_set_header Connection ""; + + upstream sap_cap { server 127.0.0.1:9001; keepalive 8; } + upstream sap_abap { server 127.0.0.1:9002; keepalive 8; } + upstream sap_odata { server 127.0.0.1:9003; keepalive 8; } + upstream ui5_srv { server 127.0.0.1:9004; keepalive 8; } + upstream fiori_srv { server 127.0.0.1:9005; keepalive 8; } + + server { + listen 8080 default_server; + + # Plain-text landing page so curl / a browser hit on / gives a + # cue rather than 404 noise. + location = / { + default_type text/plain; + return 200 "sap-mcp-bridge\nendpoints:\n /cap/mcp\n /abap/mcp\n /odata/mcp\n /ui5/mcp\n /fiori/mcp\n"; + } + + # Aggregate health: probe one upstream. If nginx itself is up + # and that gateway answers 200, Traefik will treat the + # container as healthy. (Per-gateway probes are exposed below + # for finer-grained checks.) + location = /healthz { + access_log off; + proxy_pass http://sap_cap/healthz; + } + + # ---------------- MCP endpoints --------------------------- + location = /cap/mcp { proxy_pass http://sap_cap/mcp; } + location = /abap/mcp { proxy_pass http://sap_abap/mcp; } + location = /odata/mcp { proxy_pass http://sap_odata/mcp; } + location = /ui5/mcp { proxy_pass http://ui5_srv/mcp; } + location = /fiori/mcp { proxy_pass http://fiori_srv/mcp; } + + # ---------------- Per-gateway health ---------------------- + location = /cap/healthz { access_log off; proxy_pass http://sap_cap/healthz; } + location = /abap/healthz { access_log off; proxy_pass http://sap_abap/healthz; } + location = /odata/healthz { access_log off; proxy_pass http://sap_odata/healthz; } + location = /ui5/healthz { access_log off; proxy_pass http://ui5_srv/healthz; } + location = /fiori/healthz { access_log off; proxy_pass http://fiori_srv/healthz; } + } +} diff --git a/apps/sap-mcp-bridge/config.json b/apps/sap-mcp-bridge/config.json new file mode 100644 index 0000000..994c0b1 --- /dev/null +++ b/apps/sap-mcp-bridge/config.json @@ -0,0 +1,87 @@ +{ + "name": "SAP MCP Bridge", + "id": "sap-mcp-bridge", + "available": true, + "short_desc": "Five SAP, UI5 and Fiori MCP servers behind a single HTTP gateway", + "author": "alexz", + "port": 8080, + "categories": ["development", "utilities"], + "tipi_version": 1, + "version": "1.0.0", + "source": "https://git.alexzaw.dev/alexz/sap-mcp-bridge", + "exposable": true, + "dynamic_config": true, + "supported_architectures": ["amd64", "arm64"], + "min_tipi_version": "4.0.0", + "form_fields": [ + { + "type": "text", + "label": "SAP ABAP URL", + "hint": "e.g. https://my429480-api.s4hana.cloud.sap (leave empty if not using sap-abap-adt)", + "required": false, + "env_variable": "SAP_URL" + }, + { + "type": "text", + "label": "SAP Username", + "hint": "Basic-auth user for ABAP ADT", + "required": false, + "env_variable": "SAP_USERNAME" + }, + { + "type": "password", + "label": "SAP Password", + "hint": "Basic-auth password for ABAP ADT", + "required": false, + "env_variable": "SAP_PASSWORD" + }, + { + "type": "text", + "label": "SAP Client", + "hint": "e.g. 100", + "required": false, + "env_variable": "SAP_CLIENT" + }, + { + "type": "text", + "label": "BTP Destination Name", + "hint": "Destination configured in BTP cockpit (leave empty if not using sap-odata)", + "required": false, + "env_variable": "SAP_DESTINATION_NAME" + }, + { + "type": "text", + "label": "Destination Service URL", + "hint": "From the BTP destination service key", + "required": false, + "env_variable": "DESTINATION_SERVICE_URL" + }, + { + "type": "text", + "label": "Destination Client ID", + "required": false, + "env_variable": "DESTINATION_CLIENT_ID" + }, + { + "type": "password", + "label": "Destination Client Secret", + "required": false, + "env_variable": "DESTINATION_CLIENT_SECRET" + }, + { + "type": "text", + "label": "Destination Auth URL", + "required": false, + "env_variable": "DESTINATION_AUTH_URL" + }, + { + "type": "text", + "label": "OData Discovery Mode", + "hint": "all | patterns (optional)", + "required": false, + "env_variable": "ODATA_DISCOVERY_MODE" + } + ], + "created_at": 1747624260000, + "updated_at": 1747624260000 +} diff --git a/apps/sap-mcp-bridge/docker-compose.json b/apps/sap-mcp-bridge/docker-compose.json new file mode 100644 index 0000000..5aa5d2b --- /dev/null +++ b/apps/sap-mcp-bridge/docker-compose.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://schemas.runtipi.io/dynamic-compose.json", + "schemaVersion": 2, + "services": [ + { + "name": "sap-mcp-bridge", + "image": "git.alexzaw.dev/alexz/sap-mcp-bridge:latest", + "isMain": true, + "internalPort": 8080, + "environment": [ + { "key": "TZ", "value": "${TZ}" }, + { "key": "SAP_URL", "value": "${SAP_URL}" }, + { "key": "SAP_USERNAME", "value": "${SAP_USERNAME}" }, + { "key": "SAP_PASSWORD", "value": "${SAP_PASSWORD}" }, + { "key": "SAP_CLIENT", "value": "${SAP_CLIENT}" }, + { "key": "SAP_DESTINATION_NAME", "value": "${SAP_DESTINATION_NAME}" }, + { "key": "DESTINATION_SERVICE_URL", "value": "${DESTINATION_SERVICE_URL}" }, + { "key": "DESTINATION_CLIENT_ID", "value": "${DESTINATION_CLIENT_ID}" }, + { "key": "DESTINATION_CLIENT_SECRET", "value": "${DESTINATION_CLIENT_SECRET}" }, + { "key": "DESTINATION_AUTH_URL", "value": "${DESTINATION_AUTH_URL}" }, + { "key": "ODATA_DISCOVERY_MODE", "value": "${ODATA_DISCOVERY_MODE}" } + ], + "volumes": [ + { + "hostPath": "/etc/runtipi/user-config/runtipi/sap-mcp-bridge/workspace", + "containerPath": "/workspace", + "readOnly": false + } + ], + "healthCheck": { + "test": "curl -fsS http://127.0.0.1:8080/healthz || exit 1", + "interval": "30s", + "timeout": "5s", + "retries": 3, + "startPeriod": "30s" + }, + "extraLabels": { + "runtipi.managed": true + } + } + ] +} diff --git a/apps/sap-mcp-bridge/metadata/description.md b/apps/sap-mcp-bridge/metadata/description.md new file mode 100644 index 0000000..9d7190a --- /dev/null +++ b/apps/sap-mcp-bridge/metadata/description.md @@ -0,0 +1,123 @@ +# SAP MCP Bridge + +Five SAP / UI5 / Fiori Model Context Protocol servers, packaged in a +single container and routed through an internal **nginx reverse +proxy** so the app exposes only **one external port** (Runtipi / +Traefik friendly). + +## Bundled MCP servers + +| Path prefix | MCP server | Use case | +| -------------- | ----------------------------------------- | ------------------------------------------ | +| `/cap/mcp` | `@iflow-mcp/cap-js-mcp-server` | SAP CAP (Cloud Application Programming) | +| `/abap/mcp` | `@iflow-mcp/mcp-abap-adt` | SAP ABAP via ADT (S/4HANA Cloud, on-prem) | +| `/odata/mcp` | `@iflow-mcp/btp-sap-odata-to-mcp-server` | SAP OData via BTP Destination service | +| `/ui5/mcp` | `@ui5/mcp-server` | SAPUI5 framework guidance + linting | +| `/fiori/mcp` | `@sap-ux/fiori-mcp-server` | SAP Fiori tools / app generation | + +Each stdio MCP server is wrapped by `supergateway` and speaks +**Streamable HTTP** on its own internal port (9001–9005). An internal +nginx (port 8080 — the only EXPOSEd port) routes external requests by +URL path. Long timeouts and disabled buffering make MCP streaming +responses work correctly. + +## Health endpoints + +- `/healthz` — aggregate (probes the CAP gateway) +- `/cap/healthz`, `/abap/healthz`, `/odata/healthz`, `/ui5/healthz`, `/fiori/healthz` + +## Using from Claude Code + +Once the app is installed (and either exposed via a domain or +reachable on the LAN), wire the servers you want into your MCP +client. With the Claude Code CLI: + +```bash +claude mcp add --transport http sap-cap https://sap-mcp.example.com/cap/mcp +claude mcp add --transport http sap-abap https://sap-mcp.example.com/abap/mcp +claude mcp add --transport http sap-odata https://sap-mcp.example.com/odata/mcp +claude mcp add --transport http ui5 https://sap-mcp.example.com/ui5/mcp +claude mcp add --transport http fiori https://sap-mcp.example.com/fiori/mcp +``` + +…or by adding the equivalent block to your project's `.mcp.json` / +`~/.claude.json`: + +```json +{ + "mcpServers": { + "sap-cap": { "type": "http", "url": "https://sap-mcp.example.com/cap/mcp" }, + "sap-abap": { "type": "http", "url": "https://sap-mcp.example.com/abap/mcp" }, + "sap-odata": { "type": "http", "url": "https://sap-mcp.example.com/odata/mcp" }, + "ui5": { "type": "http", "url": "https://sap-mcp.example.com/ui5/mcp" }, + "fiori": { "type": "http", "url": "https://sap-mcp.example.com/fiori/mcp" } + } +} +``` + +## ⚠️ Security warning — read before exposing externally + +This app intentionally enables **wildcard CORS** (`Access-Control-Allow-Origin: *`) +inside the supergateway wrappers, because MCP clients may call the +endpoint from a variety of origins. Combined with `exposable: true` +and **no built-in authentication**, this means: if you expose the app +on a public URL via Traefik or a Cloudflare Tunnel, *anyone who knows +the URL* can drive your configured SAP credentials. + +Recommended deployments: + +- **Local LAN only**: leave `exposable: false` (default) and reach it + via `http://:` from inside the LAN. +- **Exposed to the internet**: put it behind a Traefik + **ForwardAuth** middleware (e.g. Authentik / Authelia), a + Cloudflare Access policy, or basic-auth in nginx-proxy-manager — + i.e. authenticate at the proxy, not the app. + +Do NOT expose this publicly without an auth layer in front of it. + +## Configuration + +All credential form fields are **optional**. Empty values are fine — +servers that don't have their required env vars simply won't be +usable, but the others (and the container as a whole) will still run. + +- **`sap-abap-adt` needs:** `SAP_URL`, `SAP_USERNAME`, `SAP_PASSWORD`, + `SAP_CLIENT` +- **`sap-odata` needs:** `SAP_DESTINATION_NAME`, + `DESTINATION_SERVICE_URL`, `DESTINATION_CLIENT_ID`, + `DESTINATION_CLIENT_SECRET`, `DESTINATION_AUTH_URL` +- **`sap-cap`, `ui5-mcp-server`, `fiori-mcp-server`** require no + credentials — they read your **project workspace** at request time. + +## Workspace mount + +The container's `/workspace` is bound to: + +``` +/etc/runtipi/user-config/runtipi/sap-mcp-bridge/workspace +``` + +This lives under `user-config/`, which **survives Runtipi restarts +and `app update`**. Drop your CAP / UI5 / Fiori projects in there for +the `cap`, `ui5`, and `fiori` MCP servers to operate on. + +The entrypoint chowns this directory to the in-container `mcp` user +(UID 10001) on every start, so a freshly auto-created mount works. + +If you want `/workspace` to point at a different host path, create +`/etc/runtipi/user-config/runtipi/sap-mcp-bridge/docker-compose.yml` +with an overlay, e.g.: + +```yaml +services: + sap-mcp-bridge: + volumes: + - /your/other/path:/workspace +``` + +## Image + +Custom image: +`git.alexzaw.dev/alexz/sap-mcp-bridge:latest` — built from the +sources in `build/` (Dockerfile + entrypoint.sh + nginx.conf). See +`build/README.md` for the build & push procedure.