Skip to content

Commit 96c9309

Browse files
committed
feat: pf-4298 build, run container
* containerfile, multi-stage node 24 build * docs, new container run section, examples * config, container example * package.json, container:build and container:start scripts * scripts, build image, run container
1 parent 818016b commit 96c9309

8 files changed

Lines changed: 358 additions & 1 deletion

File tree

.containerignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Ignore everything by default
2+
*
3+
4+
# Allowlist only the files and directories required for the build context
5+
!package.json
6+
!package-lock.json
7+
!tsconfig.json
8+
!src/

Containerfile

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#
2+
# PatternFly MCP server — container image definition.
3+
#
4+
# Multi-stage build. Produces a minimal Node.js 24 runtime image that
5+
# launches the MCP server as a stdio process; HTTP transport is
6+
# available by passing `--http --port <n>` at `podman run` time (the
7+
# `ENTRYPOINT` forwards all arguments verbatim to the CLI).
8+
#
9+
# Base image: UBI 9 Node.js 24 minimal.
10+
# - Anonymous pull from `registry.access.redhat.com` (no login required).
11+
# - glibc-based, rootless-friendly, sets `APP_ROOT=/opt/app-root` and a
12+
# default non-root user at UID 1001.
13+
#
14+
#
15+
# ---- Stage 1: builder ---------------------------------------------------
16+
# Installs deps, compiles the bundle with pkgroll, and prunes dev deps so
17+
# only the runtime payload moves into stage 2.
18+
FROM registry.access.redhat.com/ubi9/nodejs-24-minimal:latest AS builder
19+
20+
# Non-root build/runtime user. Matches the UBI base default; exposed as an
21+
# ARG so downstream tooling can override without editing the file.
22+
ARG CONTAINER_UID=1001
23+
ENV CONTAINER_UID=${CONTAINER_UID}
24+
25+
# UBI defines `APP_ROOT=/opt/app-root`; reuse it so paths line up with the
26+
# base image's conventions.
27+
WORKDIR ${APP_ROOT}
28+
29+
# Copy only the files needed to resolve deps first.
30+
COPY --chown=${CONTAINER_UID}:0 package.json package-lock.json tsconfig.json ./
31+
32+
USER ${CONTAINER_UID}
33+
34+
# Lockfile-exact install.
35+
RUN npm ci --ignore-scripts --no-audit --no-fund
36+
37+
# Copy the rest of the source and produce the bundle.
38+
COPY --chown=${CONTAINER_UID}:0 src ./src
39+
RUN npx --no-install pkgroll --minify \
40+
&& npm prune --omit=dev --ignore-scripts
41+
42+
# ---- Stage 2: runtime ---------------------------------------------------
43+
# Minimal runtime image. Only the compiled bundle, pruned `node_modules`,
44+
# and `package.json` are carried over from the builder stage.
45+
FROM registry.access.redhat.com/ubi9/nodejs-24-minimal:latest AS runtime
46+
47+
# Standard OCI annotations
48+
LABEL org.opencontainers.image.title="patternfly-mcp" \
49+
org.opencontainers.image.description="PatternFly documentation MCP server (Node.js 24)" \
50+
org.opencontainers.image.source="https://github.com/patternfly/patternfly-mcp" \
51+
org.opencontainers.image.url="https://github.com/patternfly/patternfly-mcp" \
52+
org.opencontainers.image.licenses="MIT" \
53+
org.opencontainers.image.vendor="Red Hat" \
54+
io.modelcontextprotocol.transport="stdio"
55+
56+
# Re-declare the build arg in this stage. ARGs do not cross stages.
57+
ARG CONTAINER_UID=1001
58+
ENV CONTAINER_UID=${CONTAINER_UID}
59+
60+
# Runtime environment defaults:
61+
# - `NODE_ENV=production` enables production code paths in deps
62+
# - `NPM_CONFIG_UPDATE_NOTIFIER` suppress npm self-update nags
63+
# - `NO_COLOR` keep stderr/stdout machine-readable
64+
# (MCP clients consume these streams)
65+
ENV NODE_ENV=production \
66+
NPM_CONFIG_UPDATE_NOTIFIER=false \
67+
NO_COLOR=1
68+
69+
WORKDIR ${APP_ROOT}
70+
71+
# Runtime payload
72+
COPY --from=builder --chown=${CONTAINER_UID}:0 ${APP_ROOT}/package.json ${APP_ROOT}/package.json
73+
COPY --from=builder --chown=${CONTAINER_UID}:0 ${APP_ROOT}/node_modules ${APP_ROOT}/node_modules
74+
COPY --from=builder --chown=${CONTAINER_UID}:0 ${APP_ROOT}/dist ${APP_ROOT}/dist
75+
76+
USER ${CONTAINER_UID}
77+
78+
# stdio MCP server by default. Clients attach over stdin/stdout. `CMD`
79+
# provides only a default flag set; anything passed after the image name
80+
# on `podman run ... <image> <flags>` replaces `CMD` entirely, so every
81+
# CLI option (including `--http --port <n>` for HTTP transport) works
82+
# without rebuilding the image.
83+
ENTRYPOINT ["node", "dist/cli.js"]
84+
CMD ["--log-stderr"]

