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
23 changes: 23 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,29 @@ jobs:
- name: List artefacts
run: ls -l dist/

# Envelope-sign the bundles before upload when signing material is
# configured as repo secrets (PEM contents, not paths):
# MCPB_SIGNING_CERT, MCPB_SIGNING_KEY, MCPB_SIGNING_INTERMEDIATES
# 'make sign' regenerates each .sha256 to match the signed bytes, and
# soft-skips with a notice when the secrets are not set.
- name: Sign bundles (optional)
env:
MCPB_SIGNING_CERT: ${{ secrets.MCPB_SIGNING_CERT }}
MCPB_SIGNING_KEY: ${{ secrets.MCPB_SIGNING_KEY }}
MCPB_SIGNING_INTERMEDIATES: ${{ secrets.MCPB_SIGNING_INTERMEDIATES }}
run: |
if [ -n "$MCPB_SIGNING_CERT" ] && [ -n "$MCPB_SIGNING_KEY" ]; then
dir="$(mktemp -d)"
printf '%s\n' "$MCPB_SIGNING_CERT" > "$dir/cert.pem"
printf '%s\n' "$MCPB_SIGNING_KEY" > "$dir/key.pem"
export MCPB_SIGN_CERT="$dir/cert.pem" MCPB_SIGN_KEY="$dir/key.pem"
if [ -n "$MCPB_SIGNING_INTERMEDIATES" ]; then
printf '%s\n' "$MCPB_SIGNING_INTERMEDIATES" > "$dir/intermediates.pem"
export MCPB_SIGN_INTERMEDIATES="$dir/intermediates.pem"
fi
fi
make sign

# 'make publish' uses gh release upload --clobber, so re-running this
# workflow (or overlapping with a manual local publish) is safe.
- name: Upload to release
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Three workflows in [.github/workflows/](.github/workflows/):

