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 session-bootstrap skill — fetch it via pulse_skills_get (or pulse skills show session-bootstrap).

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_inspect       → open a cohort
#    pulse_predict       → validate a request
#    pulse_process       → execute

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

The core tools below are registered at server start (the manifest’s mcp_tools is the authoritative list for an instance). Names and order match internal/mcp/toolmeta/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_inspectRead .pulse header + schema (no record bytes). Carries an additive warnings array beside the result — a payload length that is not a whole multiple of the record stride floors record_count and says so there. 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 internal/imports/, with TTL-tracked sidecar. Pulse-format inputs pass through.
pulse_dedupDeduplicate an existing single-file cohort into parent groups (format 0x02), in place or to out; suggest_groups alone is read-only candidate detection.
pulse_materializeWrite a request’s derived columns (features, attributes, windows) as ordinary fields of a NEW cohort at out — {request, out, overwrite}, the request nested — so a later request can regress, group, filter or join on them. Returns target, records, derived_columns, warnings.
pulse_recommendTurn a question kind (an intent ID) into ranked draft requests: <placeholder> skeletons without cohort, drafts bound to the cohort’s fields and predict-validated with it. Non-analytic intents return routes_to instead.
pulse_explainSay in plain words what a request will do (never run; predict-checked when it names a cohort) or what a result found: findings with closed verdicts read at the result’s own alpha, strength bands with their convention, and caveats. Pass a result exactly as returned, with its request beside it.
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; the optional intent filter returns that intent’s skills.
pulse_skills_getFetch one skill body by name.

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). The per-file entries are produced by a startup scan the operator can disable (pulse mcp --no-cohort-scan); a read by URI resolves either way.
pulse-skill://<name>One per skill — every embedded skill plus the two rendered from Go registries, glossary and intents. 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-requestquestionClassifies the question against the intents (label + example phrasings) and hands it to the matching mounted pulse-<intent> prompt, or — for prepare and lookup — straight to the mounted route tool (pulse_import / pulse_dedup, pulse_lookup; simulate has no MCP tool). It lists only what this server mounts. Otherwise a guided tool-call sequence: manifest → examples search → inspect → predict → process.
pulse-describecohort, measureThe guided intent workflow for describe (below).
pulse-compare-groupscohort, outcome, groupThe guided intent workflow for compare_groups (below).
pulse-relationshipcohort, measures, categoriesThe guided intent workflow for relationship (below).
pulse-driverscohort, outcome, predictorsThe guided intent workflow for drivers (below).
pulse-change-over-timecohort, time, measureThe guided intent workflow for change_over_time (below).
pulse-compositioncohort, category, byThe guided intent workflow for composition (below).
pulse-benchmarkcohort, measure, groupThe guided intent workflow for benchmark (below).
pulse-distribution-shapecohort, measureThe guided intent workflow for distribution_shape (below).
pulse-segmentcohort, measuresThe guided intent workflow for segment (below).
pulse-measure-constructcohort, itemsThe guided intent workflow for measure_construct (below).
pulse-flowscohort, from, toThe guided intent workflow for flows (below).
pulse-data-qualitycohort, fieldsThe guided intent workflow for data_quality (below).

Intent prompts. One pulse-<intent> prompt per analytic intent (the twelve above; the tooling intents prepare, simulate and lookup route to tools and get none), generated from the intent registry. Every argument is optional: cohort plus one field hint per role of the intent’s shapes, comma-separated field names. All share one script: pulse_inspect the cohort → pulse_recommend {intent, fields} (the role arguments become fields hints) → pulse_explain the draft request and confirm it with the user → pulse_process → pulse_explain the response. Under a feature profile each prompt is its own mcp_extra:prompt_<intent> feature (it needs capability:recommend, capability:explain and capability:process), and it is not mounted when every operator serving its intent is hidden.

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

The default sequence 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_import when the user hands the LLM a raw tabular file (CSV/TSV/NDJSON/JSONArray/Parquet/Arrow/Excel). Returns a managed handle that subsequent tools address as if it were a .pulse. Skip when the cohort already exists as a managed handle or .pulse file in PULSE_DATA_DIR.
  3. pulse_inspect on the handle (or path). Reads header + schema only — no record bytes — and registers session-scoped, schema-bound variants of the action tools (see below).
  4. pulse_predict with the authored request. Validates against the schema, surfaces applied defaults, streamability, and any structural errors / warnings. Each error code carries Fixup metadata so the LLM can repair the request without another round trip.
  5. pulse_process to execute. Use pulse_compose to batch multiple requests against the same cohort in one round trip.

