Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Deny-all build context: the Containerfile COPYs exactly these paths.
*
!pyproject.toml
!USER_README.md
!LICENSE
!src/
!src/**
**/*.egg-info/**
**/__pycache__/**
**/*.pyc
!requirements-container.txt
517 changes: 517 additions & 0 deletions .github/workflows/container.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ temp/

# macOS
.DS_Store
.playwright-mcp
6 changes: 6 additions & 0 deletions .lockgen.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Seed inputs for requirements-container.txt that pyproject.toml does not
# provide: the PEP 517 build backend, pinned so the project wheel can build
# with `pip wheel --no-build-isolation` inside the Containerfile's builder
# stage. Keep these floors in sync with [build-system] requires.
setuptools>=78.1.1
wheel>=0.46.2
15 changes: 15 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ Before submitting changes:
2. `ruff check .` - lint passes
3. `mypy src/` - type checks pass

### Container dependency lock

`requirements-container.txt` is the hash-pinned dependency closure installed into the container image (root `Containerfile`). Regenerate and commit it whenever `pyproject.toml` dependencies, `[build-system] requires`, or `.lockgen.in` change:

```bash
uv pip compile pyproject.toml .lockgen.in \
--generate-hashes --universal --no-build --python-version 3.12 \
--exclude-newer "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o requirements-container.txt
```

