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
94 changes: 86 additions & 8 deletions .github/workflows/container.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
name: Container

on:
release:
types: [ published ]
pull_request:
paths:
- '.containerignore'
Expand All @@ -9,25 +11,101 @@ on:
- 'tests/container/**'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
push:
branches: [ "main" ]
paths:
- '.containerignore'
- 'Containerfile'
- 'scripts/container.*'
- 'tests/container/**'
- 'package.json'
- 'package-lock.json'

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
container-audit:
name: Container Audit
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- name: Checkout repository
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7.0.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' }}

- name: Build local image for verification
uses: redhat-actions/buildah-build@v3
with:
image: localhost/patternfly-mcp
tags: latest
containerfiles: ./Containerfile

- name: Audit Container
run: npm run container:test

publish:
name: Container Image Publish
runs-on: ubuntu-latest
timeout-minutes: 25
needs: [ container-audit ]
# Explicitly locks down execution to stable GitHub release publications only
if: |
github.repository == 'patternfly/patternfly-mcp' &&
github.event_name == 'release' &&
github.event.action == 'published' &&
!github.event.release.prerelease &&
!contains(github.event.release.tag_name, '-')
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Log in to registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# Generates 'X.Y.Z' and 'latest' from your Git Release Tag natively
- name: Setup OCI metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{major}}.{{minor}}.{{patch}}
type=raw,value=latest

- name: Setup QEMU for multi-arch
uses: docker/setup-qemu-action@v4
with:
platforms: 'linux/amd64,linux/arm64'

- name: Build multi-arch image
id: build-image
uses: redhat-actions/buildah-build@v3
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: ${{ steps.meta.outputs.tags }}
archs: amd64, arm64
containerfiles: ./Containerfile