Cheaper probes are available without going through pulse_process:

  • pulse_sample for a row preview.
  • pulse_facet for distinct values of a single field.
  • pulse_examples_search / pulse_examples_get to crib a runnable request template from the embedded library.

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 — plus source_tz / column_source_tz / dst_policy when the import read naive datetimes in a source zone (the pulse_import fields of the same names; omitted for a UTC import). 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 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, 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_*)
attributes[].target, attributes[].predictors[]Numeric fields only (regression attributes)
regressions[].target, regressions[].predictors[]Numeric fields only
regressions[].typeFull regression catalogue (REG_*)
matrices[].typeFull matrix catalogue (MAT_*)
joins[].on[].left_fieldAll cohort field names (the right cohort is not bound)
pulse_facet field argAll cohort field names

Beyond the enums, a bound schema carries every key the payload schema declares on the request roots it describes (Request, ComposedRequest, ChainRequest, FacetRequest, and pulse_predict’s alternative roots), nested slot objects included, and no key the instance’s payload schema lacks. The only deliberate omissions are a test’s multiplicity.alpha (the engine refuses it: a test reads its own alpha), a facet overlay’s weight (facets are never weighted) and the labels slot on an instance offering no label table. TestBindForInstance_PayloadSchemaParity holds the two in step, profile-free and under feature profiles.

Items that fan out (for_each). On a features[], attributes[] or windows[] item that carries a for_each, every enum-bound string above (the operator type, field, partition_by[], order_by[].field, a regression attribute’s target / predictors[], the window frame.mode, a weight field) also accepts an axis placeholder: the property is {"type": "string", "anyOf": [{"enum": [...]}, {"pattern": "\\{\\{\\s*[A-Za-z_][A-Za-z0-9_]*\\s*\\}\\}"}]}, and the item carries "if": {"required": ["for_each"]} with an else that forbids the placeholder form at the same sites — so an item without for_each keeps the strict enum (a typo is still rejected) and a {"field": "{{ch}}_spend", "for_each": …} validates. Plain draft 2020-12 keywords, pinned against a real validator by TestBind_ForEach* (internal/mcp). An instance hiding capability:for_each binds neither the key nor the placeholder branch. A reader looking for a property’s enum finds it at enum or, on these items, as the first anyOf branch.

Trigger and lifecycle. Binding fires on a successful pulse_inspect. The go-sdk server auto-fires notifications/tools/list_changed on the post-serve AddTool / RemoveTools swap; the host refreshes its tool list and picks up the bound schemas on the next list. Bound tools share names with the global tools — the 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 mutates the single stdio-session server post-serve (AddTool / RemoveTools auto-emit list_changed), so stdio — the transport pulse mcp ships — honours it. There is no per-session schema override for a shared HTTP server, a documented limitation. The manifest’s accepts_types table is still authoritative, so authoring is never blocked regardless of transport.
  • 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.

Lean responses by default

pulse_process, pulse_predict, every pulse_compose slot and every pulse_process_chain stage shape a request WITHOUT its own return block by the standard preset — the primary result plus metadata, tests, crosstab and overlays, never components — and stamp returned {preset: "standard", digest}. Precedence: request return > host (pulse mcp --return, gosdk.Config.DefaultReturn, mcpserve.Options.DefaultReturn) > instance default (pulse.Options.DefaultReturn, else the feature profile’s return) > standard. An agent that needs everything sends "return": {"preset": "full"}; an operator restores the pre-default output with pulse mcp --return full. The library default is unchanged (full). Contract: Response shaping.

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 bind-on-inspect works on the single stdio session (see Limitations). Stdout is the JSON-RPC channel; stderr is the log channel — never write structured output to stdout outside the protocol.
  • Streamable HTTP. Not exposed by the mcp CLI leaf yet. The underlying go-sdk server supports it; embedders build their own go-sdk server and call gosdk.Register(server, p, cfg) (or use mcpserve.Serve), then serve over the transport of their choice. Bind-on-inspect has no per-session override on a shared HTTP server (see Limitations).

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

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 bindingsession-bootstrap
Author a Process requestrequest-envelope, aggregation-design
Compose multiple sub-requests in one callcompose-requests
Iterate on a request with pulse_predicttool-predict
Look up an error code or warningtool-errors-lookup
Pick an aggregator / filtereraggregation-design
Pick an attribute (z-score, percentile, formula, …)attribute-composition
Design a groupergrouper-design
Use a window operator (WIN_*)window-design
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
Gate, mask or derive a synthetic field (rules[])synth-structural-rules
Understand a cohort’s schema layoutcohort-schema-design
Import a tabular source into .pulsetool-import
Pick an export formatpulse manifest --json (Export capability block)
Work with decimal128 (currency, precise arithmetic)financial-cohorts
Get started end-to-end (LLM walkthrough)session-bootstrap

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.