cspell.config.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"words": [
44
"amet",
55
"codemods",
6+
"containerfile",
67
"deprioritized",
78
"ized",
89
"llms",
@@ -16,13 +17,17 @@
1617
"prefault",
1718
"rereview",
1819
"rescan",
20+
"rootfs",
1921
"rsort",
22+
"sandboxed",
2023
"sandboxing",
2124
"sparkline",
2225
"streamable",
26+
"tmpfs",
2327
"treeify",
2428
"unrepresentable",
2529
"unsub",
26-
"untampered"
30+
"untampered",
31+
"userns"
2732
]
2833
}

docs/usage.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ A comprehensive guide to PatternFly MCP Server tools, resources, and configurati
66
- [Built-in tools](#built-in-tools)
77
- [Built-in resources](#built-in-resources)
88
- [MCP client configuration](#mcp-client-configuration)
9+
- [Running via container](#running-via-container-podman--docker)
910
- [Custom MCP tool plugins](#custom-mcp-tool-plugins)
1011
- [Experimental settings](./experimental.md)
1112
- [Troubleshooting](#troubleshooting)
@@ -200,6 +201,122 @@ Depending on your environment, you may have to delay updating to the minimum Nod
200201
}
201202
```
202203

204+
### Running via container (podman)
205+
206+
The server can also be launched from a container image instead of `npx`. This is useful when you want a pinned, sandboxed runtime that doesn't depend on the host's Node.js installation. Running with an MCP client simply spawns `podman` (or `docker`) instead of `npx`.
207+
208+
> **Prerequisites:**
209+
> You can use Podman or Docker to run the container.
210+
>
211+
> We recommend using Podman Desktop for convenience:
212+
> - [Podman Desktop](https://podman-desktop.io/downloads)
213+
> - [Podman](https://podman.io/)
214+
215+
#### Build the image locally
216+
217+
From the repository root:
218+
219+
```bash
220+
bash ./scripts/container/build.sh
221+
```
222+
223+
or using Node.js and NPM:
224+
225+
```
226+
npm run build:container
227+
```
228+
229+
This produces the following images:
230+
- `localhost/patternfly-mcp:<version>`,
231+
- `localhost/patternfly-mcp:<version>-node24`,
232+
- `localhost/patternfly-mcp:sha-<short>`
233+
- `localhost/patternfly-mcp:latest`.
234+
235+
You can confirm by running `$ podman images` from the terminal. View the [Containerfile](../Containerfile) for the image definition.
236+
237+
#### Running your local image, MCP client configuration
238+
239+
```json
240+
{
241+
"mcpServers": {
242+
"patternfly-mcp": {
243+
"command": "podman",
244+
"args": [
245+
"run", "--rm", "-i",
246+
"--userns=keep-id",
247+
"--security-opt=no-new-privileges",
248+
"--cap-drop=ALL",
249+
"localhost/patternfly-mcp:latest",
250+
"--log-stderr"
251+
],
252+
"description": "PatternFly rules and documentation (local container)"
253+
}
254+
}
255+
}
256+
```
257+
258+
> **Important**:
259+
> - `-i` (interactive stdin) is **required** for stdio MCP. Do **not** pass `-t`. Anything appended after the image name is forwarded verbatim to the CLI, so every flag (`--verbose`, `--http`, `--port`, `--tool`, ...) works without rebuilding.
260+
> - The same configuration should work with `docker`, just replace `"command": "podman"` with `"command": "docker"`.
261+
262+
#### Smoke test
263+
264+
```bash
265+
podman run --rm localhost/patternfly-mcp:latest node -e "console.log(process.versions.node)" # -> 24.x
266+
```
267+
268+
#### Running in HTTP transport mode
269+
270+
The image's `ENTRYPOINT` forwards every argument straight to the CLI, so HTTP mode works against the **same image** with no rebuild — just publish a port and pass `--http --port`.
271+
272+
```bash
273+
podman run --rm \
274+
--userns=keep-id \
275+
--security-opt=no-new-privileges \
276+
--cap-drop=ALL \
277+
--read-only \
278+
--tmpfs /tmp:rw,size=64m \
279+
-p 3030:8080 \
280+
localhost/patternfly-mcp:latest \
281+
--http --port 8080 \
282+
--allowed-origins "http://127.0.0.1:3030" \
283+
--allowed-hosts "127.0.0.1" \
284+
--log-stderr
285+
```
286+
287+
Notes:
288+
- `-p <host>:<container>` publishes the port. The container-side port (`8080` above) must match `--port`. The host-side port is whatever you want clients to connect to.
289+
- `8080` inside the container matches the port the UBI Node.js base already advertises via `EXPOSE`, and a non-root user can bind it without extra capabilities.
290+
- `-i` (interactive stdin) is **not** required in HTTP mode and is omitted here.
291+
- `--allowed-origins` and `--allowed-hosts` are required guardrails when the server is reachable beyond stdio; set them explicitly.
292+
- `--read-only` is safe today because the server writes nothing to the rootfs (only `/tmp`, which is a tmpfs). When the persistent SQLite cache lands, you will need to either drop `--read-only` or mount a writable volume for the cache directory.
293+
- TLS is out of scope for the image itself. For anything beyond `localhost`, terminate TLS at a reverse proxy (Caddy, nginx, an OpenShift route, etc.).
294+
295+
##### MCP client configuration (HTTP)
296+
297+
HTTP MCP clients connect via URL rather than spawning a process. Start the container separately (e.g. with the command above, `podman-compose`, or a systemd unit) and point the client at the published host port:
298+
299+
```json
300+
{
301+
"mcpServers": {
302+
"patternfly-mcp": {
303+
"url": "http://localhost:3030",
304+
"description": "PatternFly rules and documentation (HTTP transport, containerized)"
305+
}
306+
}
307+
}
308+
```
309+
310+
> Stdio-only MCP clients should keep using the [stdio container configuration above](#running-your-local-image-mcp-client-configuration); the HTTP form requires a client that supports HTTP/SSE MCP transports.
311+
312+
##### Verify it's listening
313+
314+
```bash
315+
curl -sS -i http://localhost:3030/ | head -20
316+
```
317+
318+
`--log-stderr` will print the registered routes at startup, which is the easiest way to confirm the server bound the expected port.
319+
203320
## Custom MCP tool plugins
204321

205322
You can extend the server's capabilities by loading custom **Tool Plugins** at startup.

mcp-config-container-example.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"mcpServers": {
3+
"patternfly-mcp": {
4+
"command": "podman",
5+
"args": [
6+
"run", "--rm", "-i",
7+
"--userns=keep-id",
8+
"--security-opt=no-new-privileges",
9+
"--cap-drop=ALL",
10+
"localhost/patternfly-mcp:latest",
11+
"--log-stderr"
12+
],
13+
"description": "PatternFly rules and documentation (local container)"
14+
}
15+
}
16+
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
"build": "npm run build:clean; npm run test:types; pkgroll",
2929
"build:clean": "rm -rf dist",
3030
"build:watch": "npm run build -- --watch",
31+
"container:build": "bash ./scripts/container.build.sh",
32+
"container:start": "bash ./scripts/container.run.sh",
3133
"release": "changelog --non-cc --link-url https://github.com/patternfly/patternfly-mcp.git",
3234
"start": "node dist/cli.js --log-stderr",
3335
"start:dev": "tsx watch src/cli.ts --verbose --log-stderr",

scripts/container.build.sh

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env bash
2+
# Build the PatternFly MCP container image locally from the repository's
3+
# Containerfile. Podman is the primary, supported runtime; Docker is
4+
# detected as a fallback only if Podman is not installed.
5+
#
6+
# Produces four tags under the `${IMAGE}` repository:
7+
# - ${IMAGE}:<semver> (from package.json#version)
8+
# - ${IMAGE}:<semver>-node24 (Node.js 24 base image identifier)
9+
# - ${IMAGE}:sha-<git short> (current HEAD short SHA, or `dev` outside git)
10+
# - ${IMAGE}:latest
11+
#
12+
# Usage:
13+
# ./scripts/container.build.sh
14+
# IMAGE=localhost/patternfly-mcp ./scripts/container.build.sh
15+
#
16+
# Environment:
17+
# IMAGE Image repository (without tag). Defaults to `localhost/patternfly-mcp`.
18+
#
19+
# main()
20+
#
21+
{
22+
# Fail fast on errors, unset variables, and broken pipes.
23+
set -euo pipefail
24+
25+
# Derive tag metadata from package.json and git. `SHA` falls back to `dev`
26+
# when invoked outside a git checkout (e.g. extracted tarball).
27+
VERSION="$(node -p "require('./package.json').version")"
28+
SHA="$(git rev-parse --short HEAD 2>/dev/null || echo dev)"
29+
IMAGE="${IMAGE:-localhost/patternfly-mcp}"
30+
ENGINE=""
31+
32+
# Prefer podman. Docker is supported but intentionally not advertised; if
33+
# neither runtime is available, fail with a clear error.
34+
if [ "$(command -v podman)" ]; then
35+
ENGINE="podman"
36+
elif [ "$(command -v docker)" ]; then
37+
ENGINE="docker"
38+
else
39+
echo 'Error: Podman and Docker not found.' >&2
40+
exit 1
41+
fi
42+
43+
echo "Using $ENGINE...";
44+
45+
# Build with all four tags applied in a single pass so the layer cache is
46+
# shared and the tags are guaranteed to reference the same image digest.
47+
"$ENGINE" build \
48+
--file Containerfile \
49+
--tag "${IMAGE}:${VERSION}" \
50+
--tag "${IMAGE}:${VERSION}-node24" \
51+
--tag "${IMAGE}:sha-${SHA}" \
52+
--tag "${IMAGE}:latest" \
53+
.
54+
55+
# Echo the resulting tag set so callers (and CI logs) can see exactly what
56+
# was produced without re-running `podman images`.
57+
echo
58+
echo "Built tags:"
59+
echo " ${IMAGE}:${VERSION}"
60+
echo " ${IMAGE}:${VERSION}-node24"
61+
echo " ${IMAGE}:sha-${SHA}"
62+
echo " ${IMAGE}:latest"
63+
}

0 commit comments

Comments
 (0)