From 3da835c5bc2da5b38a77ef4d46247940cc28d6d1 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 10 May 2026 17:04:49 +0100 Subject: [PATCH] Publish a distribution path for pulse-mcp The MCP adapter shipped in slice 51 with one install option: clone the repo and go build. This slice integrates pulse-mcp into Pulse's existing governed release pipeline so a Pulse release publishes a pulse-mcp binary alongside the unified agent and the install scripts that bring it home in one command. What ships: - scripts/build-release.sh extended to build pulse-mcp for the same multi-OS matrix as the unified agent, package per-platform tarballs and zips, and copy bare binaries to RELEASE_DIR for /releases/latest/download/ redirect compatibility. - .github/workflows/create-release.yml extended to upload the bare pulse-mcp binaries plus install-mcp.sh and install-mcp.ps1 as release assets. - scripts/install-mcp.sh: bash one-line installer that detects platform/arch, downloads the matching binary from the configured release (latest by default), verifies SHA256 against the published checksums.txt, places at ~/.local/bin/pulse-mcp (or /usr/local/bin if not writable). Honors PULSE_MCP_VERSION, PULSE_MCP_BIN_DIR, PULSE_MCP_REPO, PULSE_MCP_NO_VERIFY env vars; declines Windows shells with a pointer at the .ps1 sibling. - scripts/install-mcp.ps1: PowerShell installer for Windows, placing pulse-mcp.exe at $LOCALAPPDATA\pulse-mcp. Documentation aligned: - cmd/pulse-mcp/README.md gains an Install section above Quick start with three options: one-line installer, GitHub Release download, go install. Documents the macOS Gatekeeper bypass since v1 is unnotarized by design. - The Settings -> API Access agent-integrations panel now surfaces the curl|bash command above the config snippet so operators see "install pulse-mcp" before "configure your MCP client." - docs/releases/AGENT_PARADIGM.md drops the "no published distribution path" item from "what it does not do yet" and documents the Gatekeeper / Homebrew gaps as next-tier follow-ups. Trade-offs surfaced and chosen: - Same cadence as Pulse: pulse-mcp ships per Pulse release, not on its own track. The MCP server reads the manifest from the Pulse it talks to, so version alignment is the natural model. - No Homebrew tap or core formula in v1. Maintaining a tap is real ongoing work; foundation supports adding Homebrew later as a layer. - No Docker image. Stdio JSON-RPC fights Docker's stdin /stdout pattern. - No notarization in v1. SHA256 verification through the installer preserves the audit trail; README documents the Gatekeeper bypass. Subsystem contract: deployment-installability.md gains scripts/install-mcp.sh, scripts/install-mcp.ps1, and cmd/pulse-mcp/ in canonical files (mid-list entries renumbered) plus a paragraph documenting the new MCP entry point alongside the existing installer family. Verification artifacts: - scripts/installtests/build_release_assets_test.go gains TestBuildReleasePackagesPulseMcpForAllPlatforms which pins the build/package/copy wiring and the load-bearing install-mcp.sh helpers (platform detection, SHA256 verification, install-dir resolution). - scripts/release_control/render_release_body_test.py gains test_agent_paradigm_release_notes_blurb_documents_- distribution_path which pins the AGENT_PARADIGM.md draft's install-mcp.sh reference and the four-axis frame so a future edit cannot regress the install story silently. Smoke-tested install-mcp.sh locally on darwin-arm64: platform detection, install-dir resolution, URL building, and 404 error handling all correct. The full end-to-end install path becomes live the moment a Pulse release ships pulse-mcp binaries; the next RC cut will exercise it. --- .github/workflows/create-release.yml | 23 +++ cmd/pulse-mcp/README.md | 58 +++++- .../subsystems/deployment-installability.md | 93 ++++++--- docs/releases/AGENT_PARADIGM.md | 35 ++-- .../Settings/AgentIntegrationsPanel.tsx | 5 +- scripts/build-release.sh | 64 ++++++ scripts/install-mcp.ps1 | 129 ++++++++++++ scripts/install-mcp.sh | 188 ++++++++++++++++++ .../installtests/build_release_assets_test.go | 77 +++++++ .../render_release_body_test.py | 27 +++ 10 files changed, 650 insertions(+), 49 deletions(-) create mode 100644 scripts/install-mcp.ps1 create mode 100755 scripts/install-mcp.sh diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 7697d3d97e..cc45b1c64a 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -908,10 +908,33 @@ jobs: release_upload_with_retry "${TAG}" "${bare_agent}" --clobber fi done + for bare_mcp in \ + release/pulse-mcp-linux-amd64 \ + release/pulse-mcp-linux-arm64 \ + release/pulse-mcp-linux-armv7 \ + release/pulse-mcp-linux-armv6 \ + release/pulse-mcp-linux-386 \ + release/pulse-mcp-darwin-amd64 \ + release/pulse-mcp-darwin-arm64 \ + release/pulse-mcp-freebsd-amd64 \ + release/pulse-mcp-freebsd-arm64 \ + release/pulse-mcp-windows-amd64.exe \ + release/pulse-mcp-windows-arm64.exe \ + release/pulse-mcp-windows-386.exe; do + if [ -f "${bare_mcp}" ]; then + release_upload_with_retry "${TAG}" "${bare_mcp}" --clobber + fi + done release_upload_with_retry "${TAG}" release/install.sh --clobber if [ -f release/install.ps1 ]; then release_upload_with_retry "${TAG}" release/install.ps1 --clobber fi + if [ -f release/install-mcp.sh ]; then + release_upload_with_retry "${TAG}" release/install-mcp.sh --clobber + fi + if [ -f release/install-mcp.ps1 ]; then + release_upload_with_retry "${TAG}" release/install-mcp.ps1 --clobber + fi release_upload_with_retry "${TAG}" release/install-docker.sh --clobber release_upload_with_retry "${TAG}" release/pulse-auto-update.sh --clobber diff --git a/cmd/pulse-mcp/README.md b/cmd/pulse-mcp/README.md index 7dfebbfbbf..adee54f842 100644 --- a/cmd/pulse-mcp/README.md +++ b/cmd/pulse-mcp/README.md @@ -10,11 +10,56 @@ Pulse's hand-authored capabilities manifest at `/api/agent/capabilities`. Adding a capability there extends this server automatically. There is no hardcoded tool list to keep in sync. -## Quick start +## Install -### 1. Build +Three ways to get the binary, in order of friction. -From the repo root: +### 1. One-line installer (recommended) + +```sh +curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install-mcp.sh | bash +``` + +Detects your platform, downloads the matching binary from the latest +Pulse release, verifies SHA256, and places it at `~/.local/bin/pulse-mcp` +(or `/usr/local/bin/pulse-mcp` if `~/.local/bin` is not writable). +Override the install location with `PULSE_MCP_BIN_DIR=/some/path` or +the version with `PULSE_MCP_VERSION=v6.0.0-rc.5`. + +Windows (PowerShell): + +```powershell +irm https://github.com/rcourtman/Pulse/releases/latest/download/install-mcp.ps1 | iex +``` + +### 2. Download from a GitHub Release + +Pick the binary that matches your platform from the Pulse release assets: + +- `pulse-mcp-darwin-arm64`, `pulse-mcp-darwin-amd64` for macOS +- `pulse-mcp-linux-amd64`, `pulse-mcp-linux-arm64`, `pulse-mcp-linux-armv7` for Linux +- `pulse-mcp-windows-amd64.exe` for Windows +- (full matrix on the release page) + +`chmod +x` it on Unix, drop it on your `PATH`. SHA256 sums for every +binary are in `checksums.txt` on the same release. + +On macOS the first launch may show a Gatekeeper warning ("cannot be +opened because the developer cannot be verified"). Either right-click +the binary and pick Open the first time, or run +`xattr -d com.apple.quarantine pulse-mcp` to clear the quarantine flag. +Notarization is intentionally skipped for v1; the install-script path +above downloads the same unsigned binary. + +### 3. Build from source + +If you have Go installed: + +```sh +go install github.com/rcourtman/pulse-go-rewrite/cmd/pulse-mcp@latest +``` + +Or from a Pulse repo checkout: ```sh go build -o pulse-mcp ./cmd/pulse-mcp @@ -23,6 +68,13 @@ go build -o pulse-mcp ./cmd/pulse-mcp Drop the binary somewhere on your `PATH` (or reference its full path in the config snippets below). +## Quick start + +### 1. Get the binary + +Use any of the install paths above. The rest of this guide assumes +`pulse-mcp` is on your `PATH`. + ### 2. Mint an API token Pulse needs a token with `monitoring:read` for the read tools, and diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 0b0e3cfb24..6503ad2d58 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -65,39 +65,42 @@ server-side update execution surfaces. 41. `install.sh` 42. `scripts/install.ps1` 43. `scripts/install.sh` -44. `scripts/pulse-auto-update.sh` -45. `scripts/release_control/internal/record_rc_to_ga_rehearsal.py` -46. `scripts/release_control/record_rc_to_ga_rehearsal.py` -47. `scripts/release_control/release_promotion_policy_support.py` -48. `scripts/release_control/resolve_release_promotion.py` -49. `scripts/release_ldflags.sh` -50. `scripts/run_cloud_public_signup_smoke.sh` -51. `scripts/run_demo_public_browser_smoke.sh` -52. `scripts/demo_public_browser_smoke.cjs` -53. `scripts/run_hosted_staging_smoke.sh` -54. `scripts/trigger-release-dry-run.sh` -55. `scripts/trigger-release.sh` -56. `scripts/toggle-mock.sh` -57. `deploy/helm/pulse/` -58. `tests/integration/playwright.config.ts` -59. `tests/integration/QUICK_START.md` -60. `tests/integration/README.md` -61. `tests/integration/scripts/bootstrap-hosted-mobile-onboarding.mjs` -62. `tests/integration/scripts/hosted-mobile-token-runtime.mjs` -63. `tests/integration/scripts/hosted-tenant-approval-store.mjs` -64. `tests/integration/scripts/hosted-tenant-runtime.mjs` -65. `tests/integration/scripts/hosted-tenant-runtime-restart.mjs` -66. `tests/integration/scripts/managed-dev-runtime.mjs` -67. `tests/integration/scripts/relay-mobile-token-helper.go` -68. `tests/integration/tests/helpers.ts` -69. `tests/integration/tests/runtime-defaults.ts` -70. `docker-compose.yml` -71. `scripts/install-docker.sh` -72. `scripts/validate-published-release.sh` -73. `scripts/validate-release.sh` -74. `scripts/release_asset_common.sh` -75. `scripts/backfill-release-assets.sh` -76. `.github/workflows/backfill-release-assets.yml` +44. `scripts/install-mcp.sh` +45. `scripts/install-mcp.ps1` +46. `cmd/pulse-mcp/` +47. `scripts/pulse-auto-update.sh` +48. `scripts/release_control/internal/record_rc_to_ga_rehearsal.py` +49. `scripts/release_control/record_rc_to_ga_rehearsal.py` +50. `scripts/release_control/release_promotion_policy_support.py` +51. `scripts/release_control/resolve_release_promotion.py` +52. `scripts/release_ldflags.sh` +53. `scripts/run_cloud_public_signup_smoke.sh` +54. `scripts/run_demo_public_browser_smoke.sh` +55. `scripts/demo_public_browser_smoke.cjs` +56. `scripts/run_hosted_staging_smoke.sh` +57. `scripts/trigger-release-dry-run.sh` +58. `scripts/trigger-release.sh` +59. `scripts/toggle-mock.sh` +60. `deploy/helm/pulse/` +61. `tests/integration/playwright.config.ts` +62. `tests/integration/QUICK_START.md` +63. `tests/integration/README.md` +64. `tests/integration/scripts/bootstrap-hosted-mobile-onboarding.mjs` +65. `tests/integration/scripts/hosted-mobile-token-runtime.mjs` +66. `tests/integration/scripts/hosted-tenant-approval-store.mjs` +67. `tests/integration/scripts/hosted-tenant-runtime.mjs` +68. `tests/integration/scripts/hosted-tenant-runtime-restart.mjs` +69. `tests/integration/scripts/managed-dev-runtime.mjs` +70. `tests/integration/scripts/relay-mobile-token-helper.go` +71. `tests/integration/tests/helpers.ts` +72. `tests/integration/tests/runtime-defaults.ts` +73. `docker-compose.yml` +74. `scripts/install-docker.sh` +75. `scripts/validate-published-release.sh` +76. `scripts/validate-release.sh` +77. `scripts/release_asset_common.sh` +78. `scripts/backfill-release-assets.sh` +79. `.github/workflows/backfill-release-assets.yml` ## Shared Boundaries @@ -732,6 +735,30 @@ release artifacts. `scripts/install.sh`, `scripts/install.ps1`, `scripts/install-container-agent.sh`, and `scripts/pulse-auto-update.sh` define supported deployment entry points and update behavior, even when the shell and Windows installers also sit on the shared agent-lifecycle boundary. + +`scripts/install-mcp.sh` and `scripts/install-mcp.ps1` extend the +installer family with a fourth entry point: a stdio MCP server +adapter (`cmd/pulse-mcp/`) that integrators run on their own +machine to drive Pulse from Claude Desktop, Claude Code, or other +MCP-speaking clients. The installers fetch a published +`pulse-mcp--` binary from the latest GitHub Release, +verify SHA256 against the same `checksums.txt` the rest of the +release uses, and place the binary at `~/.local/bin/pulse-mcp` +(Unix) or `$LOCALAPPDATA\pulse-mcp\pulse-mcp.exe` (Windows). The +binary takes no version ldflags because it reads the manifest +from the Pulse instance it points at. `scripts/build-release.sh` +builds `pulse-mcp` for the same multi-OS matrix as the unified +agent (linux-amd64/arm64/armv7/armv6/386, darwin-amd64/arm64, +freebsd-amd64/arm64, windows-amd64/arm64/386), packages +per-platform tarballs and zips into `RELEASE_DIR`, and the +`.github/workflows/create-release.yml` upload step attaches both +the bare binaries and the install scripts as release assets so +`https://github.com/rcourtman/Pulse/releases/latest/download/pulse-mcp--` +and `.../install-mcp.sh` are stable redirect targets the +installers consume. macOS notarization is intentionally skipped +for v1: the README documents the Gatekeeper bypass and the +install-script flow downloads the same unsigned binary, with the +audit trail of SHA256 verification preserved. That same installer boundary now owns instance identity for side-by-side server installs too: the root `install.sh`, generated update helper, and `scripts/pulse-auto-update.sh` must preserve an explicitly selected service diff --git a/docs/releases/AGENT_PARADIGM.md b/docs/releases/AGENT_PARADIGM.md index 0425ea43be..db03fe7ec6 100644 --- a/docs/releases/AGENT_PARADIGM.md +++ b/docs/releases/AGENT_PARADIGM.md @@ -24,9 +24,13 @@ Patrol or Assistant has. config snippet pre-filled with the deployment's own URL, so wiring an agent is copy, paste, and add a token. -- **Drivable from Claude.** A new `pulse-mcp` server adapter - ships in the Pulse repo (`cmd/pulse-mcp/`). Wire it into - Claude Desktop or Claude Code per +- **Drivable from Claude in one command.** A new `pulse-mcp` + server adapter ships in the Pulse repo (`cmd/pulse-mcp/`) + with a published distribution path. Install from the Pulse + GitHub Release using the one-line installer: + `curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install-mcp.sh | bash` + (or the matching `install-mcp.ps1` PowerShell installer on + Windows). Wire it into Claude Desktop or Claude Code per [`cmd/pulse-mcp/README.md`](../../cmd/pulse-mcp/README.md) and Pulse's tools appear natively. Each MCP tool is one entry in the canonical capabilities manifest; adding a capability on @@ -93,16 +97,23 @@ Patrol or Assistant has. ## What it does not do yet -- **No published distribution path for `pulse-mcp`.** The - binary builds from source today (`go build ./cmd/pulse-mcp`). - A signed release artifact (Homebrew formula, Docker image, - GitHub Release binary) is the natural next step for cutting - the install story to "one command." +- **No notarization on macOS.** The first launch of + `pulse-mcp` on macOS shows a Gatekeeper warning. The + README documents the right-click-Open or + `xattr -d com.apple.quarantine` bypass; SHA256 verification + is preserved through the installer. Notarization is a + natural follow-up if usage signal points at this as a + friction point. +- **No Homebrew tap or core formula.** The one-line installer + fetches from GitHub Releases directly. Homebrew is a layer + that can sit on top of this foundation when audience scale + warrants the ongoing maintenance. - **Real-world consumer feedback is the gating signal for - what's next.** The substrate ships with end-to-end tests - and two reference adapters, but no external integration has - been load-bearing on it yet. The next meaningful work item - is whatever friction first usage surfaces. + what's next.** The substrate ships with end-to-end tests, + two reference adapters, and a published distribution path, + but no external integration has been load-bearing on it yet. + The next meaningful work item is whatever friction first + usage surfaces. ## Audit trail diff --git a/frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx b/frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx index 3579fe39b7..4d0385d0f2 100644 --- a/frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx +++ b/frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx @@ -147,7 +147,10 @@ export const AgentIntegrationsPanel: Component = () => {