- **[build.yml](.github/workflows/build.yml)** - reusable. Builds each bundle with `make one TARGET=<t> VERSION=<v>` on a runner that can execute the embedded binary (`ubuntu-latest`, `ubuntu-24.04-arm`, `macos-latest`), runs `scripts/smoke-test.py` against it, and uploads `dist/` artefacts. The windows-x64 bundle builds on ubuntu (packaging has no Windows dependency) and is smoke-tested on `windows-latest` from the artifact. The Gemini agent check runs on linux-x64 only and soft-skips without `GEMINI_API_KEY`.
- **[ci.yml](.github/workflows/ci.yml)** - on PRs to main and pushes to main. Reads `stackql_release` from [release.yaml](release.yaml) and calls build.yml. Nothing is published.
- **[publish.yml](.github/workflows/publish.yml)** - on pushing a `v*` tag. Fails fast if the tag does not exactly match `stackql_release` in release.yaml, rebuilds and smoke-tests everything, then runs `make publish` to upload all bundles + `.sha256` files to the matching `stackql/stackql` release. Requires the `STACKQL_RELEASE_TOKEN` repo secret (fine-grained PAT with `contents:write` on `stackql/stackql` - the default `GITHUB_TOKEN` cannot upload cross-repo).
- **[publish.yml](.github/workflows/publish.yml)** - on pushing a `v*` tag. Fails fast if the tag does not exactly match `stackql_release` in release.yaml, rebuilds and smoke-tests everything, optionally envelope-signs the bundles (runs `make sign` when the `MCPB_SIGNING_CERT`/`MCPB_SIGNING_KEY` PEM-content secrets are set; soft-skips otherwise), then runs `make publish` to upload all bundles + `.sha256` files to the matching `stackql/stackql` release. Requires the `STACKQL_RELEASE_TOKEN` repo secret (fine-grained PAT with `contents:write` on `stackql/stackql` - the default `GITHUB_TOKEN` cannot upload cross-repo). Signing happens before checksumming: the signature is appended to the bundle bytes, so `make sign` regenerates each `.sha256`. `mcpb verify` is broken in the current upstream CLI (node-forge cannot verify PKCS#7); the scripts treat it as advisory and assert the appended `MCPB_SIG_END` block instead.

The release sequence: upstream `stackql/stackql` release publishes the core assets -> raise a PR here bumping `stackql_release` in release.yaml (CI proves the bundles build and pass smoke tests against the real release assets) -> merge -> push the matching tag (e.g. `v0.10.500`) -> publish.yml attaches the `.mcpb` assets to the upstream release.

Expand Down Expand Up @@ -142,7 +142,7 @@ The end goal is signed, verifiable, functional MCP binary assets distributed thr

1. **Mach-O / Authenticode signatures on the embedded binary** - applied upstream during the stackql release build. Windows: Authenticode-signed `stackql.exe`. macOS: Developer ID Application signature embedded in the universal `stackql` binary inside the `.pkg`, plus Apple notarisation keyed to the binary's cdhash. Linux: no platform-level signing, by convention.
2. **SHA-256 on the bundle envelope** - written by `package.sh` next to every `.mcpb`. Published with the bundle and pinned in the official MCP Registry `server.json`. Anyone installing the bundle can verify the bytes.
3. **MCPB envelope signature (`mcpb sign`)** - currently *not applied*. The hooks are in `package.sh` (`MCPB_SELF_SIGN`, `MCPB_SIGN_CERT`/`MCPB_SIGN_KEY`/`MCPB_SIGN_INTERMEDIATES`) and remain dormant until envelope signing is wired up.
3. **MCPB envelope signature (`mcpb sign`)** - wired but inactive until signing material is configured. `make sign` (scripts/sign.sh) signs `dist/*.mcpb` in place and regenerates checksums; the publish workflow calls it and soft-skips unless the `MCPB_SIGNING_CERT`/`MCPB_SIGNING_KEY` secrets are set. The same `MCPB_SELF_SIGN`/`MCPB_SIGN_CERT`/`MCPB_SIGN_KEY`/`MCPB_SIGN_INTERMEDIATES` hooks remain in `package.sh` for sign-at-build.
4. **Anthropic Desktop Extensions directory listing** - the editorial "vetted by Claude" signal that users see in Claude Desktop's Browse Extensions UI. Submission is via the review form at `claude.com/docs/connectors/building/submission`; requirements (privacy policy, logo, screenshots) are in [listings.md](listings.md).
5. **Official MCP Registry entry** - canonical metadata pointing at the GitHub release assets and pinning their SHA-256.

Expand Down
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ ASSETS := stackql_linux_amd64.zip \
stackql_windows_amd64.zip \
stackql_darwin_multiarch.pkg

.PHONY: all check-version check-target download package one signed publish \
.PHONY: all check-version check-target download package one signed sign publish \
server-json registry-publish clean clean-bin \
linux-x64 linux-arm64 windows-x64 darwin-universal \
list help
Expand All @@ -66,6 +66,9 @@ help:
@echo " fail hard if the bundle is not produced"
@echo " (use on a Mac for darwin-universal)"
@echo " make signed VERSION=X.Y.Z build with MCPB_SELF_SIGN=true"
@echo " make sign envelope-sign dist/*.mcpb in place and"
@echo " regenerate .sha256 (MCPB_SELF_SIGN=true"
@echo " or MCPB_SIGN_CERT + MCPB_SIGN_KEY)"
@echo " make publish VERSION=X.Y.Z upload dist/* to the stackql/stackql"
@echo " release matching v<VERSION>"
@echo " make server-json VERSION=X.Y.Z render registry/server.json (pins"
Expand Down Expand Up @@ -136,6 +139,15 @@ package: check-version
signed: check-version
MCPB_SELF_SIGN=true $(PACKAGE) --version $(VERSION)

# Envelope-sign whatever is already in dist/ and regenerate the .sha256
# files (the signature is appended to the bundle, so checksums must be
# recomputed). Same env contract as package.sh: MCPB_SELF_SIGN=true, or
# MCPB_SIGN_CERT + MCPB_SIGN_KEY (+ optional MCPB_SIGN_INTERMEDIATES).
# No-ops with a notice when no signing material is configured, so CI can
# call it unconditionally before 'make publish'.
sign:
bash scripts/sign.sh

# Render registry/server.json from the template, pinning the four per-platform
# SHA-256s read from dist/*.sha256. Fails hard if any sha file is missing -
# run this on a machine that has gathered all four bundles (typically the
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ The sequence:

One-time setup: add a repo secret `STACKQL_RELEASE_TOKEN` - a fine-grained PAT (or GitHub App token) with `contents:write` on `stackql/stackql`. The default `GITHUB_TOKEN` cannot upload assets to another repo. Optionally add `GEMINI_API_KEY` to enable the agent smoke test on the linux-x64 job; without it that step soft-skips.

Optional envelope signing: if the repo secrets `MCPB_SIGNING_CERT` and `MCPB_SIGNING_KEY` (PEM contents, plus optional `MCPB_SIGNING_INTERMEDIATES`) are set, the publish job runs `make sign` to `mcpb sign` every bundle and regenerate its `.sha256` before upload. Without the secrets the step prints a notice and skips, and unsigned bundles ship as before. Note `mcpb verify` in the current CLI is broken upstream (node-forge cannot verify PKCS#7, so every signed bundle reports as unsigned); `make sign` treats it as advisory and asserts the appended signature block instead.

Steps 3 and 4 of the local runbook below (MCP Registry publish and aggregator listings) are still manual after a CI publish.

## Release runbook (local fallback)
Expand Down Expand Up @@ -274,6 +276,9 @@ make <target> VERSION=X.Y.Z build a single target from current bin/ state
make one TARGET=<t> VERSION=X.Y.Z download just one target's artefact and build
that one bundle (use on a Mac for darwin)
make signed VERSION=X.Y.Z build with MCPB_SELF_SIGN=true (testing only)
make sign envelope-sign dist/*.mcpb in place and regenerate
.sha256 (MCPB_SELF_SIGN=true or MCPB_SIGN_CERT +
MCPB_SIGN_KEY; no-ops with a notice when unset)
make publish VERSION=X.Y.Z upload dist/* to the stackql/stackql release
make server-json VERSION=X.Y.Z render registry/server.json (pins 4 SHAs)
make registry-publish VERSION=X.Y.Z render + publish to the Official MCP Registry
Expand Down
21 changes: 19 additions & 2 deletions scripts/package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,29 @@ sha_file() {
)
}

verify_bundle() {
# 'mcpb verify' is currently broken upstream (the CLI calls node-forge's
# p7.verify, which is not implemented, so every signed bundle reports as
# unsigned). Treat its result as advisory and assert the appended
# signature block directly.
local f="$1"
if mcpb verify "$f"; then
return 0
fi
if tail -c 64 "$f" | grep -aq "MCPB_SIG_END"; then
echo " warn: 'mcpb verify' failed but the signature block is present (known upstream CLI bug)"
return 0
fi
echo " error: no signature block found after signing $(basename "$f")" >&2
return 1
}

sign_bundle() {
local f="$1"
if [ "${MCPB_SELF_SIGN:-false}" = "true" ]; then
echo " signing bundle (self-signed): $(basename "$f")"
mcpb sign "$f" --self-signed
mcpb verify "$f" || true
verify_bundle "$f"
elif [ -n "${MCPB_SIGN_CERT:-}" ] && [ -n "${MCPB_SIGN_KEY:-}" ]; then
echo " signing bundle (production cert): $(basename "$f")"
if [ -n "${MCPB_SIGN_INTERMEDIATES:-}" ]; then
Expand All @@ -81,7 +98,7 @@ sign_bundle() {
else
mcpb sign "$f" --cert "$MCPB_SIGN_CERT" --key "$MCPB_SIGN_KEY"
fi
mcpb verify "$f"
verify_bundle "$f"
else
echo " bundle signing skipped (set MCPB_SELF_SIGN=true or MCPB_SIGN_CERT + MCPB_SIGN_KEY)"
fi
Expand Down
89 changes: 89 additions & 0 deletions scripts/sign.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
#
# sign.sh - envelope-sign existing dist/*.mcpb bundles and regenerate their
# .sha256 companions (the signature is appended to the bundle bytes, so the
# checksum must be recomputed after signing).
#
# Same env contract as package.sh:
# self-signed (testing):
# MCPB_SELF_SIGN=true scripts/sign.sh
# production cert:
# MCPB_SIGN_CERT=cert.pem MCPB_SIGN_KEY=key.pem \
# [MCPB_SIGN_INTERMEDIATES="intermediate-ca.pem root-ca.pem"] \
# scripts/sign.sh
#
# With neither configured, prints a notice and exits 0 so CI can call it
# unconditionally as a publish step.
#
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DIST_DIR="${DIST_DIR:-$ROOT_DIR/dist}"

if command -v mcpb >/dev/null 2>&1; then
mcpb() { command mcpb "$@"; }
else
mcpb() { npx --yes @anthropic-ai/mcpb "$@"; }
fi

sha_file() {
# Write "<hash> <basename>" so the checksum matches the released filename.
local f="$1" dir base
dir="$(dirname "$f")"; base="$(basename "$f")"
( cd "$dir"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$base" > "${base}.sha256"
else
shasum -a 256 "$base" > "${base}.sha256"
fi
cat "${base}.sha256"
)
}

verify_bundle() {
# 'mcpb verify' is currently broken upstream (the CLI calls node-forge's
# p7.verify, which is not implemented, so every signed bundle reports as
# unsigned). Treat its result as advisory and assert the appended
# signature block directly.
local f="$1"
if mcpb verify "$f"; then
return 0
fi
if tail -c 64 "$f" | grep -aq "MCPB_SIG_END"; then
echo " warn: 'mcpb verify' failed but the signature block is present (known upstream CLI bug)"
return 0
fi
echo " error: no signature block found after signing $(basename "$f")" >&2
return 1
}

if [ "${MCPB_SELF_SIGN:-false}" != "true" ] && { [ -z "${MCPB_SIGN_CERT:-}" ] || [ -z "${MCPB_SIGN_KEY:-}" ]; }; then
echo "bundle signing skipped (set MCPB_SELF_SIGN=true or MCPB_SIGN_CERT + MCPB_SIGN_KEY)"
exit 0
fi

shopt -s nullglob
bundles=( "$DIST_DIR"/stackql-mcp-*.mcpb )
if [ ${#bundles[@]} -eq 0 ]; then
echo "error: no bundles found in $DIST_DIR" >&2
exit 1
fi

for f in "${bundles[@]}"; do
if [ "${MCPB_SELF_SIGN:-false}" = "true" ]; then
echo "==> signing (self-signed): $(basename "$f")"
mcpb sign "$f" --self-signed
else
echo "==> signing (production cert): $(basename "$f")"
if [ -n "${MCPB_SIGN_INTERMEDIATES:-}" ]; then
# shellcheck disable=SC2086
mcpb sign "$f" --cert "$MCPB_SIGN_CERT" --key "$MCPB_SIGN_KEY" --intermediate $MCPB_SIGN_INTERMEDIATES
else
mcpb sign "$f" --cert "$MCPB_SIGN_CERT" --key "$MCPB_SIGN_KEY"
fi
fi
verify_bundle "$f"
sha_file "$f"
done

echo "signed ${#bundles[@]} bundle(s) and regenerated checksums."
Loading