fix: do not unmap IPv4-mapped IPv6 in explicit ALLOWED_ORIGINS (round… #5616
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| # A published GitHub release triggers the `publish` job below. The release's | |
| # target commit determines which workflow runs — so this only publishes when a | |
| # release is cut from a commit that carries this (v2) workflow. | |
| release: | |
| types: [published] | |
| # Default least-privilege scope for GITHUB_TOKEN. Without this, jobs inherit the | |
| # repository's default token permissions, which are broader than any job here | |
| # needs (CodeQL `actions/missing-workflow-permissions`). The `publish` and | |
| # `publish-github-container-registry` jobs declare their own blocks below, which | |
| # override this one entirely rather than adding to it — so each publish job must | |
| # continue to list every scope it needs, including `contents: read`. | |
| permissions: | |
| contents: read | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| cache: 'npm' | |
| - name: Install dependencies (root + all clients) | |
| # The root postinstall (scripts/install-clients.mjs) cascades | |
| # `npm install` into clients/web, clients/cli, clients/tui, and | |
| # clients/launcher, so this single step sets up every client. | |
| run: npm install | |
| - name: Validate (coverage guards, format, lint, typecheck, build, fast tests) | |
| # Runs the four durable guards first (verify:format-coverage, | |
| # verify:skills, verify:typecheck-coverage, verify:dep-lockstep), then | |
| # test:scripts, then validate:core, then each client's | |
| # self-validation: format:check + lint + typecheck + build + test (no | |
| # coverage instrumentation — fast). This also builds every client bundle | |
| # (web dist, cli/tui/launcher) that the smokes below need. The heavier | |
| # per-file coverage gate runs in the parallel `coverage` job below | |
| # (#2159), which consumes nothing this job produces — every client's | |
| # `test:coverage` builds whatever it needs itself. Unit tests run in | |
| # both jobs (fast here, instrumented there); now that the two run in | |
| # parallel that duplication costs no wall clock at all. | |
| # A future optimization could split `coverage` into per-client parallel | |
| # jobs, but that's a larger restructure and deliberately out of scope. | |
| run: npm run validate | |
| - name: Validate the skills with the authoritative CLI (#2163) | |
| # `npm run validate` above already ran `verify:skills`, whose own parser | |
| # reads each SKILL.md the way Claude Code does. This is the second | |
| # opinion: `claude plugin validate` is the authoritative schema, and the | |
| # guard skips it whenever the CLI is absent — which, without this step, | |
| # would mean always, in CI. Installing it here is what makes the | |
| # acceptance criterion true rather than aspirational. | |
| # | |
| # The script resolves the CLI itself — an installed one ONLY when it | |
| # matches the pin exactly, otherwise the pinned package via `npx -y` — | |
| # so this is the same command `npm run local:gate` runs, and the two | |
| # cannot drift. Exact rather than a floor: a newer local CLI is a | |
| # DIFFERENT schema, which is how a "reproducible" gate starts disagreeing | |
| # across machines. Pinned | |
| # rather than @latest: an unpinned validator can start failing a PR that | |
| # changed nothing. It needs no authentication — verified with a clean HOME. | |
| run: npm run verify:skills:cli | |
| - name: Verify the browser-externalized-builtin build gate (#1769) | |
| # Runs a real `vite build` with a Node built-in forced into the browser | |
| # graph and asserts the build FAILS via the #1769 gate. The unit tests | |
| # cover the detection logic against a captured message; only this real | |
| # build catches the risk the issue calls out — that the Vite warning | |
| # phrasing drifts across releases, silently disabling the message-keyed | |
| # gate. It restores the mutated entry afterward (see the script). | |
| run: npm run verify:build-gate | |
| - name: Verify no externalized dependency was inlined (#2067) | |
| # The mirror of the "must be bundled" rule. `undici` was declared only in | |
| # the root and `clients/cli` manifests, so tsup — which auto-externalizes | |
| # what the NEAREST manifest declares — inlined 1.05MB of it into the web | |
| # and TUI bundles. Inlined CommonJS in an ESM bundle throws | |
| # `Dynamic require of "assert" is not supported` on first use, and the | |
| # rewritten relative specifier meant no user-side install could fix it. | |
| # This reads the built output rather than the config, because those two | |
| # disagreed for four releases. | |
| run: npm run verify:bundle-externals | |
| # Playwright chromium is installed BEFORE the smokes because | |
| # `smoke:web:browser` (the headless-browser boot smoke, #1615) drives the | |
| # prod web bundle in chromium — restoring/installing it here lets that | |
| # smoke reuse the cache instead of downloading its own copy. The Storybook | |
| # step below reuses the same install. | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-${{ runner.os }}-${{ hashFiles('clients/web/package-lock.json') }} | |
| - name: Install Playwright browsers | |
| working-directory: ./clients/web | |
| run: npx playwright install --with-deps chromium | |
| - name: Run cross-client smokes | |
| # NOTE: this workflow runs GitHub CI's tier only. The LOCAL pre-push | |
| # gate is `npm run local:gate`, and it is a strict superset — do not add | |
| # it, a `local:*` script, `smoke:web:firefox`/`smoke:web:webkit`, | |
| # `smoke:web:engine`, or a non-Chromium `SMOKE_BROWSER` to any workflow. | |
| # `npm run test:scripts` fails if you do (scripts/lib/workflow-gate.mjs, | |
| # #2146). `npm run smoke` and `smoke:web:chromium` belong here and are | |
| # not affected. The canonical CI-vs-local table is in the root README. | |
| # | |
| # End-to-end smokes through the built launcher (--help dispatch + prod | |
| # CLI/web). Not part of any client's `validate`: it needs the | |
| # cli/tui/launcher bundles, which `validate` above already built | |
| # (smoke:web builds clients/web/dist on demand — #1486). smoke:web:browser | |
| # boots the prod web bundle in headless chromium (#1615); smoke:web:app | |
| # goes further and drives connect → open app → widget ready against a | |
| # composable MCP App server (#1859). Both reuse the chromium installed | |
| # above. smoke:tui self-skips here — the Ink TUI needs a real TTY (raw | |
| # mode) that headless CI lacks, so its boot/render check is local-only. | |
| run: npm run smoke | |
| - name: Run Storybook play-function tests | |
| working-directory: ./clients/web | |
| run: npm run test:storybook | |
| # The per-file coverage gate, in its own job so it runs in PARALLEL with | |
| # `build` rather than serially after it (#2159). The two together were 84% of | |
| # a ~17m wall clock; split, the workflow finishes in roughly the length of | |
| # `build` alone. | |
| # | |
| # This is safe because `coverage` consumes nothing `build` produces: every | |
| # client's `test:coverage` is self-sufficient (web and cli build the test | |
| # servers — and cli its own bin — themselves; tui and launcher run from | |
| # source). The only consumers of `clients/*/build` are the smokes, which stay | |
| # in `build` alongside the `validate` that produces them. | |
| # | |
| # Do NOT "optimize" this back into one job by backgrounding the two commands: | |
| # that puts two vitest fleets on one 4-core runner, which is already known to | |
| # time tests out at the 5s default. Separate jobs get separate runners. | |
| coverage: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| cache: 'npm' | |
| - name: Install dependencies (root + all clients) | |
| run: npm install | |
| - name: Enforce per-file coverage gate (≥90% on all four dimensions) | |
| # CI-ENFORCED coverage gate (#1550): runs `npm run coverage`, which | |
| # chains every client's `test:coverage` (v8-instrumented) and fails the | |
| # job if ANY file drops below 90% on lines, statements, functions, or | |
| # branches. This is the whole point of the job — a PR that regresses | |
| # coverage below the threshold blocks merge instead of relying on a | |
| # contributor remembering to run the gate locally. | |
| # | |
| # This also covers the web integration project: web's `test:coverage` | |
| # runs `--project=unit --project=integration --coverage`, so there is no | |
| # separate integration step anywhere in this workflow. | |
| run: npm run coverage | |
| # Publish the single `@modelcontextprotocol/inspector` package to npm on a | |
| # published GitHub release. v2 is not an npm workspace, so there is no | |
| # `publish-all` / `--workspaces` (v1) — just one `npm publish`, whose `prepack` | |
| # (`npm run build`) builds every client bundle into the `files` allowlist. | |
| # `pack:verify` runs first as the pre-publish gate: it builds, packs the real | |
| # tarball, installs it into a clean throwaway consumer, and drives the | |
| # installed `mcp-inspector` bin (web/cli/tui) end to end — so a broken package | |
| # is caught before it reaches npm rather than after. | |
| publish: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'release' | |
| environment: release | |
| needs: [build, coverage] | |
| # Serialize publishes so two releases cut in quick succession can't run | |
| # overlapping `npm publish`es. Never cancel an in-flight publish. | |
| concurrency: | |
| group: publish-npm | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| # Required for npm provenance (`--provenance` mints a signed attestation | |
| # via GitHub's OIDC token). The repo is public, so provenance is available. | |
| id-token: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| cache: 'npm' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Assert release tag matches package version | |
| # `npm publish` ships whatever `version` is in the root package.json, | |
| # regardless of the release's tag. Fail fast (before the heavy install / | |
| # pack:verify) if they disagree — e.g. a release drafted without running | |
| # `npm version`, or cut from the wrong commit — since publishing is | |
| # irreversible. Tolerates the conventional leading `v` (npm version tags | |
| # as `vX.Y.Z`). The tag arrives via `env:` (not spliced into the script) | |
| # to avoid the script-injection surface of interpolating `${{ }}` into a | |
| # `run:` block. | |
| env: | |
| TAG: ${{ github.event.release.tag_name }} | |
| run: | | |
| PKG="$(node -p "require('./package.json').version")" | |
| if [ "${TAG#v}" != "$PKG" ]; then | |
| echo "Release tag '$TAG' does not match package.json version '$PKG'" | |
| exit 1 | |
| fi | |
| # OIDC trusted publishing requires npm >= 11.5.1; Node 22's bundled npm is | |
| # 10.x, which fails with ENEEDAUTH before OIDC is ever attempted. | |
| - name: Ensure npm CLI supports OIDC trusted publishing | |
| run: npm install -g npm@^11.5.1 | |
| - name: Install dependencies (root + all clients) | |
| run: npm install | |
| - name: Verify the publishable tarball end to end | |
| # Builds, `npm pack`s, installs the tarball into a clean consumer, and | |
| # drives the installed bin. Needs registry access to pull the tarball's | |
| # runtime deps — available here. `smoke:tui` inside it self-skips on CI. | |
| run: npm run pack:verify | |
| - name: Publish to npm (single package, with provenance) | |
| # `prepack` (`npm run build`) rebuilds the client bundles into the tarball. | |
| # The build runs three times on this path (build job → pack:verify → | |
| # prepack); the redundancy is intentional — each is a clean-tree rebuild | |
| # and the `prepack` one is what actually populates the published tarball, | |
| # so don't "optimize" it away. | |
| # | |
| # The dist-tag is derived from the version, and passing it explicitly is | |
| # NOT optional: `npm publish` defaults to `--tag latest` regardless of | |
| # semver prerelease status, so publishing `2.0.0-rc.1` without this would | |
| # point every `npx @modelcontextprotocol/inspector` at a release | |
| # candidate. A prerelease is a hyphen after the patch component | |
| # (`2.0.0-rc.1`); build metadata uses `+` and is not a prerelease. Done | |
| # in shell rather than with `semver` because that package is only a | |
| # transitive dependency here and must not be relied on in CI. | |
| # | |
| # There is deliberately NO `NODE_AUTH_TOKEN` here. Publishing uses npm | |
| # OIDC trusted publishing (`id-token: write` + `environment: release`), | |
| # which needs no token — and the repo has no `NPM_TOKEN` secret. Setting | |
| # it from a non-existent secret writes an EMPTY `_authToken` into the | |
| # `.npmrc` that `setup-node` generates, and npm then fails `ENEEDAUTH` | |
| # before OIDC is ever attempted. Do not "restore" it. | |
| run: | | |
| VERSION="$(node -p "require('./package.json').version")" | |
| case "$VERSION" in | |
| *-*) NPM_TAG=next ;; | |
| *) NPM_TAG=latest ;; | |
| esac | |
| echo "Publishing $VERSION under dist-tag '$NPM_TAG'" | |
| npm publish --access public --provenance --tag "$NPM_TAG" | |
| # 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, coverage] | |
| permissions: | |
| contents: read | |
| packages: write | |
| attestations: write | |
| # Lets `attest-build-provenance` write the artifact metadata storage | |
| # record alongside the provenance itself (#2228). See the long comment on | |
| # the "Generate artifact attestation" step for why this is separate from | |
| # `attestations: write` and what to check after the next release. | |
| artifact-metadata: write | |
| id-token: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - 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@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Build and push Docker image | |
| id: push | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| push: true | |
| platforms: linux/amd64,linux/arm64 | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| - name: Generate artifact attestation | |
| # Two scopes, two different things, and only the first is the | |
| # attestation (#2228). `attestations: write` persists the signed SLSA | |
| # provenance — that half has always worked, and | |
| # `gh attestation verify oci://ghcr.io/modelcontextprotocol/inspector:<version> | |
| # --repo modelcontextprotocol/inspector` passed on 2.5.0 without the | |
| # second scope. `artifact-metadata: write` persists the separate | |
| # *storage record*: GitHub's org-level index of where a published | |
| # artifact lives (registry, active/eol status), surfaced at | |
| # https://github.com/orgs/modelcontextprotocol/artifacts. The action | |
| # emits one automatically when `push-to-registry` is true AND the | |
| # workflow carries this scope; with only the first condition met, every | |
| # release logged two warning annotations on an otherwise-green job: | |
| # | |
| # Failed to create storage record: Error: Failed to persist storage | |
| # record: no artifacts found | |
| # Please check that the "artifact-metadata:write" permission has been | |
| # included | |
| # | |
| # `no artifacts found` reads like it is about this job uploading no | |
| # *workflow* artifact, and is not: it is the generic 404 body of the org | |
| # artifact-metadata API that @actions/attest POSTs to. The same message | |
| # comes back from the sibling read endpoint for 2.5.0's real digest, and | |
| # from an all-zeros digest that cannot exist — so it carries no | |
| # information beyond "nothing resolved", and the documented precondition | |
| # we were failing is this permission. | |
| # | |
| # ⚠️ Confirm at the next release rather than assuming: the job should be | |
| # annotation-free, and | |
| # `gh api /orgs/modelcontextprotocol/artifacts/<digest>/metadata/storage-records` | |
| # should return a record instead of 404. If the warning persists, the | |
| # honest fix is `create-storage-record: false` plus a note here saying | |
| # the record is unavailable to us — not carrying an unexplained warning. | |
| uses: actions/attest-build-provenance@v4 | |
| with: | |
| subject-name: ghcr.io/${{ github.repository }} | |
| subject-digest: ${{ steps.push.outputs.digest }} | |
| push-to-registry: true |