Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .containerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Ignore everything by default
*

# Allowlist only the files and directories required for the build context
!package.json
!package-lock.json
!tsconfig.json
!src/
32 changes: 32 additions & 0 deletions .github/workflows/container.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Container

on:
pull_request:
paths:
- '.containerignore'
- 'Containerfile'
- 'scripts/container.*'
- 'tests/container/**'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:

jobs:
container-audit:
name: Container Audit
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
node-version: '24'
cache: npm
- name: Install dependencies
run: npm ci
- name: Run container audit
run: npm run test:audit-container
# Advisories are non-blocking for PRs
continue-on-error: ${{ github.event_name == 'pull_request' }}
84 changes: 84 additions & 0 deletions Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#
# PatternFly MCP server — container image definition.
#
# Multi-stage build. Produces a minimal Node.js 24 runtime image that
# launches the MCP server as a stdio process; HTTP transport is
# available by passing `--http --port <n>` at `podman run` time (the
# `ENTRYPOINT` forwards all arguments verbatim to the CLI).
#
# Base image: UBI 9 Node.js 24 minimal.
# - Anonymous pull from `registry.access.redhat.com` (no login required).
# - glibc-based, rootless-friendly, sets `APP_ROOT=/opt/app-root` and a
# default non-root user at UID 1001.
#
#
# ---- Stage 1: builder ---------------------------------------------------
# Installs deps, compiles the bundle with pkgroll, and prunes dev deps so
# only the runtime payload moves into stage 2.
FROM registry.access.redhat.com/ubi9/nodejs-24-minimal:latest AS builder

# Non-root build/runtime user. Matches the UBI base default; exposed as an
# ARG so downstream tooling can override without editing the file.
ARG CONTAINER_UID=1001
ENV CONTAINER_UID=${CONTAINER_UID}

# UBI defines `APP_ROOT=/opt/app-root`; reuse it so paths line up with the
# base image's conventions.
WORKDIR ${APP_ROOT}

# Copy only the files needed to resolve deps first.
COPY --chown=${CONTAINER_UID}:0 package.json package-lock.json tsconfig.json ./

USER ${CONTAINER_UID}

# Lockfile-exact install.
RUN npm ci --ignore-scripts --no-audit --no-fund

# Copy the rest of the source and produce the bundle.
COPY --chown=${CONTAINER_UID}:0 src ./src
RUN npx --no-install pkgroll --minify \
&& npm prune --omit=dev --ignore-scripts

# ---- Stage 2: runtime ---------------------------------------------------
# Minimal runtime image. Only the compiled bundle, pruned `node_modules`,
# and `package.json` are carried over from the builder stage.
FROM registry.access.redhat.com/ubi9/nodejs-24-minimal:latest AS runtime

# Standard OCI annotations
LABEL org.opencontainers.image.title="patternfly-mcp" \
org.opencontainers.image.description="PatternFly documentation MCP server (Node.js 24)" \
org.opencontainers.image.source="https://github.com/patternfly/patternfly-mcp" \
org.opencontainers.image.url="https://github.com/patternfly/patternfly-mcp" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.vendor="Red Hat" \
io.modelcontextprotocol.transport="stdio"

# Re-declare the build arg in this stage. ARGs do not cross stages.
ARG CONTAINER_UID=1001
ENV CONTAINER_UID=${CONTAINER_UID}

# Runtime environment defaults:
# - `NODE_ENV=production` enables production code paths in deps
# - `NPM_CONFIG_UPDATE_NOTIFIER` suppress npm self-update nags
# - `NO_COLOR` keep stderr/stdout machine-readable
# (MCP clients consume these streams)
ENV NODE_ENV=production \
NPM_CONFIG_UPDATE_NOTIFIER=false \
NO_COLOR=1

WORKDIR ${APP_ROOT}

# Runtime payload
COPY --from=builder --chown=${CONTAINER_UID}:0 ${APP_ROOT}/package.json ${APP_ROOT}/package.json
COPY --from=builder --chown=${CONTAINER_UID}:0 ${APP_ROOT}/node_modules ${APP_ROOT}/node_modules
COPY --from=builder --chown=${CONTAINER_UID}:0 ${APP_ROOT}/dist ${APP_ROOT}/dist

USER ${CONTAINER_UID}

