diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 625bef9..2d23fde 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -167,9 +167,171 @@ 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 + + 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 + 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: 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: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish + release: name: Create Release - needs: build + needs: [build, package-deb, package-aur] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') permissions: @@ -186,8 +348,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 @@ -201,11 +363,32 @@ jobs: cat > release_notes.md << 'EOF' ## Bun Docs MCP Proxy ${{ github.ref_name }} - ### Installation + ### Installation Options - Download the binary for your platform below, extract it, and make it executable (Linux/macOS). + #### Package Managers (Recommended) - #### 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 + + # 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 @@ -217,11 +400,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` @@ -231,6 +419,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) @@ -278,4 +469,6 @@ jobs: --notes-file release_notes.md \ artifacts/*/*.tar.gz \ artifacts/*/*.zip \ + artifacts/*/*.deb \ + artifacts/aur-package/* \ artifacts/SHA256SUMS 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..237e814 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,31 @@ 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"] + +[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" @@ -36,3 +61,34 @@ 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" + +[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/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/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..07c7e96 --- /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": ">=18" + } +} diff --git a/npm/scripts/install.js b/npm/scripts/install.js new file mode 100644 index 0000000..6ce81ef --- /dev/null +++ b/npm/scripts/install.js @@ -0,0 +1,260 @@ +#!/usr/bin/env node + +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'); + +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 release archive names + const platformMap = { + '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}`; + 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); + }); +} + +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 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; + } + } + throw new Error(`Binary ${binaryName} not found in extracted archive.`); + } +} + +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).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; + } + } + throw new Error(`Binary ${binaryName} not found in extracted archive.`); + } +} + +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.'); + + // 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 { + 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();