Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MCP Integration

Audience: operators wiring Pulse into an MCP-aware AI client (Claude Desktop, Claude Code, Cursor, Zed, custom hosts), and embedders who want to expose Pulse to an LLM agent.

This page is the human-facing guide: what the server does, how to wire it up, what the LLM sees, and how to debug a misbehaving session. Agent-facing guidance ships inside the binary as the mcp-integration skill — fetch it via pulse_skills_get (or pulse skills show mcp-integration).

What pulse mcp is

pulse mcp runs the Pulse library as a Model Context Protocol (MCP) server. The host (Claude Desktop, Claude Code, etc.) launches it as a subprocess, speaks JSON-RPC over its stdio streams, and shuts it down on session close. The LLM sees Pulse as a set of tools (callable functions), resources (browseable URIs), and prompts (canned slash commands).

┌─────────────┐  stdio JSON-RPC  ┌────────────┐  Go calls  ┌─────────────┐
│  AI client  │ ───────────────→ │ pulse mcp  │ ─────────→ │ pulse.Pulse │
│   (host)    │ ←─────────────── │ (this bin) │ ←───────── │  (library)  │
└─────────────┘                  └────────────┘            └─────────────┘
                                       │
                                       └── stderr ─→ host log pane

The server is a thin translator. Every tool wraps a public method on pulse.Pulse; the same code path powers the CLI.

Quickstart

# 1. Build and place on PATH
make build && cp ./bin/pulse /usr/local/bin/

# 2. Pick a data directory
mkdir -p /var/data/pulse

# 3. Wire into your host (see below) and restart it

# 4. From the LLM session, call:
#    pulse_manifest      → cache once
#    pulse_ask           → run analyses

Wiring into a host

Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "pulse": {
      "command": "/usr/local/bin/pulse",
      "args": ["mcp"],
      "env": {
        "PULSE_DATA_DIR": "/var/data/pulse"
      }
    }
  }
}

Restart Claude Desktop. Pulse tools appear in the tool picker.

Claude Code

claude mcp add pulse --env PULSE_DATA_DIR=/var/data/pulse -- pulse mcp

Or by hand in ~/.claude.json (or per-project .claude.json):

{
  "mcpServers": {
    "pulse": {
      "command": "/usr/local/bin/pulse",
      "args":    ["mcp"],
      "env":     { "PULSE_DATA_DIR": "/var/data/pulse" }
    }
  }
}

Cursor / Zed / generic stdio hosts

Any host that speaks the MCP stdio transport can launch pulse mcp the same way — provide the binary path, the mcp argument, and the PULSE_DATA_DIR env var.

What the LLM sees

Tool surface

Sixteen tools, registered at server start. Names and order match internal/mcp/mcptools/meta.go.

ToolPurpose
pulse_manifestCall first. Self-description: commands, operators (with accepted types + streamability), tier-1/tier-2 tests, regressions, synth distributions, error code list, MCP tool list, cohort field types with operator cross-references. Cache once per session.
pulse_askPreferred entry point. One-shot: optional auto-import → inspect → predict → execute. Accepts source (raw file path) + query (natural language, beta) or a structured request.
pulse_inspectRead .pulse header + schema (no record bytes). Side effect: registers session-scoped schema-bound tool variants (see below).
pulse_predictValidate a request against the schema without executing. Returns errors, warnings, applied defaults, streamability reasons.
pulse_processExecute one pre-built request.
pulse_composeExecute a batch of requests against the same cohort in one round trip.
pulse_sampleReturn up to N rows for preview / diagnostics.
pulse_facetDistinct values for a single field.
pulse_importConvert a tabular source (csv, tsv, ndjson, jsonarray, parquet, arrow, excel) into a managed .pulse handle under imports/, with TTL-tracked sidecar. Pulse-format inputs pass through.
pulse_dropDelete a managed-import handle and its sidecar.
pulse_imports_listEnumerate managed handles with sidecar metadata (source, format, imported_at, expires_at, ttl, expired flag, pinned flag).
pulse_examples_searchSearch the embedded request-example library by query, taxonomy tags (ANDed), or category.
pulse_examples_getFetch one runnable example body by name.
pulse_errors_lookupPer-code Message + Fixup detail (kept out of the manifest for context economy).
pulse_skills_listEmbedded skill metadata.
pulse_skills_getFetch one skill body by name.

Natural-language query is beta. Heuristic parsing only — silent misinterpretation is possible. The LLM should always check the query_resolution and resolved request in the response before trusting results. For production, author a structured request against the cached manifest and skip the query field.

Resources

