From 2975125b5a43e65724e41c73ec0ab5bedc529f8f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 03:08:08 +0000 Subject: [PATCH 1/5] feat: add crates.io, npm, AUR, and cargo-binstall publishing support - Add crates.io metadata (repository, keywords, categories) to Cargo.toml - Add cargo-binstall support for fast binary installation - Create PKGBUILD for Arch Linux (AUR) distribution - Create npm wrapper package with automatic binary downloader - Update .gitignore to include npm/bin directory Publishing options now available: - cargo install bun-docs-mcp-proxy (from source) - cargo binstall bun-docs-mcp-proxy (pre-built binaries) - npm install -g bun-docs-mcp-proxy (Node.js users) - yay -S bun-docs-mcp-proxy (Arch Linux) --- .gitignore | 1 + Cargo.toml | 9 +++ PKGBUILD | 29 +++++++ npm/README.md | 64 ++++++++++++++++ npm/bin/bun-docs-mcp-proxy.js | 39 ++++++++++ npm/package.json | 37 +++++++++ npm/scripts/install.js | 140 ++++++++++++++++++++++++++++++++++ 7 files changed, 319 insertions(+) create mode 100644 PKGBUILD create mode 100644 npm/README.md create mode 100644 npm/bin/bun-docs-mcp-proxy.js create mode 100644 npm/package.json create mode 100644 npm/scripts/install.js diff --git a/.gitignore b/.gitignore index 593e863..d5e55e9 100644 --- a/.gitignore +++ b/.gitignore @@ -281,6 +281,7 @@ extension.wasm .serena/ bin/ +!npm/bin/ tarpaulin-report.html diff --git a/Cargo.toml b/Cargo.toml index 5311fc7..6ba3f82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,10 @@ version = "0.2.1" edition = "2024" description = "MCP proxy for Bun documentation search" license = "MIT" +repository = "https://github.com/kjanat/bun-docs-mcp-proxy" +homepage = "https://github.com/kjanat/bun-docs-mcp-proxy" +keywords = ["mcp", "bun", "documentation", "proxy", "json-rpc"] +categories = ["command-line-utilities", "development-tools"] [dependencies] anyhow = "1.0" @@ -36,3 +40,8 @@ strip = true # Remove debug symbols lto = true # Link-time optimization panic = "abort" # Smaller binary codegen-units = 1 # Better optimization + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }" +bin-dir = "{ bin }{ binary-ext }" +pkg-fmt = "tgz" diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..08e2016 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,29 @@ +# Maintainer: Your Name +pkgname=bun-docs-mcp-proxy +pkgver=0.2.1 +pkgrel=1 +pkgdesc="MCP proxy for Bun documentation search" +arch=('x86_64' 'aarch64') +url="https://github.com/kjanat/bun-docs-mcp-proxy" +license=('MIT') +depends=() +makedepends=('rust' 'cargo') +source=("$pkgname-$pkgver.tar.gz::https://github.com/kjanat/$pkgname/archive/v$pkgver.tar.gz") +sha256sums=('SKIP') # Update with actual checksum after first build + +build() { + cd "$pkgname-$pkgver" + cargo build --release --locked +} + +check() { + cd "$pkgname-$pkgver" + cargo test --release --locked +} + +package() { + cd "$pkgname-$pkgver" + install -Dm755 "target/release/$pkgname" "$pkgdir/usr/bin/$pkgname" + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" + install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md" +} diff --git a/npm/README.md b/npm/README.md new file mode 100644 index 0000000..84ed512 --- /dev/null +++ b/npm/README.md @@ -0,0 +1,64 @@ +# bun-docs-mcp-proxy + +Native Rust proxy for Bun documentation MCP context server. Bridges stdio-based MCP clients with the Bun HTTP MCP server. + +This npm package provides pre-built binaries for easy installation in Node.js/npm environments. + +## Installation + +```bash +npm install -g bun-docs-mcp-proxy +# or +npx bun-docs-mcp-proxy +``` + +## Usage + +```bash +bun-docs-mcp-proxy +``` + +The proxy reads JSON-RPC 2.0 messages from stdin and writes responses to stdout. + +## Features + +- Zero runtime dependencies (native binary) +- Tiny binary (~2.7 MB with TLS support) +- Fast startup (4ms cold start) +- Low memory (~2-5 MB RSS) +- Cross-platform (Linux, macOS, Windows on x64 and ARM64) + +## Supported Platforms + +- Linux x64 (glibc) +- Linux ARM64 (glibc) +- macOS x64 (Intel) +- macOS ARM64 (Apple Silicon) +- Windows x64 +- Windows ARM64 + +## Alternative Installation Methods + +### From source (requires Rust) +```bash +cargo install bun-docs-mcp-proxy +``` + +### Using cargo-binstall (faster, no compilation) +```bash +cargo binstall bun-docs-mcp-proxy +``` + +### Arch Linux (AUR) +```bash +yay -S bun-docs-mcp-proxy +``` + +## Repository + +For source code, issues, and more information, visit: +https://github.com/kjanat/bun-docs-mcp-proxy + +## License + +MIT diff --git a/npm/bin/bun-docs-mcp-proxy.js b/npm/bin/bun-docs-mcp-proxy.js new file mode 100644 index 0000000..f229d89 --- /dev/null +++ b/npm/bin/bun-docs-mcp-proxy.js @@ -0,0 +1,39 @@ +#!/usr/bin/env node + +const { spawn } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +const isWindows = process.platform === 'win32'; +const binaryName = isWindows ? 'bun-docs-mcp-proxy.exe' : 'bun-docs-mcp-proxy'; +const binaryPath = path.join(__dirname, binaryName); + +// Check if binary exists +if (!fs.existsSync(binaryPath)) { + console.error('Error: Binary not found. Please run: npm install'); + process.exit(1); +} + +// Spawn the binary and forward all stdio +const child = spawn(binaryPath, process.argv.slice(2), { + stdio: 'inherit', + windowsHide: true, +}); + +// Forward exit code +child.on('exit', (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + } else { + process.exit(code); + } +}); + +// Handle process termination +process.on('SIGINT', () => { + child.kill('SIGINT'); +}); + +process.on('SIGTERM', () => { + child.kill('SIGTERM'); +}); diff --git a/npm/package.json b/npm/package.json new file mode 100644 index 0000000..68e4184 --- /dev/null +++ b/npm/package.json @@ -0,0 +1,37 @@ +{ + "name": "bun-docs-mcp-proxy", + "version": "0.2.1", + "description": "MCP proxy for Bun documentation search - native binary distribution", + "keywords": [ + "mcp", + "bun", + "documentation", + "proxy", + "json-rpc", + "model-context-protocol" + ], + "homepage": "https://github.com/kjanat/bun-docs-mcp-proxy", + "bugs": { + "url": "https://github.com/kjanat/bun-docs-mcp-proxy/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/kjanat/bun-docs-mcp-proxy.git" + }, + "license": "MIT", + "author": "kjanat", + "bin": { + "bun-docs-mcp-proxy": "./bin/bun-docs-mcp-proxy.js" + }, + "scripts": { + "postinstall": "node scripts/install.js" + }, + "files": [ + "bin", + "scripts", + "README.md" + ], + "engines": { + "node": ">=14" + } +} diff --git a/npm/scripts/install.js b/npm/scripts/install.js new file mode 100644 index 0000000..1f6971a --- /dev/null +++ b/npm/scripts/install.js @@ -0,0 +1,140 @@ +#!/usr/bin/env node + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const { promisify } = require('util'); + +const streamPipeline = promisify(require('stream').pipeline); + +const VERSION = require('../package.json').version; +const REPO = 'kjanat/bun-docs-mcp-proxy'; + +function getPlatform() { + const platform = process.platform; + const arch = process.arch; + + // Map Node.js platform/arch to Rust target triples + const platformMap = { + 'linux-x64': 'x86_64-unknown-linux-gnu', + 'linux-arm64': 'aarch64-unknown-linux-gnu', + 'darwin-x64': 'x86_64-apple-darwin', + 'darwin-arm64': 'aarch64-apple-darwin', + 'win32-x64': 'x86_64-pc-windows-msvc', + 'win32-arm64': 'aarch64-pc-windows-msvc', + }; + + const key = `${platform}-${arch}`; + const target = platformMap[key]; + + if (!target) { + throw new Error( + `Unsupported platform: ${platform} ${arch}\n` + + `Please install from source: cargo install bun-docs-mcp-proxy` + ); + } + + return { target, isWindows: platform === 'win32' }; +} + +async function download(url, destination) { + return new Promise((resolve, reject) => { + https.get(url, (response) => { + if (response.statusCode === 302 || response.statusCode === 301) { + // Follow redirect + download(response.headers.location, destination).then(resolve).catch(reject); + return; + } + + if (response.statusCode !== 200) { + reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`)); + return; + } + + const fileStream = fs.createWriteStream(destination); + response.pipe(fileStream); + + fileStream.on('finish', () => { + fileStream.close(); + resolve(); + }); + + fileStream.on('error', (err) => { + fs.unlink(destination, () => {}); + reject(err); + }); + }).on('error', reject); + }); +} + +function extractTarGz(archivePath, outputDir, binaryName) { + // Use native tar command (available on Unix and modern Windows) + try { + execSync(`tar -xzf "${archivePath}" -C "${outputDir}"`, { stdio: 'pipe' }); + + // Move binary from extracted directory to bin directory + const extractedBinary = path.join(outputDir, binaryName); + if (!fs.existsSync(extractedBinary)) { + // Binary might be in a subdirectory + const files = fs.readdirSync(outputDir); + for (const file of files) { + const fullPath = path.join(outputDir, file, binaryName); + if (fs.existsSync(fullPath)) { + fs.renameSync(fullPath, extractedBinary); + // Clean up directory + fs.rmSync(path.join(outputDir, file), { recursive: true, force: true }); + break; + } + } + } + } catch (error) { + throw new Error(`Failed to extract archive: ${error.message}`); + } +} + +async function install() { + try { + const { target, isWindows } = getPlatform(); + const binaryName = isWindows ? 'bun-docs-mcp-proxy.exe' : 'bun-docs-mcp-proxy'; + const archiveExt = isWindows ? 'zip' : 'tar.gz'; + const archiveName = `bun-docs-mcp-proxy-${target}.${archiveExt}`; + + const downloadUrl = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`; + const binDir = path.join(__dirname, '..', 'bin'); + const archivePath = path.join(binDir, archiveName); + const binaryPath = path.join(binDir, binaryName); + + // Check if binary already exists + if (fs.existsSync(binaryPath)) { + console.log('Binary already installed'); + return; + } + + console.log(`Downloading bun-docs-mcp-proxy v${VERSION} for ${target}...`); + console.log(`URL: ${downloadUrl}`); + + await download(downloadUrl, archivePath); + console.log('Download complete, extracting...'); + + extractTarGz(archivePath, binDir, binaryName); + + // Clean up archive + fs.unlinkSync(archivePath); + + // Make binary executable (Unix-like systems) + if (!isWindows) { + fs.chmodSync(binaryPath, 0o755); + } + + console.log('Installation complete!'); + console.log(`Binary installed at: ${binaryPath}`); + } catch (error) { + console.error('Installation failed:', error.message); + console.error('\nYou can install from source using:'); + console.error(' cargo install bun-docs-mcp-proxy'); + process.exit(1); + } +} + +install(); From ca53a6807caf0effc8843a191e057911253a1a63 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 03:09:58 +0000 Subject: [PATCH 2/5] feat: add crates.io publishing to release workflow --- .github/workflows/release.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 625bef9..73a8dfb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -167,6 +167,28 @@ jobs: name: bun-docs-mcp-proxy-${{ matrix.platform.name }} path: target/${{ matrix.platform.target }}/release/bun-docs-mcp-proxy-${{ matrix.platform.name }}.zip + publish-crates: + name: Publish to crates.io + needs: lint + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache + uses: Swatinem/rust-cache@v2 + + - name: Publish to crates.io + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish + release: name: Create Release needs: build From d8526604d8ca19aac00a51cf42757c424a5d7a59 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 03:12:09 +0000 Subject: [PATCH 3/5] feat: add automated npm publishing to release workflow - Add publish-npm job that runs after GitHub release is created - Update npm install.js to use correct archive naming from releases - Add support for Windows zip extraction via PowerShell - Add .npmignore to exclude binaries (downloaded via postinstall) - npm package downloads appropriate binary during postinstall When a tag is pushed, the workflow will: 1. Build binaries for all platforms 2. Create GitHub Release with binaries 3. Publish to crates.io 4. Publish to npm (package downloads from GitHub Releases) --- .github/workflows/release.yml | 23 +++++++++++++++++ npm/.npmignore | 8 ++++++ npm/scripts/install.js | 47 ++++++++++++++++++++++++++++------- 3 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 npm/.npmignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73a8dfb..b881579 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -189,6 +189,29 @@ jobs: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: cargo publish + publish-npm: + name: Publish to npm + needs: release + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + + - name: Publish to npm + working-directory: npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish + release: name: Create Release needs: build diff --git a/npm/.npmignore b/npm/.npmignore new file mode 100644 index 0000000..bb9df21 --- /dev/null +++ b/npm/.npmignore @@ -0,0 +1,8 @@ +# Don't include binaries in the package - they're downloaded during postinstall +bin/* +!bin/bun-docs-mcp-proxy.js + +# Development files +*.log +.DS_Store +node_modules/ diff --git a/npm/scripts/install.js b/npm/scripts/install.js index 1f6971a..24bec2e 100644 --- a/npm/scripts/install.js +++ b/npm/scripts/install.js @@ -15,14 +15,14 @@ function getPlatform() { const platform = process.platform; const arch = process.arch; - // Map Node.js platform/arch to Rust target triples + // Map Node.js platform/arch to release archive names const platformMap = { - 'linux-x64': 'x86_64-unknown-linux-gnu', - 'linux-arm64': 'aarch64-unknown-linux-gnu', - 'darwin-x64': 'x86_64-apple-darwin', - 'darwin-arm64': 'aarch64-apple-darwin', - 'win32-x64': 'x86_64-pc-windows-msvc', - 'win32-arm64': 'aarch64-pc-windows-msvc', + 'linux-x64': 'linux-x86_64', + 'linux-arm64': 'linux-aarch64', + 'darwin-x64': 'macos-x86_64', + 'darwin-arm64': 'macos-aarch64', + 'win32-x64': 'windows-x86_64', + 'win32-arm64': 'windows-aarch64', }; const key = `${platform}-${arch}`; @@ -89,7 +89,32 @@ function extractTarGz(archivePath, outputDir, binaryName) { } } } catch (error) { - throw new Error(`Failed to extract archive: ${error.message}`); + throw new Error(`Failed to extract tar.gz archive: ${error.message}`); + } +} + +function extractZip(archivePath, outputDir, binaryName) { + // Use PowerShell on Windows (always available) + try { + execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${outputDir}' -Force"`, { stdio: 'pipe' }); + + // Move binary from extracted directory to bin directory if needed + const extractedBinary = path.join(outputDir, binaryName); + if (!fs.existsSync(extractedBinary)) { + // Binary might be in a subdirectory + const files = fs.readdirSync(outputDir); + for (const file of files) { + const fullPath = path.join(outputDir, file, binaryName); + if (fs.existsSync(fullPath)) { + fs.renameSync(fullPath, extractedBinary); + // Clean up directory + fs.rmSync(path.join(outputDir, file), { recursive: true, force: true }); + break; + } + } + } + } catch (error) { + throw new Error(`Failed to extract zip archive: ${error.message}`); } } @@ -117,7 +142,11 @@ async function install() { await download(downloadUrl, archivePath); console.log('Download complete, extracting...'); - extractTarGz(archivePath, binDir, binaryName); + if (isWindows) { + extractZip(archivePath, binDir, binaryName); + } else { + extractTarGz(archivePath, binDir, binaryName); + } // Clean up archive fs.unlinkSync(archivePath); From 68ce5ed511d47dd3aec9f0f10bd0fc145aee3b2d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 03:27:21 +0000 Subject: [PATCH 4/5] feat: add Debian and AUR packaging support with cargo-deb and cargo-aur - Add cargo-deb metadata to Cargo.toml for .deb package generation - Add cargo-aur metadata to Cargo.toml for AUR package generation - Create package-deb job in release workflow (x86_64 and ARM64) - Create package-aur job to generate PKGBUILD and source tarball - Update release job to include .deb packages and AUR artifacts - Update release notes with package manager installation options Distribution options now include: - Debian/Ubuntu: .deb packages for x86_64 and ARM64 - Arch Linux: Auto-generated PKGBUILD and source tarball - All packages include attestations and checksums Users can install with: - cargo install bun-docs-mcp-proxy (from source) - cargo binstall bun-docs-mcp-proxy (pre-built) - npm install -g bun-docs-mcp-proxy (Node.js) - sudo dpkg -i bun-docs-mcp-proxy_x86_64.deb (Debian/Ubuntu) - makepkg -si (Arch Linux, using included PKGBUILD) --- .github/workflows/release.yml | 148 ++++++++++++++++++++++++++++++++-- Cargo.toml | 21 +++++ 2 files changed, 161 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b881579..7e813c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -189,6 +189,107 @@ jobs: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: cargo publish + package-deb: + name: Build Debian packages + needs: lint + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: read + id-token: write + attestations: write + strategy: + matrix: + target: + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-gnu + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Cache + uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.target }}-deb + + - name: Install cargo-deb + uses: taiki-e/install-action@v2 + with: + tool: cargo-deb + + - name: Install cross-compilation tools (ARM64) + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - name: Build and create .deb package + run: cargo deb --target ${{ matrix.target }} + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + + - name: Rename .deb file + run: | + DEB_FILE=$(find target/${{ matrix.target }}/debian -name "*.deb") + ARCH=$(echo ${{ matrix.target }} | cut -d- -f1) + mv "$DEB_FILE" "target/${{ matrix.target }}/debian/bun-docs-mcp-proxy_${ARCH}.deb" + + - name: Generate attestation + uses: actions/attest-build-provenance@v1 + with: + subject-path: "target/${{ matrix.target }}/debian/*.deb" + + - name: Upload artifact + uses: actions/upload-artifact@v5 + with: + name: deb-${{ matrix.target }} + path: target/${{ matrix.target }}/debian/*.deb + + package-aur: + name: Generate AUR package + needs: lint + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: read + id-token: write + attestations: write + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-aur + uses: taiki-e/install-action@v2 + with: + tool: cargo-aur + + - name: Generate AUR package + run: cargo aur + + - name: Generate attestation + uses: actions/attest-build-provenance@v1 + with: + subject-path: "target/cargo-aur/*.tar.gz" + + - name: Upload artifacts + uses: actions/upload-artifact@v5 + with: + name: aur-package + path: | + target/cargo-aur/*.tar.gz + target/cargo-aur/PKGBUILD + publish-npm: name: Publish to npm needs: release @@ -214,7 +315,7 @@ jobs: release: name: Create Release - needs: build + needs: [build, package-deb, package-aur] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') permissions: @@ -231,8 +332,8 @@ jobs: - name: Generate checksums run: | cd artifacts - # Generate SHA256SUMS file - find . -type f \( -name "*.tar.gz" -o -name "*.zip" \) -exec sha256sum {} \; | sed 's|./[^/]*/||' > SHA256SUMS + # Generate SHA256SUMS file for binaries and packages + find . -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.deb" \) -exec sha256sum {} \; | sed 's|./[^/]*/||' > SHA256SUMS cat SHA256SUMS - name: Create Release @@ -246,11 +347,32 @@ jobs: cat > release_notes.md << 'EOF' ## Bun Docs MCP Proxy ${{ github.ref_name }} - ### Installation + ### Installation Options + + #### Package Managers (Recommended) - Download the binary for your platform below, extract it, and make it executable (Linux/macOS). + ```bash + # Rust (from source) + cargo install bun-docs-mcp-proxy + + # Rust (pre-built binaries, faster) + cargo binstall bun-docs-mcp-proxy + + # npm/Node.js + npm install -g bun-docs-mcp-proxy - #### Linux/macOS + # Debian/Ubuntu (x86_64 or ARM64) + wget https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/bun-docs-mcp-proxy_x86_64.deb + sudo dpkg -i bun-docs-mcp-proxy_x86_64.deb + + # Arch Linux (AUR) + # Download PKGBUILD from this release and build with makepkg + # Or wait for AUR package publication + ``` + + #### Direct Binary Download + + **Linux/macOS:** ```bash # Download and extract (adjust URL for your platform) curl -L https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/bun-docs-mcp-proxy-linux-x86_64.tar.gz | tar xz @@ -262,11 +384,16 @@ jobs: sudo mv bun-docs-mcp-proxy /usr/local/bin/ ``` - #### Windows + **Windows:** Download the `.zip` file, extract it, and add to your PATH. - ### Platforms + ### Available Downloads + **Debian Packages:** + - **Debian/Ubuntu x86_64**: `bun-docs-mcp-proxy_x86_64.deb` + - **Debian/Ubuntu ARM64**: `bun-docs-mcp-proxy_aarch64.deb` + + **Binary Archives:** - **Linux x86_64**: `bun-docs-mcp-proxy-linux-x86_64.tar.gz` - **Linux ARM64**: `bun-docs-mcp-proxy-linux-aarch64.tar.gz` - **Linux x86_64 musl** (static): `bun-docs-mcp-proxy-linux-x86_64-musl.tar.gz` @@ -276,6 +403,9 @@ jobs: - **Windows x86_64**: `bun-docs-mcp-proxy-windows-x86_64.zip` - **Windows ARM64**: `bun-docs-mcp-proxy-windows-aarch64.zip` + **AUR Package:** + - `PKGBUILD` and source tarball included for Arch Linux users + ### Verification #### Automated Verification (Recommended) @@ -323,4 +453,6 @@ jobs: --notes-file release_notes.md \ artifacts/*/*.tar.gz \ artifacts/*/*.zip \ + artifacts/*/*.deb \ + artifacts/aur-package/* \ artifacts/SHA256SUMS diff --git a/Cargo.toml b/Cargo.toml index 6ba3f82..75e5775 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,27 @@ homepage = "https://github.com/kjanat/bun-docs-mcp-proxy" keywords = ["mcp", "bun", "documentation", "proxy", "json-rpc"] categories = ["command-line-utilities", "development-tools"] +[package.metadata.deb] +maintainer = "kjanat" +copyright = "2025, kjanat " +license-file = ["LICENSE", "4"] +extended-description = """\ +Native Rust proxy for Bun documentation MCP context server. \ +Bridges stdio-based MCP clients with the Bun HTTP MCP server at https://bun.com/docs/mcp. \ +\ +Features zero runtime dependencies, 2.7 MB binary size, 4ms cold start, \ +and comprehensive error handling.""" +depends = "$auto" +section = "utils" +priority = "optional" +assets = [ + ["target/release/bun-docs-mcp-proxy", "usr/bin/", "755"], + ["README.md", "usr/share/doc/bun-docs-mcp-proxy/", "644"], +] + +[package.metadata.aur] +depends = [] + [dependencies] anyhow = "1.0" eventsource-stream = "0.2" From 278bc44fe4bf508c178ff0eaa14350ed933f17f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 03:37:45 +0000 Subject: [PATCH 5/5] fix: address critical security issues and improve packaging reliability CRITICAL SECURITY FIXES: - Add SHA256 checksum verification to npm install script (npm/scripts/install.js:98-153) * Downloads SHA256SUMS file from release * Calculates checksum of downloaded archive * Verifies before extraction to prevent supply chain attacks - Remove manual PKGBUILD with sha256sums=('SKIP') * cargo-aur auto-generates PKGBUILD with proper checksums * Eliminates conflicting sources of truth HIGH PRIORITY FIXES: - Fix cargo-binstall URL template mismatch (Cargo.toml:70-94) * Add platform-specific overrides to match actual release naming * e.g., linux-x86_64 instead of x86_64-unknown-linux-gnu * Ensures `cargo binstall` can correctly locate binaries - Add verification step before npm publish (release.yml:310-324) * Waits 30s for release asset propagation * Verifies SHA256SUMS and key binaries are accessible * Prevents race condition where npm package publishes before binaries are available IMPROVEMENTS: - Update Node.js engine requirement from >=14 to >=18 (Node 14 EOL April 2023) - Improve error handling in extract functions * Separate try/catch blocks for extraction vs binary location * More specific error messages for each failure mode * Better guidance for users when errors occur Security verification workflow: 1. Download archive from GitHub Release 2. Download SHA256SUMS file 3. Calculate actual checksum of archive 4. Compare and fail if mismatch 5. Only extract if verification succeeds This addresses all critical security concerns from PR review. --- .github/workflows/release.yml | 16 ++++ Cargo.toml | 26 ++++++ PKGBUILD | 29 ------- npm/package.json | 2 +- npm/scripts/install.js | 153 +++++++++++++++++++++++++++------- 5 files changed, 165 insertions(+), 61 deletions(-) delete mode 100644 PKGBUILD diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e813c9..2d23fde 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -307,6 +307,22 @@ jobs: node-version: '20' registry-url: 'https://registry.npmjs.org' + - name: Verify release artifacts are available + run: | + # Extract version from tag + VERSION=${GITHUB_REF#refs/tags/v} + + # Wait a bit for GitHub to make release assets fully available + echo "Waiting 30 seconds for release assets to propagate..." + sleep 30 + + # Verify key artifacts are accessible + echo "Verifying release artifacts..." + curl -fsSL "https://github.com/${{ github.repository }}/releases/download/v${VERSION}/SHA256SUMS" -o /tmp/checksums + curl -fsSL "https://github.com/${{ github.repository }}/releases/download/v${VERSION}/bun-docs-mcp-proxy-linux-x86_64.tar.gz" --head + + echo "✓ Release artifacts verified and accessible" + - name: Publish to npm working-directory: npm env: diff --git a/Cargo.toml b/Cargo.toml index 75e5775..237e814 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,3 +66,29 @@ codegen-units = 1 # Better optimization pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }" bin-dir = "{ bin }{ binary-ext }" pkg-fmt = "tgz" + +[package.metadata.binstall.overrides.x86_64-unknown-linux-gnu] +pkg-url = "{ repo }/releases/download/v{ version }/bun-docs-mcp-proxy-linux-x86_64.tar.gz" + +[package.metadata.binstall.overrides.aarch64-unknown-linux-gnu] +pkg-url = "{ repo }/releases/download/v{ version }/bun-docs-mcp-proxy-linux-aarch64.tar.gz" + +[package.metadata.binstall.overrides.x86_64-unknown-linux-musl] +pkg-url = "{ repo }/releases/download/v{ version }/bun-docs-mcp-proxy-linux-x86_64-musl.tar.gz" + +[package.metadata.binstall.overrides.aarch64-unknown-linux-musl] +pkg-url = "{ repo }/releases/download/v{ version }/bun-docs-mcp-proxy-linux-aarch64-musl.tar.gz" + +[package.metadata.binstall.overrides.x86_64-apple-darwin] +pkg-url = "{ repo }/releases/download/v{ version }/bun-docs-mcp-proxy-macos-x86_64.tar.gz" + +[package.metadata.binstall.overrides.aarch64-apple-darwin] +pkg-url = "{ repo }/releases/download/v{ version }/bun-docs-mcp-proxy-macos-aarch64.tar.gz" + +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-url = "{ repo }/releases/download/v{ version }/bun-docs-mcp-proxy-windows-x86_64.zip" +pkg-fmt = "zip" + +[package.metadata.binstall.overrides.aarch64-pc-windows-msvc] +pkg-url = "{ repo }/releases/download/v{ version }/bun-docs-mcp-proxy-windows-aarch64.zip" +pkg-fmt = "zip" diff --git a/PKGBUILD b/PKGBUILD deleted file mode 100644 index 08e2016..0000000 --- a/PKGBUILD +++ /dev/null @@ -1,29 +0,0 @@ -# Maintainer: Your Name -pkgname=bun-docs-mcp-proxy -pkgver=0.2.1 -pkgrel=1 -pkgdesc="MCP proxy for Bun documentation search" -arch=('x86_64' 'aarch64') -url="https://github.com/kjanat/bun-docs-mcp-proxy" -license=('MIT') -depends=() -makedepends=('rust' 'cargo') -source=("$pkgname-$pkgver.tar.gz::https://github.com/kjanat/$pkgname/archive/v$pkgver.tar.gz") -sha256sums=('SKIP') # Update with actual checksum after first build - -build() { - cd "$pkgname-$pkgver" - cargo build --release --locked -} - -check() { - cd "$pkgname-$pkgver" - cargo test --release --locked -} - -package() { - cd "$pkgname-$pkgver" - install -Dm755 "target/release/$pkgname" "$pkgdir/usr/bin/$pkgname" - install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" - install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md" -} diff --git a/npm/package.json b/npm/package.json index 68e4184..07c7e96 100644 --- a/npm/package.json +++ b/npm/package.json @@ -32,6 +32,6 @@ "README.md" ], "engines": { - "node": ">=14" + "node": ">=18" } } diff --git a/npm/scripts/install.js b/npm/scripts/install.js index 24bec2e..6ce81ef 100644 --- a/npm/scripts/install.js +++ b/npm/scripts/install.js @@ -3,6 +3,7 @@ const https = require('https'); const fs = require('fs'); const path = require('path'); +const crypto = require('crypto'); const { execSync } = require('child_process'); const { promisify } = require('util'); @@ -68,28 +69,112 @@ async function download(url, destination) { }); } +async function downloadText(url) { + return new Promise((resolve, reject) => { + https.get(url, (response) => { + if (response.statusCode === 302 || response.statusCode === 301) { + // Follow redirect + downloadText(response.headers.location).then(resolve).catch(reject); + return; + } + + if (response.statusCode !== 200) { + reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`)); + return; + } + + let data = ''; + response.on('data', (chunk) => { + data += chunk; + }); + + response.on('end', () => { + resolve(data); + }); + }).on('error', reject); + }); +} + +function calculateSHA256(filePath) { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('sha256'); + const stream = fs.createReadStream(filePath); + + stream.on('data', (data) => { + hash.update(data); + }); + + stream.on('end', () => { + resolve(hash.digest('hex')); + }); + + stream.on('error', (err) => { + reject(err); + }); + }); +} + +async function verifyChecksum(filePath, expectedChecksum, fileName) { + console.log('Verifying checksum...'); + + const actualChecksum = await calculateSHA256(filePath); + + if (actualChecksum !== expectedChecksum) { + throw new Error( + `Checksum verification failed for ${fileName}\n` + + `Expected: ${expectedChecksum}\n` + + `Actual: ${actualChecksum}\n` + + `This may indicate a corrupted download or security issue.` + ); + } + + console.log('Checksum verified successfully ✓'); +} + +async function getExpectedChecksum(archiveName) { + const checksumsUrl = `https://github.com/${REPO}/releases/download/v${VERSION}/SHA256SUMS`; + + console.log('Downloading checksums file...'); + const checksumsContent = await downloadText(checksumsUrl); + + // Parse SHA256SUMS file (format: "checksum filename") + const lines = checksumsContent.split('\n'); + for (const line of lines) { + const match = line.match(/^([a-f0-9]{64})\s+(.+)$/); + if (match && match[2] === archiveName) { + return match[1]; + } + } + + throw new Error( + `Checksum not found for ${archiveName} in SHA256SUMS file.\n` + + `This may indicate the release is incomplete or corrupted.` + ); +} + function extractTarGz(archivePath, outputDir, binaryName) { // Use native tar command (available on Unix and modern Windows) try { execSync(`tar -xzf "${archivePath}" -C "${outputDir}"`, { stdio: 'pipe' }); + } catch (error) { + throw new Error(`Failed to extract tar.gz archive: ${error.message}\nEnsure 'tar' is available in your PATH.`); + } - // Move binary from extracted directory to bin directory - const extractedBinary = path.join(outputDir, binaryName); - if (!fs.existsSync(extractedBinary)) { - // Binary might be in a subdirectory - const files = fs.readdirSync(outputDir); - for (const file of files) { - const fullPath = path.join(outputDir, file, binaryName); - if (fs.existsSync(fullPath)) { - fs.renameSync(fullPath, extractedBinary); - // Clean up directory - fs.rmSync(path.join(outputDir, file), { recursive: true, force: true }); - break; - } + // Move binary from extracted directory to bin directory if needed + const extractedBinary = path.join(outputDir, binaryName); + if (!fs.existsSync(extractedBinary)) { + // Binary might be in a subdirectory + const files = fs.readdirSync(outputDir).filter(f => f !== path.basename(archivePath)); + for (const file of files) { + const fullPath = path.join(outputDir, file, binaryName); + if (fs.existsSync(fullPath)) { + fs.renameSync(fullPath, extractedBinary); + // Clean up directory + fs.rmSync(path.join(outputDir, file), { recursive: true, force: true }); + return; } } - } catch (error) { - throw new Error(`Failed to extract tar.gz archive: ${error.message}`); + throw new Error(`Binary ${binaryName} not found in extracted archive.`); } } @@ -97,24 +182,25 @@ function extractZip(archivePath, outputDir, binaryName) { // Use PowerShell on Windows (always available) try { execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${outputDir}' -Force"`, { stdio: 'pipe' }); + } catch (error) { + throw new Error(`Failed to extract zip archive: ${error.message}\nEnsure PowerShell is available.`); + } - // Move binary from extracted directory to bin directory if needed - const extractedBinary = path.join(outputDir, binaryName); - if (!fs.existsSync(extractedBinary)) { - // Binary might be in a subdirectory - const files = fs.readdirSync(outputDir); - for (const file of files) { - const fullPath = path.join(outputDir, file, binaryName); - if (fs.existsSync(fullPath)) { - fs.renameSync(fullPath, extractedBinary); - // Clean up directory - fs.rmSync(path.join(outputDir, file), { recursive: true, force: true }); - break; - } + // Move binary from extracted directory to bin directory if needed + const extractedBinary = path.join(outputDir, binaryName); + if (!fs.existsSync(extractedBinary)) { + // Binary might be in a subdirectory + const files = fs.readdirSync(outputDir).filter(f => f !== path.basename(archivePath)); + for (const file of files) { + const fullPath = path.join(outputDir, file, binaryName); + if (fs.existsSync(fullPath)) { + fs.renameSync(fullPath, extractedBinary); + // Clean up directory + fs.rmSync(path.join(outputDir, file), { recursive: true, force: true }); + return; } } - } catch (error) { - throw new Error(`Failed to extract zip archive: ${error.message}`); + throw new Error(`Binary ${binaryName} not found in extracted archive.`); } } @@ -140,8 +226,13 @@ async function install() { console.log(`URL: ${downloadUrl}`); await download(downloadUrl, archivePath); - console.log('Download complete, extracting...'); + console.log('Download complete.'); + + // Verify checksum for security + const expectedChecksum = await getExpectedChecksum(archiveName); + await verifyChecksum(archivePath, expectedChecksum, archiveName); + console.log('Extracting binary...'); if (isWindows) { extractZip(archivePath, binDir, binaryName); } else {