Skip to content

2.10.0

2.10.0 #18

name: Publish to MCP Registry
# Run on three triggers, in order of preference for routine releases:
# 1. Tag push matching v* — fires when `git push --follow-tags` lands a
# version tag. This is the canonical release event in this org; pairs
# with `npm version <bump> && npm publish && git push --follow-tags`.
# 2. GitHub Release published — kept for any UI-driven release path.
# 3. Manual workflow_dispatch with optional version input. Catch-up
# path when neither (1) nor (2) fired. Default version is read from
# package.json on main, so the dispatch needs no input for the
# common "publish whatever main says" case.
#
# Both the MCP Registry publish and the GitHub Release creation steps are
# idempotent — re-running the workflow on a tag whose version is already
# isLatest on the Registry, or for which a Release already exists, is a
# no-op for that surface. This lets backfills (and the dual-trigger case
# where one tag push fires both push:tags and release:published) succeed
# without duplicate-publish errors.
#
# The diagnosis at docs/audit/distribution-cascade-2026-05-16.md in the
# main freighttools repo documents why this trigger set exists, and the
# 2026-05-17 amendment in the same doc covers the Release-gap discovery
# that drove this version of the workflow.
on:
push:
tags: ['v*']
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Package version to publish (optional — defaults to package.json on main)'
required: false
type: string
permissions:
contents: write # to commit the updated server.json back to main
id-token: write # required for GitHub OIDC authentication to the MCP Registry
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve target version
id: version
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
REF_TYPE: ${{ github.ref_type }}
run: |
# Resolution order:
# 1. Manual dispatch input (highest priority — explicit override)
# 2. Tag ref (push:tags or release:published both set ref_type=tag)
# 3. package.json on main (catch-up dispatch with no input)
if [ -n "$INPUT_VERSION" ]; then
VERSION="$INPUT_VERSION"
SOURCE="workflow_dispatch input"
elif [ "$REF_TYPE" = "tag" ]; then
VERSION="${GITHUB_REF_NAME#v}"
SOURCE="tag $GITHUB_REF_NAME"
else
VERSION=$(jq -r '.version' package.json)
SOURCE="package.json"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Target version: $VERSION (from $SOURCE)"
- name: Sanity-check that npm has this version
run: |
VERSION="${{ steps.version.outputs.version }}"
NPM_VERSIONS=$(npm view freightutils-mcp versions --json)
if ! echo "$NPM_VERSIONS" | jq -e --arg v "$VERSION" 'index($v)' > /dev/null; then
echo "::error::Version $VERSION is not published to npm yet. Publish to npm first, then run this workflow."
exit 1
fi
echo "Confirmed: freightutils-mcp@$VERSION exists on npm."
- name: Update server.json version fields
run: |
VERSION="${{ steps.version.outputs.version }}"
# Update top-level version
jq --arg v "$VERSION" '.version = $v' server.json > server.json.tmp
# Update packages[0].version too
jq --arg v "$VERSION" '.packages[0].version = $v' server.json.tmp > server.json
rm server.json.tmp
echo "Updated server.json:"
cat server.json
- name: Install mcp-publisher
run: |
VERSION="1.7.6"
curl -L -o mcp-publisher.tar.gz \
"https://github.com/modelcontextprotocol/registry/releases/download/v${VERSION}/mcp-publisher_linux_amd64.tar.gz"
tar -xzf mcp-publisher.tar.gz
chmod +x mcp-publisher
./mcp-publisher --version
- name: Validate server.json
run: ./mcp-publisher validate ./server.json
- name: Check if version is already on MCP Registry
id: registry_check
run: |
VERSION="${{ steps.version.outputs.version }}"
RESULT=$(curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=freightutils")
# Skip publish if this version appears in the Registry history,
# in any status. mcp-publisher errors on duplicate-version
# publish, so this guard makes the workflow safe to re-run on
# backfill tag pushes (v2.0.0, v1.1.0, v1.0.5, v1.0.4) where
# the version was published long ago via a manual path.
ALREADY=$(echo "$RESULT" | jq -r --arg v "$VERSION" '.servers[] | select(.server.version == $v) | .server.version' | head -1)
LATEST=$(echo "$RESULT" | jq -r '.servers[] | select(._meta["io.modelcontextprotocol.registry/official"].isLatest == true) | .server.version')
if [ -n "$ALREADY" ]; then
echo "Registry already has version $VERSION (current isLatest=$LATEST) — skipping publish step."
echo "already_published=true" >> "$GITHUB_OUTPUT"
else
echo "Registry does not have $VERSION (current isLatest=$LATEST) — publish step will run."
echo "already_published=false" >> "$GITHUB_OUTPUT"
fi
- name: Authenticate via GitHub OIDC
if: steps.registry_check.outputs.already_published != 'true'
run: ./mcp-publisher login github-oidc
- name: Publish to MCP Registry
if: steps.registry_check.outputs.already_published != 'true'
run: ./mcp-publisher publish
- name: Commit updated server.json back to main
if: steps.registry_check.outputs.already_published != 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git diff --quiet server.json; then
echo "server.json unchanged, nothing to commit."
else
git add server.json
git commit -m "chore(registry): sync server.json to v${{ steps.version.outputs.version }} [skip ci]"
git push origin main
fi
- name: Verify registry entry
run: |
sleep 5
RESULT=$(curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=freightutils")
LATEST_VERSION=$(echo "$RESULT" | jq -r '.servers[] | select(._meta["io.modelcontextprotocol.registry/official"].isLatest == true) | .server.version')
EXPECTED="${{ steps.version.outputs.version }}"
if [ "$LATEST_VERSION" = "$EXPECTED" ]; then
echo "✅ Registry confirms isLatest=$EXPECTED"
else
echo "::warning::Registry shows isLatest=$LATEST_VERSION but expected $EXPECTED. May be a replication delay."
fi
- name: Cut GitHub Release if missing
# Glama reads the GitHub Releases API, not git tags or the MCP
# Registry. Without a Release per version, Glama's "Latest
# release" / tool-count cache / maintenance grade all stay
# stale. This step creates the Release if one does not already
# exist for the current tag. Notes are pulled from CHANGELOG.md
# (entry between `## X.Y.Z` and the next `## ` heading); if no
# entry exists, falls back to a pointer line. Idempotent —
# safe to re-run on existing-Release tags.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.version.outputs.version }}"
TAG="v$VERSION"
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
echo "GitHub Release $TAG already exists — skipping."
exit 0
fi
# Extract CHANGELOG entry for this version. The CHANGELOG.md
# heading format is `## X.Y.Z — YYYY-MM-DD` (em-dash, em-dash
# then date may be present). Match `## VERSION` at start of
# line, capture lines until the next `## ` heading.
NOTES_FILE=$(mktemp)
awk -v v="$VERSION" '
$0 ~ "^## "v"([^0-9]|$)" { capturing=1; next }
/^## [0-9]/ && capturing { exit }
capturing { print }
' CHANGELOG.md > "$NOTES_FILE"
# Trim leading/trailing blank lines.
sed -i -e '/./,$!d' -e ':a' -e '/^\n*$/{$d;N;ba' -e '}' "$NOTES_FILE"
if [ ! -s "$NOTES_FILE" ]; then
echo "No CHANGELOG.md entry found for $VERSION — using pointer note." >&2
echo "See [CHANGELOG.md](./CHANGELOG.md) for details and the [npm package page](https://www.npmjs.com/package/freightutils-mcp/v/$VERSION) for the release artefact." > "$NOTES_FILE"
fi
# If this tag is the current MCP Registry isLatest, mark as
# the GitHub latest too. Otherwise create as a historical
# Release (--latest=false).
LATEST_FLAG="--latest=false"
REGISTRY_LATEST=$(curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=freightutils" | jq -r '.servers[] | select(._meta["io.modelcontextprotocol.registry/official"].isLatest == true) | .server.version')
if [ "$REGISTRY_LATEST" = "$VERSION" ]; then
LATEST_FLAG="--latest=true"
fi
gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--title "$TAG" \
--notes-file "$NOTES_FILE" \
$LATEST_FLAG
echo "Created GitHub Release $TAG ($LATEST_FLAG)"