- `.lockgen.in` seeds the PEP 517 build backend (setuptools/wheel) so the project wheel can build with `pip wheel --no-build-isolation` in the image's builder stage; keep its floors in sync with `[build-system] requires` in `pyproject.toml`.
- `--python-version 3.12` (the image's interpreter) is **required**: without it uv omits `typing-extensions` (an `anyio` requirement on Python < 3.13) and the image build fails `pip check`.
- Record the new cutoff date in the lock file's header comment, then verify with `docker build -f Containerfile -t mcp-reverse-proxy .` before committing.

## Coding Standards

- **Python >= 3.11** with type hints
Expand Down
112 changes: 112 additions & 0 deletions Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# ----------------------------------------------------------------------------
# MCP Reverse Proxy - client/test image (multi-stage builder -> runtime).
# UBI 10 throughout: builder = ubi10/ubi-minimal + python3.12 RPMs (no full
# python-312 s2i image exists for UBI 10 - Red Hat catalog verified 404),
# runtime = ubi10/python-312-minimal. Pattern follows IBM/mcp-context-forge:
# overridable base-image ARGs, venv built in builder, copied to runtime,
# non-root user, OCI labels.
# ----------------------------------------------------------------------------

# uv is delivered by copying its static binaries out of the official image.
# PINNED BY DIGEST (supply chain): resolve the current digest of the uv 0.9
# line and replace <UV_DIGEST> below; resolved version: uv 0.9.30. Docker does
# NOT support inline comments on ARG lines - keep this note as full-line
# comments only.
# docker buildx imagetools inspect ghcr.io/astral-sh/uv:0.9
ARG UV_IMAGE=ghcr.io/astral-sh/uv@sha256:538e0b39736e7feae937a65983e49d2ab75e1559d35041f9878b7b7e51de91e4
# Base tags: floating Red Hat tags (deliberate - CVE rebuilds flow in; see
# Scope OUT). Both MUST resolve before building; todo-1 acceptance enforces:
# docker buildx imagetools inspect registry.access.redhat.com/ubi10/ubi-minimal:latest
# docker buildx imagetools inspect registry.access.redhat.com/ubi10/python-312-minimal:latest
ARG PYTHON_BUILDER=registry.access.redhat.com/ubi10/ubi-minimal:latest
ARG PYTHON_RUNTIME=registry.access.redhat.com/ubi10/python-312-minimal:latest

FROM ${UV_IMAGE} AS uv

# ------------------------------------------------------------------ builder
FROM ${PYTHON_BUILDER} AS builder

# UBI 10 ships no full python-312 s2i image (see header), so the builder is
# ubi-minimal plus the Python 3.12 RPMs - the mcp-context-forge wheel-image
# pattern. Dependencies install from the COMMITTED hash-pinned lock file
# (requirements-container.txt - regeneration documented in CONTRIBUTING.md;
# pure-wheel closure, no compilers needed. Contingency if a dep ever builds from
# source: add python3.12-devel gcc to the microdnf line.
USER root
WORKDIR /build

RUN microdnf install -y --nodocs --setopt=install_weak_deps=0 \
python3.12 python3.12-pip \
&& microdnf clean all

COPY pyproject.toml USER_README.md LICENSE requirements-container.txt ./
COPY src/ ./src/

# Locked closure: hash-verified deps + pinned build backend (both from the
# lock file); the project wheel builds with --no-build-isolation against
# THOSE pinned build tools, then installs with --no-deps. No `pip install -U
# pip`, no loose resolution - the Python dependency closure is hash-pinned
# (the floating UBI base and RPM inputs are deliberately NOT - see Scope OUT).
RUN python3.12 -m venv /opt/venv \
&& /opt/venv/bin/pip install --no-cache-dir --require-hashes --only-binary=:all: -r requirements-container.txt \
&& /opt/venv/bin/pip wheel --no-cache-dir --no-deps --no-build-isolation --wheel-dir /dist . \
&& /opt/venv/bin/pip install --no-cache-dir --no-deps /dist/*.whl \
&& /opt/venv/bin/pip check

# ------------------------------------------------------------------ runtime
FROM ${PYTHON_RUNTIME} AS runtime
# NOTE: the s2i runtime base's image config inherits EXPOSE 8080/tcp. It is
# inert metadata for this client (nothing listens); OCI/Docker has no
# UNEXPOSE directive, so it is accepted and documented, not removed.

ARG VERSION=dev
ARG REVISION=unknown

USER root

COPY --from=uv /uv /uvx /usr/local/bin/
COPY --from=builder /opt/venv /opt/venv

# git: the documented run example uses `uvx mcp-server-git`, whose GitPython
# dependency shells out to the git executable. ca-certificates for TLS.
# Writable HOME + uv cache: uvx downloads MCP server packages at run time
# and the runtime user is non-root (group-0 per UBI/OpenShift convention).
RUN microdnf install -y --nodocs --setopt=install_weak_deps=0 git ca-certificates \
&& microdnf clean all \
&& mkdir -p /home/default /tmp/uv-cache \
&& chown -R 1001:0 /home/default /tmp/uv-cache \
&& chmod -R g=u /home/default /tmp/uv-cache

ENV VIRTUAL_ENV=/opt/venv \
PATH="/opt/venv/bin:/usr/local/bin:$PATH" \
HOME=/home/default \
UV_CACHE_DIR=/tmp/uv-cache \
BASH_ENV= \
ENV= \
PROMPT_COMMAND= \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

LABEL name="mcp-reverse-proxy" \
maintainer="Mihai Criveti" \
org.opencontainers.image.title="mcp-reverse-proxy" \
org.opencontainers.image.description="MCP Reverse Proxy - bridge MCP servers to remote ContextForge gateways over TLS (stdio, SSE, Streamable HTTP, WebSocket)" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.source="https://github.com/contextforge-org/mcp-reverse-proxy" \
org.opencontainers.image.url="https://github.com/contextforge-org/mcp-reverse-proxy" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${REVISION}"

USER 1001
WORKDIR /home/default

ENTRYPOINT ["mcp-reverse-proxy"]
# Default command is INERT: prints usage and exits 0 (nothing executes that
# isn't part of the signed, scanned image). Bridge a server by passing the
# full arguments explicitly, e.g. --local-stdio "uvx mcp-server-git" with
# REVERSE_PROXY_GATEWAY + REVERSE_PROXY_TOKEN (or --gateway/--token) set;
# without a gateway the client exits non-zero (cli.py:339).
# TRUST BOUNDARY: MCP server packages downloaded at run time (e.g. via uvx)
# are NOT covered by this image's SBOM, vulnerability scan, or cosign
# signature - pin them (e.g. `uvx mcp-server-git==<version>`) for production.
CMD ["--help"]
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,36 @@ mcp-reverse-proxy \

Configuration can also come from environment variables (`REVERSE_PROXY_GATEWAY`, `REVERSE_PROXY_TOKEN`) or a JSON/YAML config file passed with `--config`.

## Container image

A multi-arch (`linux/amd64`, `linux/arm64`) UBI 10-based image is built from the root `Containerfile` and published to GitHub Container Registry:

```bash
docker pull ghcr.io/contextforge-org/mcp-reverse-proxy:latest

docker run --rm \
-e REVERSE_PROXY_GATEWAY="wss://gateway.example.com/reverse-proxy" \
-e REVERSE_PROXY_TOKEN="$GATEWAY_TOKEN" \
ghcr.io/contextforge-org/mcp-reverse-proxy:latest \
--local-stdio "uvx mcp-server-git" # explicit transport - the inert default command only prints usage
```

| Tag | Published when |
| --- | --- |
| `latest` | every push to `main` |
| `X.Y.Z` | a `v*` release tag is pushed (immutable full-version tag; no `v` prefix, no moving aliases) |
| `sha-<commit>` | every push to `main` |

(Prerelease tags such as `v1.2.3-rc.1` publish only the full version tag, e.g. `1.2.3-rc.1` — no `X.Y`/`X` aliases.)

The image runs as non-root (UID 1001) and includes `uv`/`uvx` and `git`, so stdio MCP servers such as `uvx mcp-server-git` work out of the box. Its default command is inert (prints usage via `--help`); bridge a server by passing the full arguments, e.g. appending `--local-stdio "uvx mcp-server-git"`. Note that MCP server packages downloaded at run time (e.g. via `uvx`) are not covered by the image's signature or vulnerability scan — pin them (for example `uvx mcp-server-git==<version>`) for production use. Images are keyless-signed with cosign:

```bash
cosign verify ghcr.io/contextforge-org/mcp-reverse-proxy:latest \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
--certificate-identity-regexp='^https://github\.com/contextforge-org/mcp-reverse-proxy/\.github/workflows/container\.yml@refs/(heads/main|tags/v.*)$'
```

## Features

- **Multi-transport MCP server connections**: stdio (subprocess), Streamable HTTP (HTTP/2), and SSE
Expand Down
39 changes: 14 additions & 25 deletions USER_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,38 +518,27 @@ sudo systemctl start mcp-reverse-proxy
sudo systemctl status mcp-reverse-proxy
```

### Docker Container
### Container image

Create `Dockerfile`:

```dockerfile
FROM python:3.11-slim

WORKDIR /app

# Install the reverse proxy
COPY . .
RUN pip install --no-cache-dir .

# Set environment variables
ENV REVERSE_PROXY_GATEWAY=""
ENV REVERSE_PROXY_TOKEN=""
ENV LOG_FORMAT=json

# Run the proxy
ENTRYPOINT ["mcp-reverse-proxy"]
CMD ["--local-stdio", "uvx mcp-server-git"]
```

Build and run:
Pre-built multi-arch images (UBI 10, non-root, cosign-signed) are published to GitHub Container Registry:

```bash
docker build -t mcp-reverse-proxy .
docker pull ghcr.io/contextforge-org/mcp-reverse-proxy:latest

docker run -d \
--name mcp-proxy \
-e REVERSE_PROXY_GATEWAY="wss://gateway.example.com/reverse-proxy" \
-e REVERSE_PROXY_TOKEN="your-token" \
mcp-reverse-proxy
ghcr.io/contextforge-org/mcp-reverse-proxy:latest \
--local-stdio "uvx mcp-server-git" # explicit transport - the inert default command only prints usage
```

The image's default command is inert (prints usage via `--help`); bridge a server by appending the full arguments, e.g. `--local-stdio "uvx mcp-server-git"` (the matching `REVERSE_PROXY_*` env vars or flags are required — without them the client exits with a config error). `uv`/`uvx` and `git` are included in the image for stdio MCP servers. Configuration works exactly as described above — CLI arguments, environment variables, or a mounted config file. Note: MCP server packages downloaded at run time (e.g. via `uvx`) are not covered by the image's signature or vulnerability scan — pin versions for production use.

To build locally instead, use the root `Containerfile` (UBI 10 builder + minimal runtime):

```bash
docker build -f Containerfile -t mcp-reverse-proxy .
```

## Troubleshooting
Expand Down
Loading
Loading