mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 04:06:21 +00:00
3da835c5bc
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.
282 lines
10 KiB
Markdown
282 lines
10 KiB
Markdown
# pulse-mcp
|
|
|
|
`pulse-mcp` is a Model Context Protocol (MCP) server that wraps Pulse's
|
|
agent surface as a tool set. It lets Claude Desktop, Claude Code, and any
|
|
other MCP-speaking client drive Pulse natively: list findings, read the
|
|
fleet, drill into a resource, set operator intent, run governed actions.
|
|
|
|
The adapter is manifest-driven. Every tool it exposes is one entry in
|
|
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.
|
|
|
|
## Install
|
|
|
|
Three ways to get the binary, in order of friction.
|
|
|
|
### 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
|
|
```
|
|
|
|
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
|
|
`monitoring:write` if you want the write tools (`set_operator_state`,
|
|
`clear_operator_state`) to work. Mint one in **Settings → Security → API
|
|
Tokens**.
|
|
|
|
### 3. Wire it into your client
|
|
|
|
#### Claude Desktop
|
|
|
|
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
(macOS) or the platform equivalent. Add a `pulse` entry under
|
|
`mcpServers`:
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"pulse": {
|
|
"command": "/usr/local/bin/pulse-mcp",
|
|
"args": ["--base-url", "http://localhost:7655"],
|
|
"env": {
|
|
"PULSE_API_TOKEN": "your-token-here"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Restart Claude Desktop. The Pulse tools appear in the tool picker.
|
|
|
|
#### Claude Code
|
|
|
|
Add the server to your project's `.mcp.json` (or your user-level config):
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"pulse": {
|
|
"command": "pulse-mcp",
|
|
"args": ["--base-url", "http://localhost:7655"],
|
|
"env": {
|
|
"PULSE_API_TOKEN": "your-token-here"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Configuration
|
|
|
|
| Flag | Default | Purpose |
|
|
| ---- | ------- | ------- |
|
|
| `--base-url` | `http://localhost:7655` | Pulse instance to talk to |
|
|
| `--token-env` | `PULSE_API_TOKEN` | Env var holding the API token |
|
|
| `--emit-notifications` | `false` | Translate Pulse SSE events into JSON-RPC notifications on stdout |
|
|
|
|
The token is always read from an environment variable, never a flag, so
|
|
it does not appear in process listings.
|
|
|
|
### About `--emit-notifications`
|
|
|
|
When enabled, the server opens a long-lived connection to
|
|
`/api/agent/events` after `initialize` and writes a JSON-RPC
|
|
notification on stdout for every non-transport event:
|
|
|
|
```json
|
|
{ "jsonrpc": "2.0", "method": "notifications/finding.created", "params": { ... } }
|
|
{ "jsonrpc": "2.0", "method": "notifications/approval.pending", "params": { ... } }
|
|
{ "jsonrpc": "2.0", "method": "notifications/action.completed", "params": { ... } }
|
|
```
|
|
|
|
The `params` object is the SSE event's `data` payload verbatim, so an
|
|
agent that already knows the substrate's wire shape sees identical
|
|
content to what an HTTP SSE consumer would. Transport plumbing
|
|
(`stream.connected`, `heartbeat`) is filtered out.
|
|
|
|
It is off by default because not every MCP client surfaces
|
|
server-initiated notifications. Enable it when wiring an autonomous
|
|
agent that processes the JSON-RPC stream programmatically. Claude
|
|
Desktop today does not surface arbitrary `notifications/*` methods
|
|
in the chat UI; if your client falls in that category, leave the
|
|
flag off and consume the SSE stream directly.
|
|
|
|
When `--emit-notifications` is on, the `initialize` response
|
|
advertises the supported event kinds under
|
|
`capabilities.experimental.pulseNotifications.kinds`. Clients that
|
|
don't understand the experimental block ignore it silently.
|
|
|
|
## What the tools do
|
|
|
|
The exact set is whatever your Pulse instance's manifest declares. As of
|
|
this writing, the published capabilities are:
|
|
|
|
**Context (read-only):**
|
|
|
|
- `get_resource_context` returns the situated picture of one resource:
|
|
identity, operator-set state, active findings, pending approvals,
|
|
recent actions including refused dispatches with their stable error
|
|
tokens.
|
|
- `get_fleet_context` returns a thin per-resource rollup across the org
|
|
for triage: identity, operator flags, per-severity finding counts,
|
|
pending-approval count.
|
|
|
|
**Operator state (per-resource intent):**
|
|
|
|
- `get_operator_state` reads the operator-set state for a resource
|
|
(intentionally offline, never-auto-remediate, maintenance window,
|
|
criticality).
|
|
- `set_operator_state` replaces the entire record. The server populates
|
|
attribution (`setAt`, `setBy`) so client values cannot spoof it.
|
|
- `clear_operator_state` removes the entry. Idempotent.
|
|
|
|
**Findings (Patrol lifecycle):**
|
|
|
|
- `list_findings` returns every Patrol finding (active, dismissed,
|
|
resolved). Filter client-side.
|
|
- `acknowledge_finding` marks a finding as seen but keeps it visible.
|
|
- `snooze_finding` hides a finding for a duration in hours.
|
|
- `dismiss_finding` permanently dismisses a finding with a reason
|
|
(`not_an_issue`, `expected_behavior`, `will_fix_later`).
|
|
- `resolve_finding` manually marks a finding resolved.
|
|
|
|
Run `tools/list` from your MCP client to see the live set.
|
|
|
|
## Stable error envelope
|
|
|
|
Every Pulse error reaches your agent verbatim, in this shape:
|
|
|
|
```json
|
|
{ "error": "<stable_code>", "message": "<human readable>" }
|
|
```
|
|
|
|
The MCP tool result wraps that JSON in a text content block with
|
|
`isError: true`. Branch on `error` (snake_case stable codes); use
|
|
`message` for surfacing to humans, never for branching.
|
|
|
|
Capability-specific codes the substrate currently emits include
|
|
`resource_not_found` (depth read on an unknown id),
|
|
`operator_state_not_set` (read on a resource with no operator entry), and
|
|
`operator_state_invalid` (write rejected by the validator). Cross-cutting
|
|
codes from the auth / multi-tenant middleware (`invalid_org`,
|
|
`org_suspended`, `access_denied`) can apply to any authenticated tool
|
|
call.
|
|
|
|
## Known limitations
|
|
|
|
- **No `subscribe_events` tool.** SSE streaming does not fit the MCP
|
|
request/response tool shape, so the adapter does not expose the
|
|
`/api/agent/events` stream as a callable tool. Agents that want
|
|
real-time push have two options: consume the SSE stream directly
|
|
via HTTP (works with any MCP client), or run with
|
|
`--emit-notifications` so the bridge translates SSE events into
|
|
JSON-RPC notifications on the stdio channel (requires a client
|
|
that processes server-initiated notifications).
|
|
|
|
- **Manifest is fetched once.** The server fetches `/api/agent/
|
|
capabilities` at startup and does not refresh during the process
|
|
lifetime. Restart `pulse-mcp` to pick up new capabilities after
|
|
upgrading Pulse.
|
|
|
|
- **No resource URIs.** MCP supports a `resources/` channel in addition
|
|
to `tools/`. The adapter exposes only tools today; this is sufficient
|
|
for the substrate's current surface and keeps the adapter small.
|
|
|
|
## Troubleshooting
|
|
|
|
**"env var PULSE_API_TOKEN is empty" on startup.**
|
|
The adapter refuses to start without a token. Mint one in Settings and
|
|
make sure your client's `env` block (or shell environment) sets
|
|
`PULSE_API_TOKEN`.
|
|
|
|
**"manifest GET returned 401" on startup.**
|
|
Discovery is supposed to be unauthenticated. If your Pulse instance is
|
|
behind a reverse proxy that adds auth in front of the public paths,
|
|
the proxy is gating the manifest endpoint. Make sure
|
|
`/api/agent/capabilities` is reachable without a credential, the same
|
|
way `/api/health` is.
|
|
|
|
**Tools work, but `set_operator_state` returns 403 access_denied.**
|
|
Your token is missing `monitoring:write`. Either mint a new token with
|
|
both scopes, or restrict yourself to the read tools.
|
|
|
|
**Tools list is empty.**
|
|
The adapter filters `subscribe_events` out (it is not request/response
|
|
shaped). If literally nothing else shows up, your Pulse instance's
|
|
manifest is empty, which is a Pulse-side bug; check
|
|
`curl http://your-pulse/api/agent/capabilities` directly.
|
|
|
|
## Implementation notes
|
|
|
|
The adapter is one stdlib-only Go file (`main.go`) in around 430 lines.
|
|
It speaks JSON-RPC 2.0 over stdio with line-delimited framing, logs to
|
|
stderr to keep the JSON-RPC channel on stdout clean, and derives each
|
|
tool's input schema from the manifest entry: `{name}` segments in the
|
|
declared path become required string properties, and non-GET/DELETE
|
|
tools accept a free-form `body` object.
|
|
|
|
The companion worked example, `cmd/agent-probe`, walks the same
|
|
substrate as a plain HTTP client and is a useful reference for anyone
|
|
building a non-MCP integration. Together the two binaries demonstrate
|
|
the substrate's two consumer profiles: stdio MCP and HTTP API. Both are
|
|
manifest-driven and stdlib-only, so adding capabilities to Pulse extends
|
|
both without code changes here.
|