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
35 changes: 35 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
Expand All @@ -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:
Expand Down
79 changes: 56 additions & 23 deletions mcp/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | — |

Expand All @@ -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)

Expand Down Expand Up @@ -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.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
52 changes: 52 additions & 0 deletions scripts/sync-mcp-version.mjs
Original file line number Diff line number Diff line change
@@ -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}.`,
);