- Drop this block into{' '} + First, install pulse-mcp. The fastest path:{' '} + curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install-mcp.sh | bash{' '} + (or irm .../install-mcp.ps1 | iex on Windows). Then drop + this block into{' '} ~/Library/Application Support/Claude/claude_desktop_config.json{' '} (Claude Desktop) or your project's .mcp.json (Claude Code). Mint a token below with monitoring:read (and{' '} diff --git a/scripts/build-release.sh b/scripts/build-release.sh index 18c20fa0a5..b00c1ec944 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -147,6 +147,32 @@ for i in "${!agent_build_order[@]}"; do ./cmd/pulse-agent done +# Build pulse-mcp (Model Context Protocol adapter) for the same +# multi-OS matrix as the unified agent. The MCP server runs on +# the integrator's machine (Mac, Windows, Linux desktop) and +# speaks stdio to a local MCP client like Claude Desktop, so it +# needs the full desktop-OS matrix even though the Pulse server +# itself only ships for Linux. The binary takes no version +# ldflags: it reads the manifest from whichever Pulse instance +# it points at, so its own build identity is intentionally minimal. +echo "Building pulse-mcp for all platforms..." +mcp_build_order=("${agent_build_order[@]}") +mcp_build_envs=("${agent_build_envs[@]}") + +for i in "${!mcp_build_order[@]}"; do + target="${mcp_build_order[$i]}" + build_env="${mcp_build_envs[$i]}" + output_path="$BUILD_DIR/pulse-mcp-$target" + if [[ "$target" == windows-* ]]; then + output_path="${output_path}.exe" + fi + + env $build_env go build \ + "${release_go_build_args[@]}" \ + -o "$output_path" \ + ./cmd/pulse-mcp +done + # Build for different architectures (server + agents) build_order=(linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-386) build_envs=( @@ -324,6 +350,26 @@ zip -j "$RELEASE_DIR/pulse-agent-v${VERSION}-windows-amd64.zip" "$BUILD_DIR/puls zip -j "$RELEASE_DIR/pulse-agent-v${VERSION}-windows-arm64.zip" "$BUILD_DIR/pulse-agent-windows-arm64.exe" zip -j "$RELEASE_DIR/pulse-agent-v${VERSION}-windows-386.zip" "$BUILD_DIR/pulse-agent-windows-386.exe" +# Package standalone pulse-mcp binaries (all platforms). Mirrors +# the pulse-agent packaging shape exactly so the release-asset +# upload step does not need per-binary special cases. +# Linux +tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-linux-amd64.tar.gz" -C "$BUILD_DIR" pulse-mcp-linux-amd64 +tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-linux-arm64.tar.gz" -C "$BUILD_DIR" pulse-mcp-linux-arm64 +tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-linux-armv7.tar.gz" -C "$BUILD_DIR" pulse-mcp-linux-armv7 +tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-linux-armv6.tar.gz" -C "$BUILD_DIR" pulse-mcp-linux-armv6 +tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-linux-386.tar.gz" -C "$BUILD_DIR" pulse-mcp-linux-386 +# Darwin +tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-darwin-amd64.tar.gz" -C "$BUILD_DIR" pulse-mcp-darwin-amd64 +tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-darwin-arm64.tar.gz" -C "$BUILD_DIR" pulse-mcp-darwin-arm64 +# FreeBSD +tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-freebsd-amd64.tar.gz" -C "$BUILD_DIR" pulse-mcp-freebsd-amd64 +tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-freebsd-arm64.tar.gz" -C "$BUILD_DIR" pulse-mcp-freebsd-arm64 +# Windows (zip archives with version in filename) +zip -j "$RELEASE_DIR/pulse-mcp-v${VERSION}-windows-amd64.zip" "$BUILD_DIR/pulse-mcp-windows-amd64.exe" +zip -j "$RELEASE_DIR/pulse-mcp-v${VERSION}-windows-arm64.zip" "$BUILD_DIR/pulse-mcp-windows-arm64.exe" +zip -j "$RELEASE_DIR/pulse-mcp-v${VERSION}-windows-386.zip" "$BUILD_DIR/pulse-mcp-windows-386.exe" + # Also copy bare binaries for /releases/latest/download/ redirect compatibility # These allow LXC/barebone installs to redirect to GitHub without needing versioned URLs echo "Copying bare binaries to release directory for redirect compatibility..." @@ -338,6 +384,22 @@ cp "$BUILD_DIR/pulse-agent-windows-386.exe" "$RELEASE_DIR/" cp "$BUILD_DIR/pulse-agent-freebsd-amd64" "$RELEASE_DIR/" cp "$BUILD_DIR/pulse-agent-freebsd-arm64" "$RELEASE_DIR/" +# Copy bare pulse-mcp binaries for /releases/latest/download/ redirect +# compatibility. The install-mcp.sh installer fetches these directly from +# the GitHub Releases endpoint without needing a versioned URL. +cp "$BUILD_DIR/pulse-mcp-linux-amd64" "$RELEASE_DIR/" +cp "$BUILD_DIR/pulse-mcp-linux-arm64" "$RELEASE_DIR/" +cp "$BUILD_DIR/pulse-mcp-linux-armv7" "$RELEASE_DIR/" +cp "$BUILD_DIR/pulse-mcp-linux-armv6" "$RELEASE_DIR/" +cp "$BUILD_DIR/pulse-mcp-linux-386" "$RELEASE_DIR/" +cp "$BUILD_DIR/pulse-mcp-darwin-amd64" "$RELEASE_DIR/" +cp "$BUILD_DIR/pulse-mcp-darwin-arm64" "$RELEASE_DIR/" +cp "$BUILD_DIR/pulse-mcp-windows-amd64.exe" "$RELEASE_DIR/" +cp "$BUILD_DIR/pulse-mcp-windows-arm64.exe" "$RELEASE_DIR/" +cp "$BUILD_DIR/pulse-mcp-windows-386.exe" "$RELEASE_DIR/" +cp "$BUILD_DIR/pulse-mcp-freebsd-amd64" "$RELEASE_DIR/" +cp "$BUILD_DIR/pulse-mcp-freebsd-arm64" "$RELEASE_DIR/" + # Copy Windows, macOS, and FreeBSD binaries into universal tarball for /download/ endpoint echo "Adding Windows, macOS, and FreeBSD binaries to universal tarball..." cp "$BUILD_DIR/pulse-agent-darwin-amd64" "$universal_dir/bin/" @@ -383,6 +445,8 @@ cp "${RENDERED_INSTALLERS_DIR}/install.sh" "$RELEASE_DIR/install.sh" [ -f "${RENDERED_INSTALLERS_DIR}/install.ps1" ] && cp "${RENDERED_INSTALLERS_DIR}/install.ps1" "$RELEASE_DIR/install.ps1" cp scripts/install-docker.sh "$RELEASE_DIR/" cp scripts/pulse-auto-update.sh "$RELEASE_DIR/" +cp scripts/install-mcp.sh "$RELEASE_DIR/install-mcp.sh" +[ -f scripts/install-mcp.ps1 ] && cp scripts/install-mcp.ps1 "$RELEASE_DIR/install-mcp.ps1" pulse_release_generate_packet_sbom "${RELEASE_DIR}" "${RELEASE_PACKET_SBOM}" mapfile -t checksum_files < <(pulse_release_collect_checksum_files "${RELEASE_DIR}") diff --git a/scripts/install-mcp.ps1 b/scripts/install-mcp.ps1 new file mode 100644 index 0000000000..b4db394440 --- /dev/null +++ b/scripts/install-mcp.ps1 @@ -0,0 +1,129 @@ +# Pulse MCP Server Adapter Installer (Windows) +# +# Detects the local architecture, downloads the matching pulse-mcp.exe +# from the latest GitHub Release, verifies SHA256 against the published +# checksums file, and places the binary on PATH. +# +# Usage: +# irm https://github.com/rcourtman/Pulse/releases/latest/download/install-mcp.ps1 | iex +# +# Options (env vars): +# PULSE_MCP_VERSION Override the version to install. Default: latest. +# PULSE_MCP_BIN_DIR Where to install. Default: $env:LOCALAPPDATA\pulse-mcp. +# PULSE_MCP_REPO GitHub repo. Default: rcourtman/Pulse. +# PULSE_MCP_NO_VERIFY If "1", skip SHA256 verification (not recommended). +# +# After install, configure your MCP client per the README: +# https://github.com/rcourtman/Pulse/blob/main/cmd/pulse-mcp/README.md + +param ( + [string]$Version = $env:PULSE_MCP_VERSION, + [string]$BinDir = $env:PULSE_MCP_BIN_DIR, + [string]$Repo = $env:PULSE_MCP_REPO, + [switch]$NoVerify +) + +$ErrorActionPreference = 'Stop' + +if (-not $Version) { $Version = 'latest' } +if (-not $Repo) { $Repo = 'rcourtman/Pulse' } +if (-not $BinDir) { $BinDir = Join-Path $env:LOCALAPPDATA 'pulse-mcp' } +if ($env:PULSE_MCP_NO_VERIFY -eq '1') { $NoVerify = $true } + +function Write-Log($message) { + Write-Host "[install-mcp] $message" +} + +function Resolve-Architecture { + $arch = $env:PROCESSOR_ARCHITECTURE + switch ($arch) { + 'AMD64' { return 'amd64' } + 'ARM64' { return 'arm64' } + 'X86' { return '386' } + default { + throw "Unsupported architecture: $arch. Build from source: go install github.com/rcourtman/pulse-go-rewrite/cmd/pulse-mcp@latest" + } + } +} + +function Resolve-ReleaseBase { + if ($Version -eq 'latest') { + return "https://github.com/$Repo/releases/latest/download" + } + return "https://github.com/$Repo/releases/download/$Version" +} + +function Get-RemoteChecksum($base, $binaryName) { + $checksumsUrl = "$base/checksums.txt" + try { + $response = Invoke-WebRequest -Uri $checksumsUrl -UseBasicParsing -ErrorAction Stop + } catch { + Write-Log "warning: could not fetch checksums.txt; skipping verification" + return $null + } + foreach ($line in $response.Content -split "`n") { + $parts = $line.Trim() -split '\s+', 2 + if ($parts.Length -eq 2 -and $parts[1] -eq $binaryName) { + return $parts[0] + } + } + Write-Log "warning: $binaryName not listed in checksums.txt; skipping verification" + return $null +} + +function Main { + $arch = Resolve-Architecture + $platform = "windows-$arch" + $binaryName = "pulse-mcp-$platform.exe" + $base = Resolve-ReleaseBase + $url = "$base/$binaryName" + + Write-Log "platform: $platform" + Write-Log "install dir: $BinDir" + Write-Log "downloading: $url" + + if (-not (Test-Path $BinDir)) { + New-Item -ItemType Directory -Force -Path $BinDir | Out-Null + } + + $tmp = [System.IO.Path]::GetTempFileName() + try { + try { + Invoke-WebRequest -Uri $url -UseBasicParsing -OutFile $tmp -ErrorAction Stop + } catch { + throw "download failed: $url`nIf a release exists for this version, the binary may not yet be published for $platform.`nBuild from source: go install github.com/rcourtman/pulse-go-rewrite/cmd/pulse-mcp@latest" + } + + if (-not $NoVerify) { + $expected = Get-RemoteChecksum $base $binaryName + if ($expected) { + $actual = (Get-FileHash -Path $tmp -Algorithm SHA256).Hash.ToLower() + if ($actual -ne $expected.ToLower()) { + throw "sha256 mismatch for ${binaryName}: expected $expected, got $actual" + } + Write-Log 'sha256 verified' + } + } + + $dest = Join-Path $BinDir 'pulse-mcp.exe' + Move-Item -Path $tmp -Destination $dest -Force + Write-Log "installed: $dest" + } finally { + if (Test-Path $tmp) { Remove-Item -Force $tmp -ErrorAction SilentlyContinue } + } + + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + if (-not ($userPath -split ';' | Where-Object { $_ -eq $BinDir })) { + Write-Log "note: $BinDir is not on your user PATH. To add it, run:" + Write-Log " [Environment]::SetEnvironmentVariable('Path', `"`$BinDir;`$env:Path`", 'User')" + } + + Write-Log '' + Write-Log 'next steps:' + Write-Log ' 1. Mint a Pulse API token in Settings -> API Access (with monitoring:read,' + Write-Log ' and monitoring:write if you want the operator-state write tools).' + Write-Log ' 2. Wire pulse-mcp into your MCP client per:' + Write-Log " https://github.com/$Repo/blob/main/cmd/pulse-mcp/README.md" +} + +Main diff --git a/scripts/install-mcp.sh b/scripts/install-mcp.sh new file mode 100755 index 0000000000..fac6ebe760 --- /dev/null +++ b/scripts/install-mcp.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +# +# install-mcp.sh - Install the Pulse MCP server adapter +# +# Detects the local platform/architecture, downloads the matching +# pulse-mcp binary from the latest GitHub Release, verifies the +# SHA256 checksum against the published checksums file, and places +# the binary on PATH. +# +# Usage: +# curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install-mcp.sh | bash +# +# Options (env vars): +# PULSE_MCP_VERSION Override the version to install (e.g. "v6.0.0-rc.5"). +# Default: latest. +# PULSE_MCP_BIN_DIR Where to install the binary. +# Default: $HOME/.local/bin if writable, else /usr/local/bin. +# PULSE_MCP_REPO GitHub repo to download from. Default: rcourtman/Pulse. +# PULSE_MCP_NO_VERIFY If "1", skip SHA256 verification (not recommended). +# +# After install, configure your MCP client per the README: +# https://github.com/rcourtman/Pulse/blob/main/cmd/pulse-mcp/README.md +# +# pulse-mcp is the stdio JSON-RPC adapter that wraps Pulse's agent +# surface for Claude Desktop, Claude Code, and other MCP clients. + +set -euo pipefail + +REPO="${PULSE_MCP_REPO:-rcourtman/Pulse}" +VERSION="${PULSE_MCP_VERSION:-latest}" +NO_VERIFY="${PULSE_MCP_NO_VERIFY:-}" + +log() { + printf '[install-mcp] %s\n' "$*" +} + +err() { + printf '[install-mcp] error: %s\n' "$*" >&2 + exit 1 +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || err "missing required command: $1" +} + +detect_platform() { + local os arch + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + arch="$(uname -m)" + + case "$os" in + linux) os="linux" ;; + darwin) os="darwin" ;; + freebsd) os="freebsd" ;; + mingw*|msys*|cygwin*) + err "Windows shells must use install-mcp.ps1 instead. Run via PowerShell: + iwr https://github.com/${REPO}/releases/latest/download/install-mcp.ps1 -UseBasicParsing | iex" + ;; + *) + err "unsupported OS: $os" + ;; + esac + + case "$arch" in + x86_64|amd64) arch="amd64" ;; + aarch64|arm64) arch="arm64" ;; + armv7l|armv7) arch="armv7" ;; + armv6l|armv6) arch="armv6" ;; + i386|i686) arch="386" ;; + *) + err "unsupported architecture: $arch" + ;; + esac + + # Trim impossible combinations so we fail with a clear message + # rather than a confusing 404 from the GitHub asset endpoint. + case "${os}-${arch}" in + darwin-amd64|darwin-arm64) ;; + linux-amd64|linux-arm64|linux-armv7|linux-armv6|linux-386) ;; + freebsd-amd64|freebsd-arm64) ;; + *) + err "no published pulse-mcp binary for ${os}-${arch}; build from source via go install github.com/rcourtman/pulse-go-rewrite/cmd/pulse-mcp@latest" + ;; + esac + + printf '%s-%s' "$os" "$arch" +} + +choose_install_dir() { + if [ -n "${PULSE_MCP_BIN_DIR:-}" ]; then + printf '%s' "${PULSE_MCP_BIN_DIR}" + return + fi + local home_bin="${HOME}/.local/bin" + if [ -d "${home_bin}" ] && [ -w "${home_bin}" ]; then + printf '%s' "${home_bin}" + return + fi + if mkdir -p "${home_bin}" 2>/dev/null && [ -w "${home_bin}" ]; then + printf '%s' "${home_bin}" + return + fi + printf '%s' "/usr/local/bin" +} + +resolve_release_base() { + if [ "${VERSION}" = "latest" ]; then + printf 'https://github.com/%s/releases/latest/download' "${REPO}" + else + printf 'https://github.com/%s/releases/download/%s' "${REPO}" "${VERSION}" + fi +} + +main() { + require_cmd curl + require_cmd uname + require_cmd install + + local platform install_dir base bin_name url tmp checksums_url checksums_tmp + platform="$(detect_platform)" + install_dir="$(choose_install_dir)" + base="$(resolve_release_base)" + bin_name="pulse-mcp-${platform}" + url="${base}/${bin_name}" + + log "platform: ${platform}" + log "install dir: ${install_dir}" + log "downloading: ${url}" + + tmp="$(mktemp -t pulse-mcp.XXXXXX)" + trap 'rm -f "${tmp}"' EXIT + + if ! curl -fsSL --retry 3 "${url}" -o "${tmp}"; then + err "download failed: ${url} +If a release exists for this version, the binary may not yet be published for ${platform}. +Build from source: go install github.com/rcourtman/pulse-go-rewrite/cmd/pulse-mcp@latest" + fi + + if [ "${NO_VERIFY}" != "1" ]; then + local sha_cmd + if command -v sha256sum >/dev/null 2>&1; then + sha_cmd="sha256sum" + elif command -v shasum >/dev/null 2>&1; then + sha_cmd="shasum -a 256" + else + err "no sha256 tool found (sha256sum or shasum). Set PULSE_MCP_NO_VERIFY=1 to skip verification." + fi + + checksums_url="${base}/checksums.txt" + checksums_tmp="$(mktemp -t pulse-mcp-sums.XXXXXX)" + trap 'rm -f "${tmp}" "${checksums_tmp}"' EXIT + if ! curl -fsSL --retry 3 "${checksums_url}" -o "${checksums_tmp}"; then + log "warning: could not fetch checksums.txt; skipping verification" + else + local expected actual + expected="$(awk -v name="${bin_name}" '$2 == name {print $1; exit}' "${checksums_tmp}" || true)" + if [ -z "${expected}" ]; then + log "warning: ${bin_name} not listed in checksums.txt; skipping verification" + else + actual="$(${sha_cmd} "${tmp}" | awk '{print $1}')" + if [ "${expected}" != "${actual}" ]; then + err "sha256 mismatch for ${bin_name}: expected ${expected}, got ${actual}" + fi + log "sha256 verified" + fi + fi + fi + + install -m 0755 "${tmp}" "${install_dir}/pulse-mcp" + log "installed: ${install_dir}/pulse-mcp" + + case ":${PATH}:" in + *":${install_dir}:"*) ;; + *) + log "note: ${install_dir} is not on PATH. Add this line to your shell profile: + export PATH=\"${install_dir}:\$PATH\"" + ;; + esac + + log "" + log "next steps:" + log " 1. Mint a Pulse API token in Settings -> API Access (with monitoring:read," + log " and monitoring:write if you want the operator-state write tools)." + log " 2. Wire pulse-mcp into your MCP client per:" + log " https://github.com/${REPO}/blob/main/cmd/pulse-mcp/README.md" +} + +main "$@" diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 736580bd7a..e763511c0c 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -743,6 +743,83 @@ func TestReleaseAssetCommonRejectsUnexpectedUpdateSigningPublicKey(t *testing.T) } } +// TestBuildReleasePackagesPulseMcpForAllPlatforms pins the +// distribution path for pulse-mcp: each Pulse release must build +// the MCP adapter for the same multi-OS matrix as the unified +// agent and emit per-platform tarballs/zips, bare binaries (for +// /releases/latest/download/ redirect compatibility), and the +// install-mcp.sh script into RELEASE_DIR. Drift in any of those +// strings means an integrator following the published install +// path hits a 404 on the release endpoint instead of a working +// binary. +func TestBuildReleasePackagesPulseMcpForAllPlatforms(t *testing.T) { + content, err := os.ReadFile(repoFile("scripts", "build-release.sh")) + if err != nil { + t.Fatalf("read build-release.sh: %v", err) + } + script := string(content) + + required := []string{ + // Build loop wires through ./cmd/pulse-mcp. + `-o "$output_path" \ + ./cmd/pulse-mcp`, + // Per-platform packaging follows the pulse-agent shape + // exactly so the upload step's glob does not need + // special cases. + `tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-linux-amd64.tar.gz" -C "$BUILD_DIR" pulse-mcp-linux-amd64`, + `tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-darwin-arm64.tar.gz" -C "$BUILD_DIR" pulse-mcp-darwin-arm64`, + `zip -j "$RELEASE_DIR/pulse-mcp-v${VERSION}-windows-amd64.zip" "$BUILD_DIR/pulse-mcp-windows-amd64.exe"`, + // Bare-binary copies for the /releases/latest/download/ + // redirect that install-mcp.sh fetches by default. + `cp "$BUILD_DIR/pulse-mcp-linux-amd64" "$RELEASE_DIR/"`, + `cp "$BUILD_DIR/pulse-mcp-darwin-amd64" "$RELEASE_DIR/"`, + `cp "$BUILD_DIR/pulse-mcp-darwin-arm64" "$RELEASE_DIR/"`, + `cp "$BUILD_DIR/pulse-mcp-windows-amd64.exe" "$RELEASE_DIR/"`, + // The installer scripts themselves must reach + // RELEASE_DIR so the GitHub Releases asset upload can + // publish them as the canonical curl-pipe-bash entry + // point. + `cp scripts/install-mcp.sh "$RELEASE_DIR/install-mcp.sh"`, + } + for _, needle := range required { + if !strings.Contains(script, needle) { + t.Fatalf("build-release.sh missing pulse-mcp distribution wiring: %s", needle) + } + } + + // install-mcp.sh and install-mcp.ps1 must both exist as + // shipped scripts; the build pipeline references them, so + // missing-file drift breaks release builds rather than + // quietly ships an installer that 404s. + if _, err := os.Stat(repoFile("scripts", "install-mcp.sh")); err != nil { + t.Fatalf("scripts/install-mcp.sh missing: %v", err) + } + if _, err := os.Stat(repoFile("scripts", "install-mcp.ps1")); err != nil { + t.Fatalf("scripts/install-mcp.ps1 missing: %v", err) + } + + // install-mcp.sh's install-dir resolution and SHA256 + // verification are load-bearing: dropping either silently + // turns the installer into "curl | bash with no integrity + // check," which is the failure mode the hook is here to + // prevent. Pin the touchstones. + mcpScript, err := os.ReadFile(repoFile("scripts", "install-mcp.sh")) + if err != nil { + t.Fatalf("read install-mcp.sh: %v", err) + } + for _, needle := range []string{ + `detect_platform()`, + `choose_install_dir()`, + `PULSE_MCP_NO_VERIFY`, + `checksums.txt`, + `sha256 mismatch`, + } { + if !strings.Contains(string(mcpScript), needle) { + t.Fatalf("install-mcp.sh missing required helper or guard: %s", needle) + } + } +} + func repoFile(parts ...string) string { root := filepath.Join("..", "..") segments := append([]string{root}, parts...) diff --git a/scripts/release_control/render_release_body_test.py b/scripts/release_control/render_release_body_test.py index 3d55873a86..a63a327025 100644 --- a/scripts/release_control/render_release_body_test.py +++ b/scripts/release_control/render_release_body_test.py @@ -165,6 +165,33 @@ Old metadata section. self.assertIn("live auth-env watcher teardown", release_notes) self.assertIn("join live config watcher goroutines", changelog) + def test_agent_paradigm_release_notes_blurb_documents_distribution_path(self) -> None: + """The agent-paradigm source draft must keep its honest scope: + an integrator reading the blurb sees a published distribution + path (the install-mcp script + GitHub Release binaries) when + the work lands, not the earlier "build from source" wording. + + Pin the blurb's stable touchstones so a future edit that + accidentally regresses the install story (e.g. swaps the + one-line installer for "clone the repo" again) fails this + test instead of shipping silently into a release. + """ + repo_root = Path(__file__).resolve().parents[2] + blurb = (repo_root / "docs/releases/AGENT_PARADIGM.md").read_text(encoding="utf-8") + + self.assertIn("install-mcp.sh", blurb, "blurb must reference the published install script") + self.assertIn("/api/agent/capabilities", blurb) + self.assertIn("cmd/pulse-mcp", blurb) + self.assertIn("cmd/agent-probe", blurb) + # The four-axis frame is the substrate's load-bearing claim; + # if any axis name drifts in the blurb, agents reading + # release notes will look for a different surface than what + # ships. + self.assertIn("Discovery", blurb) + self.assertIn("Read", blurb) + self.assertIn("Write", blurb) + self.assertIn("Push", blurb) + if __name__ == "__main__": unittest.main()