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
9 changes: 1 addition & 8 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,5 @@
"author": {
"name": "5uck1ess"
},
"mcpServers": {
"devkit-engine": {
"command": "${CLAUDE_PLUGIN_ROOT}/bin/devkit",
"args": [
"mcp"
]
}
}
"mcpServers": "./devkit.mcpb"
}
148 changes: 141 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,54 @@ jobs:
exit 1
fi

- name: plugin.json MCP command must match wrapper path
- name: plugin.json mcpServers must point at MCPB bundle
run: |
cmd=$(jq -r '.mcpServers["devkit-engine"].command' .claude-plugin/plugin.json)
expected='${CLAUDE_PLUGIN_ROOT}/bin/devkit'
if [ "$cmd" != "$expected" ]; then
echo "FATAL: plugin.json mcpServers.devkit-engine.command is '$cmd', expected '$expected'"
ref=$(jq -r '.mcpServers' .claude-plugin/plugin.json)
expected='./devkit.mcpb'
if [ "$ref" != "$expected" ]; then
echo "FATAL: plugin.json mcpServers is '$ref', expected '$expected'"
exit 1
fi

- name: shellcheck wrapper
- name: MCPB bundle must exist and contain launcher stubs
run: |
test -f devkit.mcpb || { echo "FATAL: devkit.mcpb bundle missing from repo root"; exit 1; }
entries=$(unzip -Z1 devkit.mcpb)
for required in manifest.json server/devkit server/devkit.exe; do
if ! printf '%s\n' "$entries" | grep -qx "$required"; then
echo "FATAL: devkit.mcpb missing required entry '$required'"
printf 'bundle contents:\n%s\n' "$entries"
exit 1
fi
done
unzip -p devkit.mcpb manifest.json | jq -e \
'.server.mcp_config.platform_overrides.win32.command == "${__dirname}/server/devkit.exe"' \
> /dev/null || {
echo "FATAL: mcpb manifest.json platform_overrides.win32.command is not the expected Windows launcher"
exit 1
}

- name: Bundled server/devkit.exe must be a real PE binary
run: |
magic=$(unzip -p devkit.mcpb server/devkit.exe | head -c 2 | xxd -p)
if [ "$magic" != "4d5a" ]; then
echo "FATAL: server/devkit.exe in bundle is not a PE binary (MZ header missing; got '$magic')"
echo "Did a probe stub or wrong-architecture binary get committed?"
exit 1
fi

- name: Bundled server/devkit must have a shell shebang
run: |
first=$(unzip -p devkit.mcpb server/devkit | head -c 2)
if [ "$first" != "#!" ]; then
echo "FATAL: server/devkit in bundle has no shebang; got '$first'"
exit 1
fi

- name: shellcheck wrappers
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
shellcheck bin/devkit
shellcheck bin/devkit bin/mcpb-build mcpb/server/devkit

- name: Wrapper --help must exit with a download attempt (no local engine)
run: |
Expand All @@ -102,6 +137,105 @@ jobs:
exit 1
fi

mcpb-launcher-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: mcpb/launcher
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: mcpb/launcher/go.mod

- name: Vet
run: go vet ./...

- name: Test
run: go test ./... -v -count=1

- name: Check formatting
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "Files not formatted:"
echo "$unformatted"
exit 1
fi

# Guards against the class of bug where mcpb/launcher/main.go,
# mcpb/manifest.json, or mcpb/server/devkit are edited but devkit.mcpb is
# not rebuilt — CI would otherwise greenlight a shipped bundle with stale
# runtime behavior.
#
# devkit.mcpb.sources.json is a sidecar manifest that records the sha256
# of each source file at bundle-build time. This job re-computes each
# hash and compares. We don't byte-compare the cross-compiled .exe
# because Go cross-builds aren't byte-identical across host OSes even
# with -trimpath (linker build ID leaks host state) — the sidecar is
# the portable equivalent.
mcpb-bundle-integrity:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Sources sidecar must exist
run: |
test -f devkit.mcpb.sources.json || {
echo "FATAL: devkit.mcpb.sources.json is missing; did you rebuild the bundle with bin/mcpb-build?"
exit 1
}

- name: Source file hashes must match sidecar
run: |
set -eo pipefail
stale=0
# Read the tracked file list from the sidecar itself so there's
# one source of truth — bin/mcpb-build defines what's tracked,
# CI re-verifies each entry.
files=$(jq -r 'keys[]' devkit.mcpb.sources.json)
if [ -z "$files" ]; then
echo "FATAL: devkit.mcpb.sources.json is empty or invalid"
exit 1
fi
while IFS= read -r file; do
want=$(jq -r --arg f "$file" '.[$f]' devkit.mcpb.sources.json)
if [ ! -f "$file" ]; then
echo "FATAL: sidecar references $file but the file is missing"
stale=1
continue
fi
have=$(sha256sum "$file" | awk '{print $1}')
if [ "$want" != "$have" ]; then
echo "FATAL: $file has been edited since devkit.mcpb was built"
echo " sidecar sha256: $want"
echo " current sha256: $have"
stale=1
fi
done <<< "$files"
if [ "$stale" != "0" ]; then
echo ""
echo "Rebuild with: bin/mcpb-build"
exit 1
fi