- name: Push image to registry
uses: redhat-actions/push-to-registry@v3
with:
image: ${{ steps.build-image.outputs.image }}
tags: ${{ steps.meta.outputs.tags }}
registry: ${{ env.REGISTRY }}
1 change: 1 addition & 0 deletions cspell.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"nosniff",
"onsessioninitialized",
"onsessionclosed",
"pfmcp",
"patternfly",
"prefault",
"pypy",
Expand Down
7 changes: 4 additions & 3 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,18 +499,19 @@ Start on a **fork** of the repository, then:
6. A git release is then created mirroring previous git releases.
- Use tags that are prefixed with `v`, (e.g. `v1.0.0`, `v1.2.1`, etc.)
- Do not "auto-generate" the release notes, prefer linking to the [CHANGELOG.md](../CHANGELOG.md), this prevents note drift.
7. After a release is created and tagged, `CI` will run and publish the related NPM package.
7. Once the GitHub Release is published, CI will automatically publish the npm package to the npm registry and the multi-arch container image to GitHub Container Registry (`ghcr.io/patternfly/patternfly-mcp`).
8. Perform the [Post-release review](#post-release-review).

### Post-release review

After each **npm release**, run a maintainer pass in addition to **CI** (integration, scheduled jobs in `audit.yml` / `container.yml`, publish workflow). CI is the automated gate; this is a smoke test to confirm everything is in working order.
After each release, run a maintainer pass in addition to **CI** (integration checks in `integration.yml`, release publishing in `publishing.yml` and `container.yml`). CI is the automated gate; this is a smoke test to confirm everything is in working order.

**When:** Each GitHub Release. Ad hoc when major features are introduced, collections, allowlists, or experimental flags change materially.

#### Default checklist

- [ ] Version on npm matches tag and [CHANGELOG.md](../CHANGELOG.md).
- [ ] Version on npm and GHCR matches [CHANGELOG.md](../CHANGELOG.md) and corresponds to the Git tag (omitting the `v` prefix).
- [ ] Confirm the `:latest` GHCR image tag is updated and resolves to the new release.
- [ ] Collections/sources: any change to `docs.json`, API seed/crawl, schema pin, or ai-handbook wiring?
- [ ] Built-in tools are still read-only; experimental surface matches [experimental.md](./experimental.md).
- [ ] Confirm allowlist / HTTP docs are still accurate (`src/options.defaults.ts`, [usage.md](./usage.md)).
Expand Down
46 changes: 42 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,43 @@ 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`.
The server can also be launched from a container image instead of `npx`. This is useful when you want a containerized, 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.

#### Use the hosted image

Update your MCP configuration to use the hosted container image, that's it!

> We make allowances for `amd64` and `arm64` architectures which should cover most use cases across Linux and macOS. In the event you need access to a different architecture, you can either build your own image from source (see below) or open an issue requesting the new architecture.

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


#### Build the image locally

From the repository root:
Expand Down Expand Up @@ -277,8 +307,16 @@ You can confirm by running `$ podman images` from the terminal. View the [Contai

#### Smoke test

Confirm the container image is available

```bash
podman run --rm --entrypoint node ghcr.io/patternfly/patternfly-mcp:latest -e "console.log(process.versions.node)"
```

Or confirm a locally built container image is available

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

#### Running in HTTP transport mode
Expand All @@ -293,7 +331,7 @@ podman run --rm \
--read-only \
--tmpfs /tmp:rw,size=64m \
-p 3030:8080 \
localhost/patternfly-mcp:latest \
ghcr.io/patternfly/patternfly-mcp:latest \
--http \
--port 8080 \
--allowed-origins "http://127.0.0.1:3030" \
Expand Down Expand Up @@ -324,7 +362,7 @@ HTTP MCP clients connect via URL rather than spawning a process. Start the conta
}
```

> 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.
> Stdio-only MCP clients should keep using the [stdio container configuration above](#use-the-hosted-image); the HTTP form requires a client that supports HTTP/SSE MCP transports.

##### Verify it's listening

Expand Down
4 changes: 2 additions & 2 deletions mcp-config-container-example.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
"-i",
"--security-opt=no-new-privileges",
"--cap-drop=ALL",
"localhost/patternfly-mcp:latest",
"ghcr.io/patternfly/patternfly-mcp:latest",
"--log-stderr"
],
"description": "PatternFly rules and documentation (local container)"
"description": "PatternFly rules and documentation"
}
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@
"build:collections-docs": "UPDATE_COLLECTIONS=true tsx ./tooling/collections/update.patternFlyDocs.ts",
"container:build": "bash ./scripts/container.build.sh",
"container:start": "bash ./scripts/container.run.sh",
"container:test": "jest --selectProjects audit:container",
"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": "NODE_OPTIONS='--experimental-vm-modules' jest --selectProjects audit",
"test:audit-container": "npm run container:build && jest --selectProjects audit:container",
"test:audit-container": "npm run container:build && npm run container:test",
"test:ci": "npm test -- --coverage",
"test:collections": "jest --selectProjects tooling:collections",
"test:collections-dev": "npm run test:collections -- --watchAll",
Expand Down
4 changes: 2 additions & 2 deletions tests/container/container.audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ describeSkip(engine !== undefined)('Container Audit', () => {
CLIENT = await startContainer({
engine
});
});
}, 60_000);

afterAll(async () => {
if (CLIENT) {
await CLIENT.stop();
}
});
}, 30_000);

it('should start and have basic tools and resources', async () => {
const tools = await CLIENT.send({ method: 'tools/list' });
Expand Down
6 changes: 5 additions & 1 deletion tests/container/utils/containerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ const buildImage = async (engine: 'podman' | 'docker', image: string) => {
try {
execSync(`${engine} image inspect ${image}`, { stdio: 'ignore' });
} catch {
console.warn(`Image ${image} not found. Building...`);
if (process.env.CI) {
throw new Error(`[CI] Pre-built container image "${image}" not found.`);
}

console.warn(`Image ${image} not found. Building locally...`);
execSync('npm run container:build', { stdio: 'inherit' });
}
};
Expand Down
Loading