diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..5f7a290f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,38 @@ +# Keep the build context small and force a clean, in-container install/build. +# Host node_modules and build outputs are excluded so nothing stale or +# wrong-architecture is copied in — the builder stage runs its own +# `npm install && npm pack`. + +.git +.github + +# Installed deps (root + every client) — reinstalled in the image +node_modules +clients/*/node_modules +test-servers/node_modules + +# Build outputs — rebuilt in the image +clients/*/build +clients/web/dist +test-servers/build +storybook-static +coverage + +# Docs/specs aren't referenced by the build — trim them from the context. +# (test-servers/ stays: `tsc -b` typechecks the integration tests that import +# the `@modelcontextprotocol/inspector-test-server` alias.) +specification +docs + +# Packed tarballs (the image makes its own) +*.tgz + +# Local / editor / env +.env +.env.* +.DS_Store +.vscode +.idea +.claude +.playwright-mcp +*.log diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 74c16615..fbdd32a0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -146,3 +146,65 @@ jobs: run: npm publish --access public --provenance env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + # Build and push the multi-arch container image to GHCR on a published + # release. The image installs the packed tarball (`Dockerfile`) so it ships + # the same artifact as npm. Independent of the npm `publish` job (both gated on + # the release event) — a container failure doesn't block the npm publish. + publish-github-container-registry: + runs-on: ubuntu-latest + if: github.event_name == 'release' + environment: release + needs: build + permissions: + contents: read + packages: write + attestations: write + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Log in to the Container registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v6 + with: + images: ghcr.io/${{ github.repository }} + # Be explicit rather than relying on `flavor.latest=auto`: on a release + # cut both the version tags and `latest` land, so the README's bare + # `ghcr.io/…/inspector` (implicit `:latest`) never 404s. + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + flavor: | + latest=true + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v4 + with: + subject-name: ghcr.io/${{ github.repository }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..0651b7d2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,51 @@ +# syntax=docker/dockerfile:1 + +# --------------------------------------------------------------------------- +# Build stage — install everything, build all clients, and pack the exact +# tarball npm would publish. `npm install` runs the root postinstall cascade +# (per-client installs, since v2 is not a workspace); `npm pack` runs `prepack` +# (`npm run build`) and emits `modelcontextprotocol-inspector-.tgz`. +# --------------------------------------------------------------------------- +FROM node:22-slim AS builder +WORKDIR /build +COPY . . +RUN npm install && npm pack + +# --------------------------------------------------------------------------- +# Runtime stage — install just the packed tarball (production deps only), +# yielding a clean global `mcp-inspector` bin: the same artifact a user gets +# from `npm i -g @modelcontextprotocol/inspector`. The package's postinstall +# early-exits under node_modules, and the tarball ships only each client's +# build/ output, so there is nothing to build here. +# --------------------------------------------------------------------------- +FROM node:22-slim AS runner +ENV NODE_ENV=production + +COPY --from=builder /build/modelcontextprotocol-inspector-*.tgz /tmp/inspector.tgz +RUN npm install -g /tmp/inspector.tgz && rm /tmp/inspector.tgz + +# Serve on all interfaces so the UI is reachable from outside the container, and +# never try to open a browser from inside it. +ENV HOST=0.0.0.0 \ + CLIENT_PORT=6274 \ + MCP_AUTO_OPEN_ENABLED=false +EXPOSE 6274 + +# Run as the non-root `node` user the base image ships. The inspector resolves +# its runtime-state dir (default catalog, OAuth token storage) from `HOME` +# (core/storage/store-io.ts), so set it explicitly to the node user's writable +# home and work from there. +ENV HOME=/home/node +USER node +WORKDIR /home/node + +# Report readiness by probing the served SPA (`/` needs no auth). Uses Node's +# global fetch — no curl/wget in the slim image. Assumes the default `--web` +# mode; running `--cli`/`--tui` has no web server, so add `--no-healthcheck` to +# `docker run` for those. +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:'+(process.env.CLIENT_PORT||6274)+'/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +# Default to the web UI; override the args to run --cli / --tui. +ENTRYPOINT ["mcp-inspector"] +CMD ["--web"] diff --git a/README.md b/README.md index 500a5641..1cc32ed6 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,10 @@ The `smoke:*` scripts run against the in-repo build tree, which is **not** the p ### Cutting a release -Publishing is automated by the `publish` job in [`.github/workflows/main.yml`](.github/workflows/main.yml), gated on a **published GitHub release** (`github.event_name == 'release'`). On release it runs `npm run pack:verify` as the pre-publish gate, then `npm publish --access public --provenance` — a single `npm publish` (v2 is not an npm workspace, so there is no v1-style `publish-all`/`--workspaces`), with a signed provenance attestation via GitHub OIDC (`id-token: write`, `environment: release`, `NPM_TOKEN`). +Publishing is automated by two release-gated jobs in [`.github/workflows/main.yml`](.github/workflows/main.yml) (`github.event_name == 'release'`, both `needs: build`): + +- **`publish`** — the npm package. Runs `npm run pack:verify` as the pre-publish gate, asserts the release tag matches the root `package.json` version, then `npm publish --access public --provenance` — a single `npm publish` (v2 is not an npm workspace, so there is no v1-style `publish-all`/`--workspaces`), with a signed provenance attestation via GitHub OIDC (`id-token: write`, `environment: release`, `NPM_TOKEN`). +- **`publish-github-container-registry`** — the container image (see [Docker](#docker)). Because there is **one version number** (only the root `package.json` has one — the clients carry none, so there is nothing to keep in sync and no `check-version` step), the release flow is just: @@ -162,7 +165,20 @@ git push --follow-tags The release's target commit selects which workflow runs, so this only publishes when a release is cut from a commit carrying this (v2) workflow. -> **Docker / GHCR is not wired up yet.** v1 published a container image to GHCR, but v2 has no Dockerfile (its `client/server/cli` layout is gone). A v2 image + GHCR job is tracked separately in [#1646](https://github.com/modelcontextprotocol/inspector/issues/1646). +### Docker + +A container image is published to GHCR (`ghcr.io/modelcontextprotocol/inspector`, `linux/amd64` + `linux/arm64`) by the release workflow. The [`Dockerfile`](Dockerfile) is a two-stage build: the first stage installs and `npm pack`s the publishable tarball; the second stage `npm install -g`s that tarball, so the image ships the exact same artifact as npm, with a clean `mcp-inspector` bin. + +```bash +# run the web UI (reads the auth token from the container logs) +docker run --rm -p 6274:6274 ghcr.io/modelcontextprotocol/inspector + +# or build the image locally +docker build -t mcp-inspector . +docker run --rm -p 6274:6274 mcp-inspector +``` + +The image defaults to `--web` bound to `0.0.0.0:6274` with browser auto-open disabled; override the args to run another mode (`docker run --rm ghcr.io/modelcontextprotocol/inspector --cli …`). Pass `-e MCP_INSPECTOR_API_TOKEN=…` to set a known token (otherwise one is generated and printed in the logs), or `-e DANGEROUSLY_OMIT_AUTH=true` to disable auth. The image runs as the non-root `node` user and has a `HEALTHCHECK` that probes the web UI — it assumes the default `--web` mode, so add `--no-healthcheck` when running `--cli`/`--tui` (which have no web server). ## Contributing — `AGENTS.md` and `CLAUDE.md`