# stdio MCP server by default. Clients attach over stdin/stdout. `CMD`
# provides only a default flag set; anything passed after the image name
# on `podman run ... <image> <flags>` replaces `CMD` entirely, so every
# CLI option (including `--http --port <n>` for HTTP transport) works
# without rebuilding the image.
ENTRYPOINT ["node", "dist/cli.js"]
CMD ["--log-stderr"]
7 changes: 6 additions & 1 deletion cspell.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"words": [
"amet",
"codemods",
"containerfile",
"deprioritized",
"ized",
"llms",
Expand All @@ -16,13 +17,17 @@
"prefault",
"rereview",
"rescan",
"rootfs",
"rsort",
"sandboxed",
"sandboxing",
"sparkline",
"streamable",
"tmpfs",
"treeify",
"unrepresentable",
"unsub",
"untampered"
"untampered",
"userns"
]
}
122 changes: 122 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ A comprehensive guide to PatternFly MCP Server tools, resources, and configurati
- [Built-in tools](#built-in-tools)
- [Built-in resources](#built-in-resources)
- [MCP client configuration](#mcp-client-configuration)
- [Running via container](#running-via-container-podman)
- [Custom MCP tool plugins](#custom-mcp-tool-plugins)
- [Experimental settings](./experimental.md)
- [Troubleshooting](#troubleshooting)
Expand Down Expand Up @@ -200,6 +201,127 @@ Depending on your environment, you may have to delay updating to the minimum Nod
}
```

### Running via container (podman)

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 spawns `podman` (or `docker`) instead of `npx`.

> **Prerequisites:**
> `podman` is the supported container runtime for PatternFly MCP. We recommend [podman desktop](https://podman-desktop.io/downloads) or [podman](https://podman.io/).
>
> We make a minimal effort to support Docker, and additional configuration may be required.

#### Build the image locally

From the repository root:

```bash
bash ./scripts/container.build.sh
```

or using Node.js and NPM:

```sh
npm run container:build
```

This produces the following images:
- `localhost/patternfly-mcp:<version>`,
- `localhost/patternfly-mcp:<version>-node24`,
- `localhost/patternfly-mcp:sha-<short>`
- `localhost/patternfly-mcp:latest`.

You can confirm by running `$ podman images` from the terminal. View the [Containerfile](../Containerfile) for the image definition.

#### Running your local image, MCP client configuration

```json
{
"mcpServers": {
"patternfly-mcp": {
"command": "podman",
"env": {
"PODMAN_USERNS": "keep-id"
},
"args": [
"run",
"--rm",
"-i",
"--security-opt=no-new-privileges",
"--cap-drop=ALL",
"localhost/patternfly-mcp:latest",
"--log-stderr"
],
"description": "PatternFly rules and documentation (local container)"
}
}
}
```

> **Important**:
> - `-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.
> - If you're attempting to run the same configuration with Docker, you'll need to make at least one adjustment:
> - Replace `"command": "podman"` with `"command": "docker"`.
> - On podman, `PODMAN_USERNS` replaces `--userns=keep-id`. If you come across the `--userns=keep-id` flag, and you are using Docker, remove it.

#### Smoke test

```bash
podman run --rm --entrypoint node localhost/patternfly-mcp:latest -e "console.log(process.versions.node)" # -> 24.x
```

#### Running in HTTP transport mode

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`.

```bash
podman run --rm \
--userns=keep-id \
--security-opt=no-new-privileges \
--cap-drop=ALL \
--read-only \
--tmpfs /tmp:rw,size=64m \
-p 3030:8080 \
localhost/patternfly-mcp:latest \
--http \
--port 8080 \
--allowed-origins "http://127.0.0.1:3030" \
--allowed-hosts "127.0.0.1" \
--log-stderr
```

Notes:
- `-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.
- `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.
- `-i` (interactive stdin) is **not** required in HTTP mode and is omitted here.
- `--allowed-origins` and `--allowed-hosts` are required guardrails when the server is reachable beyond stdio; set them explicitly.
- `--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.
- 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.).

##### MCP client configuration (HTTP)

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:

```json
{
"mcpServers": {
"patternfly-mcp": {
"url": "http://localhost:3030",
"description": "PatternFly rules and documentation (HTTP transport, containerized)"
}
}
}
```

> 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.

##### Verify it's listening

```bash
curl -sS -i http://localhost:3030/ | head -20
```

`--log-stderr` will print the registered routes at startup, which is the easiest way to confirm the server bound the expected port.

## Custom MCP tool plugins

You can extend the server's capabilities by loading custom **Tool Plugins** at startup.
Expand Down
7 changes: 7 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ export default {
roots: ['<rootDir>/tests/audit'],
testMatch: ['<rootDir>/tests/audit/**/*.test.ts'],
...baseConfig
},
{
displayName: 'audit:container',
roots: ['<rootDir>/tests/container'],
testMatch: ['<rootDir>/tests/container/container.audit.test.ts'],
setupFilesAfterEnv: ['<rootDir>/tests/container/jest.setupTests.ts'],
...baseConfig
}
]
};
20 changes: 20 additions & 0 deletions mcp-config-container-example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"mcpServers": {
"patternfly-mcp": {
"command": "podman",
"env": {
"PODMAN_USERNS": "keep-id"
},
"args": [
"run",
"--rm",
"-i",
"--security-opt=no-new-privileges",
"--cap-drop=ALL",
"localhost/patternfly-mcp:latest",
"--log-stderr"
],
"description": "PatternFly rules and documentation (local container)"
}
}
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@
"build": "npm run build:clean; npm run test:types; pkgroll",
"build:clean": "rm -rf dist",
"build:watch": "npm run build -- --watch",
"container:build": "bash ./scripts/container.build.sh",
"container:start": "bash ./scripts/container.run.sh",
"release": "changelog --non-cc --link-url https://github.com/patternfly/patternfly-mcp.git",
"start": "node dist/cli.js --log-stderr",
"start:dev": "tsx watch src/cli.ts --verbose --log-stderr",
"test": "npm run test:spell && npm run test:spell-docs && npm run test:lint && npm run test:types && jest --selectProjects unit",
"test:audit": "jest --selectProjects audit",
"test:audit-container": "npm run container:build && jest --selectProjects audit:container",
"test:ci": "npm test -- --coverage",
"test:dev": "npm test -- --watchAll",
"test:integration": "npm run build && NODE_OPTIONS='--experimental-vm-modules' jest --selectProjects e2e",
Expand Down
Loading
Loading