#!/bin/sh # Installs the MCP Canary CLI (canary) on macOS and Linux. # # curl -fsSL https://mcpcanary.com/install.sh | sh # # Never asks a question, never needs sudo, never edits shell profiles. # Environment (all optional): # CANARY_VERSION release tag to install, for example v0.1.0 (default: latest) # CANARY_INSTALL_DIR target directory (default: first writable of ~/.local/bin, ~/bin, /usr/local/bin, # /opt/homebrew/bin that is on PATH; else ~/.local/bin) # CANARY_REPO GitHub owner/repo with the releases (default: xakpc-dev-labs/canary) # CANARY_DOWNLOAD_URL base URL that holds the assets and checksums.txt (mirrors, tests) set -eu # Everything is in main, so a partial download of this script never runs half of it. main() { repo="${CANARY_REPO:-xakpc-dev-labs/canary}" version="${CANARY_VERSION:-latest}" os="$(uname -s)" case "$os" in Darwin) os=darwin ;; Linux) os=linux ;; *) fail "unsupported OS '$os'. On Windows run: irm https://mcpcanary.com/install.ps1 | iex" ;; esac arch="$(uname -m)" case "$arch" in x86_64 | amd64) arch=x64 ;; arm64 | aarch64) arch=arm64 ;; *) fail "unsupported CPU '$arch'. Builds exist for x64 and arm64." ;; esac # An x64 shell under Rosetta on Apple Silicon still gets the native arm64 binary. if [ "$os" = darwin ] && [ "$arch" = x64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = 1 ]; then arch=arm64 fi if [ "$os" = linux ] && (ldd --version 2>&1 || true) | grep -qi musl; then fail "musl libc (Alpine) is not supported yet. Use a glibc-based image such as debian:12-slim." fi asset="canary_${os}_${arch}.tar.gz" if [ -n "${CANARY_DOWNLOAD_URL:-}" ]; then base="${CANARY_DOWNLOAD_URL%/}" elif [ "$version" = latest ]; then base="https://github.com/$repo/releases/latest/download" else base="https://github.com/$repo/releases/download/$version" fi dir="$(install_dir)" tmp="$(mktemp -d 2>/dev/null || mktemp -d -t canary)" trap 'rm -rf "$tmp"' EXIT INT TERM download "$base/$asset" "$tmp/$asset" download "$base/checksums.txt" "$tmp/checksums.txt" verify "$tmp" "$asset" tar -xzf "$tmp/$asset" -C "$tmp" mkdir -p "$dir" # Write next to the target, then rename: a running canary is never left half-written. cp "$tmp/canary" "$dir/.canary.new" chmod 755 "$dir/.canary.new" mv -f "$dir/.canary.new" "$dir/canary" installed="$("$dir/canary" --version 2>/dev/null)" || fail "installed $dir/canary, but it does not run on this system." echo "canary $installed installed to $dir/canary" case ":$PATH:" in *":$dir:"*) echo "Next: canary inspect " ;; *) echo "$dir is not on PATH. For this shell run:" echo " export PATH=\"$dir:\$PATH\"" echo "Or call it as $dir/canary" ;; esac } fail() { echo "canary install: $1" >&2 exit 1 } on_path() { case ":$PATH:" in *":$1:"*) return 0 ;; esac return 1 } install_dir() { if [ -n "${CANARY_INSTALL_DIR:-}" ]; then echo "$CANARY_INSTALL_DIR" return fi for candidate in "$HOME/.local/bin" "$HOME/bin" /usr/local/bin /opt/homebrew/bin; do if on_path "$candidate" && [ -d "$candidate" ] && [ -w "$candidate" ]; then echo "$candidate" return fi done echo "$HOME/.local/bin" } download() { if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 3 -o "$2" "$1" || fail "download failed: $1" elif command -v wget >/dev/null 2>&1; then wget -q -O "$2" "$1" || fail "download failed: $1" else fail "curl or wget is required." fi } verify() { expected="$(awk -v f="$2" '$2 == f || $2 == "*" f { print $1 }' "$1/checksums.txt")" [ -n "$expected" ] || fail "checksums.txt has no entry for $2." if command -v sha256sum >/dev/null 2>&1; then actual="$(sha256sum "$1/$2" | awk '{ print $1 }')" elif command -v shasum >/dev/null 2>&1; then actual="$(shasum -a 256 "$1/$2" | awk '{ print $1 }')" else fail "sha256sum or shasum is required to verify the download." fi [ "$expected" = "$actual" ] || fail "checksum mismatch for $2 (expected $expected, got $actual)." } main "$@"