- name: Bundled manifest.json must match source
run: |
unzip -p devkit.mcpb manifest.json > /tmp/bundled-manifest.json
if ! diff -u mcpb/manifest.json /tmp/bundled-manifest.json; then
echo "FATAL: devkit.mcpb manifest.json has drifted from mcpb/manifest.json"
exit 1
fi

- name: Bundled server/devkit must match source
run: |
unzip -p devkit.mcpb server/devkit > /tmp/bundled-proxy.sh
if ! diff -u mcpb/server/devkit /tmp/bundled-proxy.sh; then
echo "FATAL: devkit.mcpb server/devkit has drifted from mcpb/server/devkit"
exit 1
fi

validate-counts:
runs-on: ubuntu-latest
steps:
Expand Down
98 changes: 98 additions & 0 deletions bin/mcpb-build
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# Rebuild devkit.mcpb from mcpb/ sources.
#
# Cross-compiles mcpb/launcher/main.go into mcpb/server/devkit.exe,
# rezips devkit.mcpb, and regenerates devkit.mcpb.sources.json with
# sha256s of every source file the bundle depends on. Dev and CI both
# call this so there's one definition of "bundle rebuild."
#
# Must be run from the repo root. Writes the bundle and sidecar via
# tempfile-then-rename so a failing step can't leave half-written
# artifacts behind.

set -euo pipefail

if [ ! -f mcpb/launcher/main.go ]; then
printf 'mcpb-build: must be run from repo root (mcpb/launcher/main.go not found)\n' >&2
exit 1
fi

# Pick a sha256 frontend. macOS ships shasum, Linux ships sha256sum.
if command -v sha256sum >/dev/null 2>&1; then
HASHER="sha256sum"
elif command -v shasum >/dev/null 2>&1; then
HASHER="shasum -a 256"
else
printf 'mcpb-build: neither sha256sum nor shasum available on PATH\n' >&2
exit 1
fi

sha256_of() {
# Use a real failure path: if the file is unreadable or missing,
# $HASHER prints an error to stderr and exits non-zero, which
# pipefail + errexit will bubble up.
$HASHER "$1" | awk '{print $1}'
}

printf 'mcpb-build: cross-compiling Windows launcher\n'
(
cd mcpb/launcher
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 \
go build -trimpath -ldflags='-s -w' -o ../server/devkit.exe .
)

printf 'mcpb-build: rebuilding devkit.mcpb\n'
rm -f devkit.mcpb.tmp
(
cd mcpb
zip -q -r ../devkit.mcpb.tmp manifest.json server/devkit server/devkit.exe
)

printf 'mcpb-build: regenerating devkit.mcpb.sources.json\n'
rm -f devkit.mcpb.sources.json.tmp
{
printf '{\n'
first=1
for f in \
mcpb/launcher/main.go \
mcpb/launcher/go.mod \
mcpb/launcher/go.sum \
mcpb/launcher/main_test.go \
mcpb/manifest.json \
mcpb/server/devkit \
mcpb/server/devkit.exe
do
if [ ! -f "$f" ]; then
# go.sum is optional when the launcher has no external deps;
# skip silently so an empty-dep module doesn't force a stub
# file. Everything else is required.
if [ "$f" = "mcpb/launcher/go.sum" ]; then
continue
fi
printf 'mcpb-build: FATAL: required source %s is missing\n' "$f" >&2
exit 1
fi
hash=$(sha256_of "$f")
if [ -z "$hash" ]; then
printf 'mcpb-build: FATAL: empty hash for %s\n' "$f" >&2
exit 1
fi
if [ "$first" = "1" ]; then
first=0
else
printf ',\n'
fi
printf ' "%s": "%s"' "$f" "$hash"
done
printf '\n}\n'
} > devkit.mcpb.sources.json.tmp

# Atomic-ish install: both files land together or neither does. The
# tempfiles survive on a failing step above so the prior bundle + sidecar
# stay untouched.
mv devkit.mcpb.tmp devkit.mcpb
mv devkit.mcpb.sources.json.tmp devkit.mcpb.sources.json

printf 'mcpb-build: done\n'
printf ' devkit.mcpb %s bytes\n' "$(wc -c < devkit.mcpb | tr -d ' ')"
printf ' mcpb/server/devkit.exe %s bytes\n' "$(wc -c < mcpb/server/devkit.exe | tr -d ' ')"
Binary file added devkit.mcpb
Binary file not shown.
8 changes: 8 additions & 0 deletions devkit.mcpb.sources.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpb/launcher/main.go": "ad2ce60992bcff323663ac31a63dfae93e782c8556043be8b77ece3b874ade36",
"mcpb/launcher/go.mod": "7f2e8c3f695fe8cbe8cafa7eb9a269e8d0e5141023f2ea20ef5fc7cbd2f22bc2",
"mcpb/launcher/main_test.go": "b723d82d0ce371a6c46eb062a1a312cf6aaaf306ef7edf40f3e35972cc4b153f",
"mcpb/manifest.json": "81d880aee266f821c75afc7578002d562da33a38ce87383ec922f3253eb5f998",
"mcpb/server/devkit": "3833a48a67dfb8d9db7e1513617c0b1df5d7ebb6bf0c3023c60ab048ed63002f",
"mcpb/server/devkit.exe": "6f2f817306d523ddd5d7087277ac933500a0e6a0dd1adc02d7c0a11d27878be3"
}
3 changes: 3 additions & 0 deletions mcpb/launcher/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/5uck1ess/devkit/mcpb/launcher

go 1.26.1
Loading
Loading