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
13 changes: 11 additions & 2 deletions .github/workflows/auto-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,16 @@ jobs:
run: |
set -euo pipefail
sed -i -E "s/^var version = \"[^\"]+\"/var version = \"${VERSION}\"/" cmd/bomly/main.go
git diff -- cmd/bomly/main.go

# The npm wrapper and the MCP Registry entry both name a release
# version. The release workflow re-derives them from the tag, so this
# is about keeping the committed files readable and honest.
jq --arg v "${VERSION}" '.version = $v' npm/package.json > npm/package.json.tmp
mv npm/package.json.tmp npm/package.json
jq --arg v "${VERSION}" '.version = $v | .packages[0].version = $v' server.json > server.json.tmp
mv server.json.tmp server.json

git diff -- cmd/bomly/main.go npm/package.json server.json

- name: Commit version bump and tag
shell: bash
Expand All @@ -120,7 +129,7 @@ jobs:
git config user.name "${{ steps.app-token.outputs.app-slug }}[bot]"
git config user.email "${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com"

git add cmd/bomly/main.go
git add cmd/bomly/main.go npm/package.json server.json
git commit -m "chore(release): ${TAG} [skip ci]"
git tag -a "${TAG}" -m "Release ${TAG}"

Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,38 @@ jobs:
with:
args: --offline --include-fragments --no-progress 'docs/**/*.md' README.md CONTRIBUTING.md
fail: true

npm-wrapper:
name: npm wrapper
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Test the bomly-mcp launcher and installer
# No dependencies to install: the package is dependency-free and the
# tests use node:test. BOMLY_MCP_SKIP_DOWNLOAD keeps CI from pulling a
# ~90 MB release archive it does not need.
env:
BOMLY_MCP_SKIP_DOWNLOAD: "1"
working-directory: npm
run: npm test
- name: Check the packed tarball is installable and self-consistent
working-directory: npm
run: |
set -euo pipefail
npm pack --dry-run
node -e '
const pkg = require("./package.json");
const server = require("../server.json");
if (pkg.version !== server.version) {
throw new Error(`npm/package.json ${pkg.version} != server.json ${server.version}`);
}
if (pkg.version !== server.packages[0].version) {
throw new Error(`npm/package.json ${pkg.version} != server.json package ${server.packages[0].version}`);
}
if (pkg.mcpName !== server.name) {
throw new Error(`mcpName ${pkg.mcpName} != server.json name ${server.name}`);
}
'
173 changes: 173 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,176 @@ jobs:
> /dev/null

gh release edit "$RELEASE_TAG" --draft=false --repo bomly-dev/bomly-cli

publish-npm:
# Publishes the `bomly-mcp` npm wrapper, which is the package the official
# MCP Registry entry points at.
#
# Authentication is npm Trusted Publishing (OIDC) — there is deliberately
# no npm token here, and none should ever be added. npm's own token screen
# warns against long-lived automation tokens for exactly this case.
#
# Must run after `publish`: the wrapper's postinstall step downloads the
# release archive and its checksum file from this tag, and draft-release
# assets are not publicly downloadable — so publishing npm any earlier
# ships a package that cannot install.
#
# Split from `publish-mcp-registry` so a failure in either half can be
# re-run on its own. npm versions are immutable, so this job also treats
# "this exact version is already published" as success rather than as an
# error that would block the registry step behind it.
#
# Inert until Ahmed turns it on. Setup is one-time and documented in
# dev-docs/CI.md; the short version:
# 1. publish bomly-mcp once by hand (trusted publishing cannot create a
# package that does not exist yet)
# 2. set the trusted publisher at
# npmjs.com/package/bomly-mcp/access -> this repo, workflow release.yml
# 3. gh variable set PUBLISH_MCP_REGISTRY -R bomly-dev/bomly-cli --body true
needs: publish
if: startsWith(github.ref, 'refs/tags/') && vars.PUBLISH_MCP_REGISTRY == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write # mints the OIDC token npm exchanges for publish rights
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false

