From a2a86c972e0d22a7ac067c3f03aebc142507f9ee Mon Sep 17 00:00:00 2001 From: DanMat Date: Tue, 21 Jul 2026 12:13:38 -0400 Subject: [PATCH] feat(release): publish packkit-mcp everywhere from CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release workflow published packkit-mcp to npm but stopped there, so server.json, the official MCP registry entry and the version the server reports over MCP all had to be updated by hand — and quietly drifted. - scripts/sync-mcp-version.mjs rewrites server.json + mcp/server.js from mcp/package.json, and fails loudly if mcpName and server.json's name disagree (that mismatch cost us a burnt npm version). - Publish to the official registry via mcp-publisher's GitHub OIDC login, so no new secrets. Gated on the npm publish actually happening, since the registry verifies ownership by fetching the npm package. - Refresh mcp/package-lock.json against the just-published create-packkit. npx users get the newest core via the caret, but `npm ci` builds — Glama's — install what the lockfile pins, which is the real staleness. Only Glama's Build & Release stays manual; its API is read-only. Documented the whole chain, and its gotchas, in mcp/RELEASING.md. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 35 ++++++++++++++++ mcp/RELEASING.md | 79 +++++++++++++++++++++++++---------- package.json | 3 +- scripts/sync-mcp-version.mjs | 52 +++++++++++++++++++++++ 4 files changed, 145 insertions(+), 24 deletions(-) create mode 100644 scripts/sync-mcp-version.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3e3b3b6..5281ef4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,6 +53,12 @@ jobs: echo "create-packkit major unchanged ($CUR_MAJOR); packkit-mcp's caret already tracks the new release." fi + # server.json (the official-registry manifest) and the version the server + # reports over MCP must track mcp/package.json, or the registry entry and + # the running server drift from the published npm package. + - name: Sync server.json + mcp/server.js to packkit-mcp's version + run: node scripts/sync-mcp-version.mjs + - name: Commit and tag run: | git config user.name "github-actions[bot]" @@ -68,17 +74,46 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Publish packkit-mcp (only when its version is new) + id: mcp working-directory: mcp run: | V=$(node -p "require('./package.json').version") if [ "$(npm view packkit-mcp@$V version 2>/dev/null)" = "$V" ]; then echo "packkit-mcp@$V already published — skipping" + echo "published=false" >> "$GITHUB_OUTPUT" else npm publish --provenance --access public + echo "published=true" >> "$GITHUB_OUTPUT" fi env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + # Clients (Glama, Cursor, VS Code…) resolve install commands from the + # official registry, so publish the server entry too. OIDC needs no + # secrets; the registry verifies npm ownership via mcpName, so this must + # run AFTER the npm publish above. + - name: Publish to the official MCP registry + if: steps.mcp.outputs.published == 'true' + run: | + curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher + ./mcp-publisher login github-oidc + ./mcp-publisher publish + + # `npx packkit-mcp` resolves the caret to the newest create-packkit, but + # builds that run `npm ci` (Glama's, notably) install whatever this + # lockfile pins — so refresh it now that the new version is on npm, or the + # hosted server keeps scaffolding with an older core. + - name: Refresh mcp lockfile against the just-published create-packkit + run: | + npm --prefix mcp install --package-lock-only --ignore-scripts + if git diff --quiet -- mcp/package-lock.json; then + echo "mcp lockfile already current" + else + git add mcp/package-lock.json + git commit -m "chore(mcp): refresh lockfile for create-packkit v${{ steps.bump.outputs.version }}" + git push + fi + - name: GitHub Release run: gh release create "v${{ steps.bump.outputs.version }}" --generate-notes env: diff --git a/mcp/RELEASING.md b/mcp/RELEASING.md index 00044ca..8282eb6 100644 --- a/mcp/RELEASING.md +++ b/mcp/RELEASING.md @@ -7,9 +7,9 @@ because several of these steps fail in non-obvious ways (see [Gotchas](#gotchas) | Surface | Identifier | Updated by | | --- | --- | --- | -| **npm** | `packkit-mcp` | `release.yml` (automatic) or `npm publish` | -| **Official MCP registry** | `io.github.DanMat/packkit-mcp` | `mcp-publisher publish` (manual) | -| **Glama** | [`DanMat/create-packkit`](https://glama.ai/mcp/servers/DanMat/create-packkit) | Admin → Dockerfile → **Build & Release** (manual) | +| **npm** | `packkit-mcp` | `release.yml` — automatic | +| **Official MCP registry** | `io.github.DanMat/packkit-mcp` | `release.yml` — automatic | +| **Glama** | [`DanMat/create-packkit`](https://glama.ai/mcp/servers/DanMat/create-packkit) | Admin → Dockerfile → **Build & Release** — **manual** | | **mcpservers.org** | submitted once | — | | **awesome-mcp-servers** | README entry + Glama score badge | — | @@ -25,38 +25,68 @@ registry**, so that one matters most. | `server.json` | `version`, `packages[0].version` | `mcp/package.json` → `version` | | `mcp/server.js` | `new Server({ version })` | `mcp/package.json` → `version` | +`npm run sync:mcp` enforces this — it rewrites `server.json` and `mcp/server.js` +from `mcp/package.json`, and exits non-zero if `mcpName` and `server.json`'s +`name` disagree. The Release workflow runs it on every release. + ## What the Release workflow does -`Actions → Release` (workflow_dispatch, pick a bump) handles the **npm** side: +`Actions → Release` (workflow_dispatch, pick a bump) now handles everything +except Glama: 1. Bumps + publishes `create-packkit`. 2. Re-pins `packkit-mcp`'s `create-packkit` dependency and patch-bumps it **only - when the create-packkit MAJOR changes** (the caret already tracks minors/patches). -3. Publishes `packkit-mcp` **only if that version isn't on npm yet**. -4. Tags and creates the GitHub Release. - -**It does not touch `server.json`, the official registry, or Glama.** So whenever -`mcp/package.json`'s version changes, do the follow-ups below or those listings -go stale. - -## Releasing a new `packkit-mcp` version + when the create-packkit MAJOR changes** — see [why](#why-packkit-mcp-isnt-bumped-every-release). +3. Runs `scripts/sync-mcp-version.mjs` so `server.json` and `mcp/server.js` track + `mcp/package.json`'s version (and fails the release if `mcpName` and + `server.json`'s `name` ever drift apart). +4. Publishes `packkit-mcp` to npm **only if that version isn't there yet**. +5. Publishes the entry to the **official MCP registry** via GitHub OIDC + (`mcp-publisher login github-oidc` — no secrets), gated on step 4 actually + publishing, because the registry verifies npm ownership. +6. **Refreshes `mcp/package-lock.json`** against the just-published + `create-packkit` and pushes it. +7. Tags and creates the GitHub Release. + +**Only Glama stays manual** — its build/release is an admin-panel action with no +public write API (their API is read-only). + +### Why `packkit-mcp` isn't bumped every release + +`packkit-mcp` depends on `create-packkit` by caret (`^X.y.z`), so `npx -y +packkit-mcp` already resolves the newest matching version at install time. +Republishing it for every create-packkit patch would be version churn with no +user-visible effect. + +What *does* go stale is `mcp/package-lock.json`: builds that run `npm ci` — +**Glama's, notably** — install the exact version it pins, not the newest. (The +lockfile isn't in the npm tarball, so it only affects git-clone builds.) That's +why step 6 refreshes it every release instead of bumping the package version. + +## Releasing by hand + +Only needed for an out-of-band `packkit-mcp` change (the workflow covers the +normal path): ```bash -# 1. Bump, keeping all four values in sync (see the table above) -# mcp/package.json version · server.json version + packages[0].version · mcp/server.js -# 2. Publish to npm (skip if the Release workflow already did it) +# 1. Bump mcp/package.json, then sync the other two version sites +npm run sync:mcp + +# 2. Publish to npm — the registry verifies against it, so this comes first cd mcp && npm publish && cd .. -# 3. Push it to the official MCP registry (run from the repo root — reads ./server.json) -mcp-publisher login github # once per machine/session; browser device-code flow +# 3. Push the entry to the official registry (run from the repo root; reads ./server.json) +mcp-publisher login github # once per machine; browser device-code flow mcp-publisher publish # 4. Verify curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=packkit" | python3 -m json.tool ``` -Then refresh **Glama**: Admin → **Dockerfile** → **Build** (test) → **Build & Release**. -Without a new release, Glama keeps serving the previous build. +Either way, finish by refreshing **Glama**: Admin → **Dockerfile** → **Build** +(test) → **Build & Release**. Without a new release Glama keeps serving the +previous build — and since it clones the latest `main`, it picks up the +refreshed lockfile at the same time. ### Glama build config (known-good) @@ -99,7 +129,10 @@ and answers an introspection request — the tool schemas appear in *Instance lo Everything else (name, description, build) is configured in Glama's admin panel after claiming. -## Possible improvement +## Not automated -`release.yml` could sync `server.json` and `mcp/server.js` whenever it bumps -`mcp/package.json`, so only the registry publish and Glama rebuild stay manual. +**Glama's build & release.** Everything else (npm, the official registry, the +version sync, the lockfile refresh) runs in `release.yml`. Glama exposes only a +read API, so after a release open Admin → Dockerfile → **Build & Release**. +Its listing metadata still refreshes on its own, since Glama syncs from the +official registry. diff --git a/package.json b/package.json index 4269ee5..02b2708 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "integration": "node scripts/integration.mjs", "build:web": "esbuild src/core/index.js --bundle --format=esm --outfile=docs/packkit-core.js", "update:node": "node scripts/update-node-versions.mjs", - "gen:reference": "node scripts/gen-reference.mjs" + "gen:reference": "node scripts/gen-reference.mjs", + "sync:mcp": "node scripts/sync-mcp-version.mjs" }, "repository": { "type": "git", diff --git a/scripts/sync-mcp-version.mjs b/scripts/sync-mcp-version.mjs new file mode 100644 index 0000000..2bd709d --- /dev/null +++ b/scripts/sync-mcp-version.mjs @@ -0,0 +1,52 @@ +#!/usr/bin/env node +// Keep the MCP server's version consistent across the three places it appears: +// +// mcp/package.json → the published npm version (source of truth) +// server.json → the official-registry entry + the npm version it points at +// mcp/server.js → the version the server advertises in its MCP handshake +// +// Run after anything changes mcp/package.json's version. The release workflow +// does this automatically; `npm run sync:mcp` does it by hand. +import { readFileSync, writeFileSync } from 'node:fs'; + +const pkg = JSON.parse(readFileSync('mcp/package.json', 'utf8')); +const version = pkg.version; + +// The registry verifies npm-package ownership by matching this field against +// server.json's name, so a mismatch fails the publish — catch it here instead. +const server = JSON.parse(readFileSync('server.json', 'utf8')); +if (pkg.mcpName !== server.name) { + console.error( + `mcpName mismatch: mcp/package.json "${pkg.mcpName}" !== server.json "${server.name}".\n` + + 'The MCP registry uses this to verify you own the npm package; they must match exactly (case-sensitive).', + ); + process.exit(1); +} + +const changed = []; + +server.version = version; +if (Array.isArray(server.packages) && server.packages[0]) { + server.packages[0].version = version; +} +const serverJson = JSON.stringify(server, null, 2) + '\n'; +if (serverJson !== readFileSync('server.json', 'utf8')) { + writeFileSync('server.json', serverJson); + changed.push('server.json'); +} + +const entry = 'mcp/server.js'; +const src = readFileSync(entry, 'utf8'); +const next = src.replace(/(name: 'packkit', version: ')[^']*(')/, `$1${version}$2`); +if (next === src && !src.includes(`version: '${version}'`)) { + console.error(`Could not find the Server({ name: 'packkit', version: '…' }) call in ${entry}.`); + process.exit(1); +} +if (next !== src) { + writeFileSync(entry, next); + changed.push(entry); +} + +console.log( + changed.length ? `Synced to ${version}: ${changed.join(', ')}` : `Already in sync at ${version}.`, +);