URI schemeYields
pulse://<path>One resource per .pulse file under the data directory. Read returns descriptor.InspectResult JSON (header + schema only — no record bytes).
pulse-skill://<name>One per embedded skill. Read returns the markdown body.

Resources are registered once at server start. Files added afterwards do not appear until the server restarts. Listing is cheap because the server only reads header bytes.

Prompts

NameArgsReturns
pulse-bootstrapnoneA short instructions block telling the assistant what to call (and in what order) before authoring any request, and where the authoritative references live. Inject at session start.
pulse-author-requestquestionA guided tool-call sequence for translating a natural-language analytical question into a Pulse request: manifest → examples search → ask.

Hosts that surface prompts as slash commands let users trigger these directly.

The two-call default for nearly every user request:

  1. pulse_manifest once at session start. No arguments. Cache the payload — it is deterministic for a binary version and carries every fact needed to author a valid request.

  2. pulse_ask for everything else. It collapses import + inspect + predict + execute into one round trip. When the user hands the LLM a raw file:

    {
      "request": "{\"source\":\"data.csv\",\"query\":\"average revenue by month\"}"
    }
    

    When the cohort already exists as a managed handle or .pulse file:

    {
      "request": "{\"cohort\":{\"filename\":\"sales.pulse\"},\"query\":\"top 5 regions by revenue\"}"
    }
    

    On predict-invalid with on_invalid="suggest", the response carries structured Fixup entries derived from each error code’s metadata so the LLM can repair the request without another round trip.

Reach for the multi-step path (pulse_inspectpulse_predictpulse_process) only when:

  • diagnosing a failed predict and you want the full envelope,
  • previewing rows (pulse_sample) or value distributions (pulse_facet),
  • pre-staging a managed handle with a specific name / TTL / pinning (pulse_import),
  • batching multiple requests in one call (pulse_compose).

Managed imports + TTL

pulse_import lets the LLM hand the server any tabular file and address it from then on as if it were a .pulse.

  • Convertible formats (csv, tsv, ndjson, jsonarray, parquet, arrow, excel) are imported into $PULSE_DATA_DIR/imports/<handle>.pulse with a sidecar <handle>.pulse.meta.json carrying imported_at, expires_at, ttl_seconds, source path, source format, and row count. result.managed=true.
  • Pulse passthroughs (.pulse extension) under PULSE_DATA_DIR are not copied — the server returns the relative path verbatim with managed=false. A .pulse outside PULSE_DATA_DIR is copied into the managed pool.

Source path resolution. Relative source paths resolve against PULSE_DATA_DIR. Absolute paths read from the host filesystem through a separate “source fs.”

Import jail. Absolute source paths are confined to a single directory tree (the jail root). Default: the working directory the MCP server was launched from. Paths that escape the jail (including ..) return PULSE_IMPORT_SOURCE_FORBIDDEN. Override via pulse.Options.ImportSourceJailRoot when embedding.

Sliding TTL. Default lifetime is 7d (overridable via PULSE_IMPORT_TTL, or per-import via the ttl field — accepts Go duration like "24h", day form like "7d", or "pin" for never-expire). Every subsequent inspect/predict/process/sample/facet/ask against the handle slides expires_at forward. The pool self-sweeps on every pulse_import call — no daemon required. Inspect with pulse_imports_list; evict manually with pulse_drop.

Schema-bound enums

After a successful pulse_inspect (or after pulse_ask opens a cohort), the server registers session-scoped variants of the action tools (pulse_process, pulse_predict, pulse_compose, pulse_sample, pulse_facet) whose JSON Schemas embed enum constraints on field-name parameters. The LLM picks field names from a typed list rather than free-texting and discovering on predict that the name was wrong.

What gets constrained on bound pulse_process / pulse_predict / pulse_compose schemas:

PathEnum
aggregations[].fieldAll cohort field names
aggregations[].typeFull aggregator catalogue (AGG_*)
attributes[].fieldNumeric fields only (includes decimal)
attributes[].typeFull attribute catalogue (ATTR_*)
filterers[].fieldAll cohort field names
filterers[].typeFull filterer catalogue (FILTER_*)
groups[].fieldAll cohort field names
groups[].typeFull grouper catalogue (GROUP_*)
windows[].field, windows[].partition_by[]All cohort field names
windows[].order_by[].fieldNumeric and date fields
windows[].typeFull window catalogue (WIN_*)
tests[].field, tests[].field2Numeric fields only
tests[].split_by / rows / cols / subject_fieldAll cohort field names
tests[].typeFull test catalogue (TEST_*)
pulse_facet field argAll cohort field names