- name: Upgrade npm for trusted publishing
# Trusted publishing needs npm >= 11.5.1. The runner image bundles an
# older npm with its Node, so pin an explicit floor rather than
# inheriting whatever the image happens to ship this month.
run: |
set -euo pipefail
npm install -g npm@^11.5.1
npm --version

- name: Stamp the release version into npm/package.json
# The tag is authoritative. Auto Version keeps the committed files in
# sync too, but deriving here means a missed bump cannot publish a
# wrapper that points at a release that does not exist.
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
jq --arg v "$version" '.version = $v' npm/package.json > npm/package.json.tmp
mv npm/package.json.tmp npm/package.json

- name: Publish bomly-mcp to npm
# No `env:` and no npmrc: npm reads the OIDC token from the Actions
# runtime itself. --provenance is passed explicitly even though trusted
# publishing is documented to imply it, because the implicit path has
# been reported to no-op.
env:
RELEASE_TAG: ${{ github.ref_name }}
working-directory: npm
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"

# Idempotent: a re-run after a partial failure must not die here on
# an immutable version that is already live.
if curl -fsSL "https://registry.npmjs.org/bomly-mcp/${version}" > /dev/null 2>&1; then
echo "bomly-mcp@${version} is already published; skipping npm publish."
exit 0
fi

npm publish --provenance --access public

publish-mcp-registry:
# Publishes the server entry in the official MCP Registry
# (https://registry.modelcontextprotocol.io).
#
# Registry auth is GitHub OIDC, so it needs no secret of its own; the
# `io.github.bomly-dev/*` namespace is granted from this repo's identity.
#
# Kept separate from publish-npm so either half can be re-run alone: npm
# versions are immutable, so a combined job that failed here could not be
# retried without tripping over the already-published version. (It was also
# the boundary that kept an npm token away from the downloaded
# mcp-publisher binary; with trusted publishing there is no token left to
# isolate, but the retry argument stands on its own.)
needs: publish-npm
if: startsWith(github.ref, 'refs/tags/') && vars.PUBLISH_MCP_REGISTRY == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write # required for mcp-publisher's OIDC login
env:
# Pinned deliberately. `releases/latest` would download and execute a
# moving target inside a job that holds an OIDC identity able to publish
# under our registry namespace. Bump this together with the digest and
# the signer identity below, after reading the release diff.
MCP_PUBLISHER_VERSION: v1.8.0
MCP_PUBLISHER_SHA256: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Stamp the release version into server.json
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
jq --arg v "$version" '.version = $v | .packages[0].version = $v' server.json > server.json.tmp
mv server.json.tmp server.json

- name: Wait for the npm package to be resolvable
# The registry verifies package ownership by fetching the package, so
# publishing the server entry before npm's CDN has the new version
# fails validation.
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
for attempt in $(seq 1 30); do
if curl -fsSL "https://registry.npmjs.org/bomly-mcp/${version}" > /dev/null 2>&1; then
echo "bomly-mcp@${version} is resolvable."
exit 0
fi
echo "Attempt ${attempt}: bomly-mcp@${version} not visible yet, waiting."
sleep 10
done
echo "::error::bomly-mcp@${version} did not become resolvable on npm within 5 minutes"
exit 1

- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

- name: Download and verify mcp-publisher
# Two independent checks before anything is extracted or executed:
# the pinned SHA-256, and the Sigstore bundle proving the archive was
# built by the registry's own tagged release workflow.
run: |
set -euo pipefail

base="https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}"
archive="mcp-publisher_linux_amd64.tar.gz"

curl -fsSL -o "${archive}" "${base}/${archive}"
curl -fsSL -o "${archive}.sigstore.json" "${base}/${archive}.sigstore.json"

echo "${MCP_PUBLISHER_SHA256} ${archive}" | sha256sum --check --strict

cosign verify-blob "${archive}" \
--bundle "${archive}.sigstore.json" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--certificate-identity "https://github.com/modelcontextprotocol/registry/.github/workflows/release.yml@refs/tags/${MCP_PUBLISHER_VERSION}"

tar xzf "${archive}" mcp-publisher

- name: Publish the server entry to the MCP Registry
run: |
set -euo pipefail
./mcp-publisher login github-oidc
./mcp-publisher publish
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,12 @@ qodana.yaml

