From 3a502fefda696372764a70a1db38511d832bba6a Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 9 May 2026 23:11:54 +0100 Subject: [PATCH] Publish the cmd/pulse-mcp integration guide Slice 51 added the MCP adapter as a worked example. This makes it a published surface: an external maintainer who wants to wire Pulse into their Claude Desktop or Claude Code can read one README and have it working without spelunking through main.go. The guide carries: - Build and install instructions - Canonical config snippets for Claude Desktop and Claude Code - The env-var contract (PULSE_API_TOKEN, configurable name, always read from env so it stays out of process listings) - The published tool list grouped by category (context, operator-state, finding) with what each does - The stable error envelope shape and the difference between capability-specific codes and cross-cutting auth codes - Documented limitations: no subscribe_events, manifest fetched once, tools-only (no resource URIs) - Troubleshooting for the common failure modes (missing token, proxy gating discovery, missing write scope) api-contracts.md now points readers at the README as the canonical integration entry point so the contract doc keeps its in-repo focus and the README owns the user-facing copy. --- cmd/pulse-mcp/README.md | 197 ++++++++++++++++++ .../v6/internal/subsystems/api-contracts.md | 9 + 2 files changed, 206 insertions(+) create mode 100644 cmd/pulse-mcp/README.md diff --git a/cmd/pulse-mcp/README.md b/cmd/pulse-mcp/README.md new file mode 100644 index 000000000..f4a28627c --- /dev/null +++ b/cmd/pulse-mcp/README.md @@ -0,0 +1,197 @@ +# 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. + +## Quick start + +### 1. Build + +From the repo root: + +```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). + +### 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 | + +The token is always read from an environment variable, never a flag, so +it does not appear in process listings. + +## 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": "", "message": "" } +``` + +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`.** SSE streaming does not fit the MCP + request/response tool shape, so the adapter does not expose the + `/api/agent/events` stream. Agents that need real-time push (finding + created, approval pending, action completed) consume the SSE stream + directly via HTTP. A future version may layer this onto MCP + 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. diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 50a707b6a..8c5391e45 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -1517,6 +1517,15 @@ substrate's stable error envelope (`{"error": "code", "message": "..."}`) verbatim through MCP's content-and-isError result so agents on the MCP side branch on the same stable codes. +The integration guide for `cmd/pulse-mcp` lives at +`cmd/pulse-mcp/README.md`. It carries the canonical Claude Desktop +and Claude Code config snippets, the env-var contract for the API +token, the published list of tools the manifest currently +exposes, and the documented limitations (no `subscribe_events`, +manifest fetched once at startup). External maintainers wiring +Pulse into their MCP-speaking client read that document, not the +package source. + `/api/agent/resource-context/{id}` is the agent-consumable bundled context endpoint. One read returns the full situated picture of a resource — identity, operator-set state (with server-computed