Trigger and lifecycle. Binding fires on a successful pulse_inspect. mcp-go auto-fires notifications/tools/list_changed on AddSessionTools; the host refreshes its tool list and picks up the bound schemas on the next list. Bound tools share names with the global tools — session-scoped variants override globals for that session.

Limitations.

  • Multi-file sessions: the latest inspect wins. Track multiple cohorts client-side.
  • No per-element type ↔ field correlation: JSON Schema can’t easily express “if aggregations[i].type == AGG_SUM then aggregations[i].field must be numeric.” Operator–type compatibility lives in the type property description; strict validation remains pulse_predict’s job.
  • Transport support: binding requires a session that implements SessionWithTools. SSE / Streamable HTTP transports work; on stdio, binding is a no-op fallback and the global (unbound) schemas remain in effect. The manifest’s accepts_types table is still authoritative, so authoring is not blocked — just less ergonomic.
  • Empty enums omitted: when the cohort has zero fields in a category (e.g. no geo fields), the enum is omitted entirely rather than emitted as [].

Disable binding entirely with --bind-on-open=false.

Configuration

Env varPurposeDefault
PULSE_DATA_DIRCohort base directory. Required.(none — server fails to start without it)
PULSE_IMPORTS_DIRSubdirectory for managed-import handles.imports
PULSE_IMPORT_TTLDefault TTL for managed handles. Accepts Go duration (24h, 30m), day form (7d, 30d), or pin.7d

Embedders can override per-instance via pulse.Options{DataDir, ImportsDir, ImportTTL, ImportSourceJailRoot, FS, ImportSourceFS, BindOnOpen} — see pulse.go.

Transport caveats

  • Stdio. The default and only transport pulse mcp ships today. Schema binding is a no-op (see Limitations). Stdout is the JSON-RPC channel; stderr is the log channel — never write structured output to stdout outside the protocol.
  • SSE / Streamable HTTP. Not exposed by the mcp CLI leaf yet. The underlying mcp-go server supports them; embedders can call mcp.NewWithOptions(p, ...) and serve via mcp-go’s SSE / streamable HTTP entry points directly.

Troubleshooting

SymptomCauseFix
data directory required: set PULSE_DATA_DIR or pass --data-dirNeither env var nor flag setPass PULSE_DATA_DIR in the host’s env block, or --data-dir in args
Tools don’t appear in the host UI after editing configHost caches tool listRestart the host fully (not just the conversation)
pulse_import returns PULSE_IMPORT_SOURCE_FORBIDDEN for an absolute pathPath escapes the import jail (default = server’s working dir)Either move the file under the jail, launch the server from a higher-level directory, or set pulse.Options.ImportSourceJailRoot when embedding
pulse_inspect succeeds but bound enums never fireStdio session — binding is a no-op thereUse pulse_predict for validation; the manifest’s accepts_types lists give the LLM the same information
Tool calls hangHost wrote non-protocol bytes to the server’s stdin, or server wrote non-protocol bytes to stdoutCheck server stderr; restart the session. pulse mcp itself only writes a one-line startup notice to stderr at boot
pulse_ask with query returns nonsense or wrong fieldsNatural-language parsing is heuristic and betaInspect query_resolution in the response. For production, author a structured request against the cached manifest

To see what the server registers without launching the host:

pulse --json | jq '.data.mcp_tools[]'
pulse manifest --json | jq '.data.skills[]'

Skill cross-reference for LLM agents

If you are writing a system prompt for an LLM agent that uses Pulse, point it at these skills rather than at this site:

LLM taskSkill
MCP wiring, tool surface, schema bindingmcp-integration
Author a Process requestrequest-recipes
Compose multiple sub-requests in one callcompose-requests
Iterate on a request with pulse_predictdebugging-with-predict
Look up an error code or warningerror-code-reference
Pick an aggregator / filtereraggregation-guide
Pick an attribute (z-score, percentile, formula, …)attribute-composition
Design a groupergrouper-design
Use a window operator (WIN_*)window-operations
Use a feature engineer (FEAT_*)feature-engineering
Run a statistical test (tier-1 or tier-2)statistical-testing
Fit a regression (OLS, GLM, Bayesian)regression-modeling
Generate synthetic datasynthetic-data
Understand a cohort’s schema layoutcohort-schema-design
Import a tabular source into .pulseimport-best-practices
Pick an export formatexport-format-selection
Work with decimal128 (currency, precise arithmetic)financial-cohorts
Route a natural-language query to a Pulse requestquery-router-prompt
Get started end-to-end (LLM walkthrough)getting-started

The agent should call pulse_skills_list once at session start to enumerate the catalog, then pulse_skills_get on demand. The returned text is authoritative; this site does not duplicate it and may lag.