# Local scratch notes (not tracked)
/notes

# npm wrapper artifacts (the binary is fetched by its postinstall step)
/npm/vendor
/npm/node_modules
/npm/*.tgz

# Copied from the repo root by npm/scripts/prepack.js at pack time
/npm/LICENSE
/npm/NOTICE
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ Bomly can run as a local MCP server so AI agents can call the same dependency gr
bomly mcp serve
```

If you have not installed the CLI, the `bomly-mcp` npm wrapper starts the same server:

```bash
npx -y bomly-mcp
```

Add Bomly to an MCP-aware agent such as Claude Code, Cursor, VS Code, or a custom tool, and the agent receives structured JSON it can summarize or reason over. See [MCP Server](docs/MCP.md) for setup recipes and the tool reference.

## Configure and Extend
Expand Down
35 changes: 35 additions & 0 deletions dev-docs/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,46 @@ Release packaging is driven by `.goreleaser.yaml`. The release workflow uses GoR
8. The `provenance` job calls `slsa-github-generator` to generate SLSA provenance (`multiple.intoto.jsonl`) as a workflow artifact. It does **not** upload it to the release itself (`upload-assets: false`) — see the caveat below.
9. The `publish` job downloads that provenance artifact, looks up the draft release **by ID** (listing releases and filtering for the matching tag with `draft == true`, not by tag name), uploads the provenance file to it directly via the GitHub REST API, then flips the release from draft to published, using the configured GoReleaser header plus GitHub-native generated release notes.
10. After the release is published, the `Release lifecycle sync` workflow dispatches the landing-page docs and changelog sync with the published timestamp.
11. If the `PUBLISH_MCP_REGISTRY` variable is set, `publish-npm` publishes the `bomly-mcp` wrapper and `publish-mcp-registry` publishes the server entry to the official MCP Registry. See [MCP Registry Publishing](#mcp-registry-publishing).

The manual approval point for a release is the `Auto Version` workflow that creates the release tag. The GitHub Release stays a draft until every asset — including SLSA provenance — is attached, then a final job publishes it. This is required by GitHub's [immutable releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases) feature: once a release is published, no further assets can be added by anyone, so the provenance file (generated by a separate downstream job, by design — see [Supply-Chain Hardening](#supply-chain-hardening-openssf-scorecard)) must land before publish, not after.

**Why the `provenance` job can't upload directly:** GitHub's "get release by tag" API does not return draft releases — they aren't associated with a tag ref until published. The SLSA generator's built-in `upload-assets` option uses `softprops/action-gh-release`, which resolves the target release by tag; against our draft, it finds nothing and **creates a second, non-draft release for the same tag** instead of failing cleanly. That second release immediately and permanently marks the tag as immutable, even if you delete the bad release afterward — there is no way to free the tag back up. (This happened once in production; recovering meant abandoning the tag and cutting a new one.) The `publish` job avoids the whole failure mode by resolving the release by ID itself and never giving any tool a chance to "helpfully" create a duplicate.

## MCP Registry Publishing

Two jobs at the end of `release.yml` publish the `bomly-mcp` npm wrapper and the server entry in the [official MCP Registry](https://registry.modelcontextprotocol.io). Both are gated on the `PUBLISH_MCP_REGISTRY` repository variable and do nothing until it is set.

**There is no npm token, and none should ever be added.** `publish-npm` authenticates with [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers), exchanging the workflow's OIDC identity for publish rights. `publish-mcp-registry` authenticates to the registry the same way, via `mcp-publisher login github-oidc`. Neither needs a secret.

### One-time setup

1. **Publish `bomly-mcp` once by hand.** Trusted publishing is configured per package on npmjs.com, so the package has to exist first — it cannot bootstrap itself. From a checkout at the tag of an already-published release:

```bash
cd npm && npm publish --access public
```

Do this from a maintainer's machine with `npm login`, not from CI. A one-off interactive publish avoids creating an automation token that would then have to be stored and revoked.

The version must match a GitHub release that is already published: the package's postinstall step downloads that release's archive, so publishing ahead of it ships a package that cannot install.

2. **Set the trusted publisher** at `https://www.npmjs.com/package/bomly-mcp/access` → repository `bomly-dev/bomly-cli`, workflow `release.yml`. Configure it on the *package* access page; the account-level packages page does not have this setting.

3. **Turn the jobs on:**

```bash
gh variable set PUBLISH_MCP_REGISTRY -R bomly-dev/bomly-cli --body true
```

### Notes

- **npm version floor.** Trusted publishing needs npm >= 11.5.1, which is newer than the runner image's bundled npm. The job installs `npm@^11.5.1` explicitly rather than inheriting whatever the image ships.
- **`--provenance` is passed explicitly** even though trusted publishing is documented to imply it, because the implicit path has been reported to silently no-op.
- **Two jobs, not one.** npm versions are immutable, so a single job that failed after publishing to npm could not be re-run — it would die on the duplicate version before reaching the registry. `publish-npm` also treats an already-published exact version as success, so a re-run walks through it.
- **`mcp-publisher` is pinned and verified.** The version, its SHA-256, and the expected Sigstore signer identity are `env:` values at the top of `publish-mcp-registry`. The binary is checked against both the pinned digest and `cosign verify-blob` before extraction, because it runs inside a job holding an OIDC identity that can publish under the `io.github.bomly-dev/*` namespace. Bump all three together, after reading the upstream release diff.
- **Version consistency** between `npm/package.json`, `server.json`, and `cmd/bomly/main.go` is maintained by `Auto Version` and asserted by the `npm wrapper` job in CI. The release jobs also re-derive the version from the tag, so a missed bump cannot publish a wrapper pointing at a release that does not exist.

## Yanking Releases

Deleting or unpublishing a GitHub Release automatically starts the yanking path in the `Release lifecycle sync` workflow. The workflow dispatches a landing-page removal event so the yanked version is removed from the version selector and changelog.
Expand Down
18 changes: 18 additions & 0 deletions docs/MCP.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ bomly mcp serve

The command writes its startup banner to stderr and then waits for an MCP client on stdio. It is not meant to be used as an interactive shell command after startup.

## Without Installing The CLI First

If you would rather not install Bomly separately, the [`bomly-mcp`](https://www.npmjs.com/package/bomly-mcp) npm package starts the same server:

```bash
npx -y bomly-mcp
```

Its install step downloads the release archive for your platform from GitHub Releases and checks it against `SHA256SUMS`, the file published with each release that lists the SHA-256 checksum of every archive in it. If the archive's checksum does not match the one listed there, the install fails and nothing is unpacked. Otherwise the binary is unpacked inside the package, together with the `LICENSE`, `NOTICE`, and `licenses/` files the archive carries. Set `BOMLY_MCP_VERSION` to pin a different CLI version.

Anywhere below that a config uses `"command": "bomly"` with `"args": ["mcp", "serve"]`, you can use `"command": "npx"` with `"args": ["-y", "bomly-mcp"]` instead. If `bomly` is already on `PATH`, prefer it: the server starts faster and you update it with your package manager.

## Claude Code

Add Bomly as a local stdio server:
Expand All @@ -35,6 +47,12 @@ Add Bomly as a local stdio server:
claude mcp add --transport stdio bomly -- bomly mcp serve
```

Or, without a separate CLI install:

```bash
claude mcp add --transport stdio bomly -- npx -y bomly-mcp
```

For a project-scoped config you can also commit a `.mcp.json` file:

```json
Expand Down
20 changes: 20 additions & 0 deletions glama.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "https://glama.ai/mcp/schemas/server.json",
"maintainers": [
"bomly-guy"
],
"name": "bomly",
"description": "Give your coding agent the dependency graph it is about to change. Scan a source tree, SBOM, Git ref, or container image; explain why a package is present; diff two graphs; check findings against policy. Vulnerability and license lookups are opt-in per call.",
"keywords": [
"dependencies",
"sbom",
"cyclonedx",
"spdx",
"supply-chain-security",
"vulnerability-scanning",
"sca",
"policy",
"devsecops",
"local-first"
]
}
Loading
Loading