pulse mcp
Audience: operators wiring Pulse into an MCP-aware AI client (Claude Desktop, Claude Code, generic MCP clients).
pulse mcp runs the Model Context Protocol server over stdio. The AI
client launches pulse mcp as a subprocess, speaks MCP over its
stdio streams, and shuts it down on session close.
LLM agents using MCP: the agent-side guide is the
session-bootstrapskill — fetch it viapulse_skills_getfor the tool catalog and request shapes. This page is for the human setting the server up.
Synopsis
pulse mcp [--data-dir PATH] [--feature-profile FILE] [--return PRESET] [--limit NAME=VALUE]... [--bind-on-open] [--no-cohort-scan] [--metrics-addr HOST:PORT] [--log-level LEVEL] [--log-format text|json]
The command reads stdin, writes MCP responses on stdout, and writes a
one-line startup notice (and any subsequent diagnostics, including
--log-level records) on stderr. Nothing but JSON-RPC is ever written
to stdout.
Flags
| Flag | Type | Default | Purpose |
|---|---|---|---|
--data-dir | string | from PULSE_DATA_DIR env var | Cohort base directory |
--bind-on-open | bool | true | Register session-scoped JSON-schema-bound tool variants on successful pulse_inspect |
--no-cohort-scan | bool | false | Skip the startup walk that enumerates .pulse files as pulse:// resources (env: PULSE_MCP_NO_COHORT_SCAN) |
--feature-profile | string | from PULSE_FEATURE_PROFILE env var | Feature profile JSON file — an OS path, not resolved under the data dir. An invalid profile fails startup |
--return | string | unset (instance / profile default, else standard) | return preset (full, standard, minimal) for a tool request without its own return block. An unknown preset fails startup (PULSE_RETURN_INVALID) |
--limit | string, repeatable | unset (profile limits, else the built-in defaults) | Instance resource limit as name=value (e.g. max_groups=1000000, request_timeout=30s). An unknown name or bad value fails startup (CLI_INPUT) |
--metrics-addr | string | unset (no port opened) | Serve Prometheus text-format metrics on GET /metrics at host:port (:0 picks a free port). A bad or busy address fails startup (CLI_INPUT) |
--log-level / --log-format | string | off / text | Root persistent flags (Global flags): structured logs on stderr |
--data-dir is required in one of its two forms (env var or
flag). The MCP server fails to start otherwise:
data directory required: set PULSE_DATA_DIR or pass --data-dir
–feature-profile
Loads a feature profile into the served instance. The value is a host
OS path — absolute or relative to the process working directory, never
resolved under --data-dir — read and strictly parsed before
pulse.New. The flag wins over PULSE_FEATURE_PROFILE; the env var is
read by this leaf only, so every other pulse command stays
unprofiled. Any invalid profile (unreadable file, malformed JSON, an
unknown key or feature, an unmet dependency) aborts startup with a
PULSE_FEATURE_PROFILE_* error. The server then registers only the
tools and prompts the profile offers; a hidden tool fails like a
nonexistent one. A profile whose behaviour sets
disable_cohort_scan, or that omits mcp_extra:cohort_resources,
skips the startup scan as if --no-cohort-scan were passed.
The stderr startup notice reports the EFFECTIVE settings, so a profile
that turned the scan off reads cohort-scan: false even without the
flag, and a loaded profile is named by its profile label
((unnamed) when it has none):
pulse mcp: serving over stdio (data dir: /mnt/cohorts, bind-on-open: true, cohort-scan: false, return: standard, feature-profile: self-serve)
Embedders get the same effective view from
mcpserve.Describe(p, opts), which returns a mcpserve.ServeInfo
(CohortScan, FeatureProfileLoaded, FeatureProfile, DefaultReturn,
Limits). The library
contract is Feature Profiles.
–return
MCP agents pay tokens for every key they receive, so the request-carrying
tools (pulse_process, pulse_predict, every pulse_compose slot, 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. The library default stays full.
Precedence, highest first:
- the request’s own
returnblock (a slot’s, a stage’s); --return/gosdk.Config.DefaultReturn/mcpserve.Options.DefaultReturn;- the instance default —
pulse.Options.DefaultReturn, else the feature profile’sreturn; - the built-in
standard.
--return full restores the unshaped (pre-default) output byte for byte.
The startup notice always names the effective preset (return: standard;
(custom) when the instance default is a preset-less include/exclude
selection), and ServeInfo.DefaultReturn carries the same value.
An engine that disables components keeps them off under any return;
only a request disable_components: false re-opens them. The full contract
is Response shaping.
–limit
Sets the served instance’s resource limits — the
pulse.Options.Limits layer — one name=value per flag, repeatable:
pulse mcp --limit max_groups=1000000 --limit request_timeout=30s
Names are the snake_case limit keys the feature profile limits
section and every PULSE_LIMIT_* error’s limit detail use:
request_timeout, max_groups, max_crosstab_cells,
max_estimated_memory, max_matrix_dim, max_compose_slots,
max_chain_stages, max_join_build_rows, max_derived_columns. A
value is a positive integer
(underscores allowed, 10_000_000), a Go duration for request_timeout
(30s, 2m), unlimited (or -1) for no limit, or 0 for the
default. Per key, --limit wins over the feature profile’s limits,
which wins over the built-in default; a later flag for the same name
wins. A malformed pair, an unknown name or a bad value aborts startup
with CLI_INPUT (details {flag, value, limit?}).
The startup notice echoes the limits that differ from the defaults, in the same grammar:
pulse mcp: serving over stdio (data dir: /mnt/cohorts, bind-on-open: true, cohort-scan: true, return: standard, limits: request_timeout=30s max_groups=1000000)
Agents read the effective values from the manifest’s limits block
(cache key (pulse_version, feature_set_digest, limits_digest));
embedders read the same block from ServeInfo.Limits.
–metrics-addr
Opt-in Prometheus exporter. Unset (the default), pulse mcp opens no
port and records no metrics. Set, it binds host:port after the tool
surface registers — a bad or busy address fails startup with
CLI_INPUT — and serves the Prometheus text format on GET /metrics
(every other path is 404) until the server exits. The startup notice
names the bound address, so :0 reveals the port it picked:
pulse mcp --log-level info --log-format json --metrics-addr 127.0.0.1:9090
# Stderr: pulse mcp: serving over stdio (data dir: /mnt/cohorts, bind-on-open: true, cohort-scan: true, return: standard, log-level: info, metrics: 127.0.0.1:9090)
Bind a loopback or otherwise private address: the endpoint carries no authentication. Metric labels are operation kinds, scopes, phases, error codes, limit names and hook names — never cohort paths or data.
–bind-on-open
When a session calls pulse_inspect successfully, the server can
register session-scoped tool variants whose JSON Schemas constrain
field-name parameters to the cohort’s actual fields. This narrows
the LLM’s choices and prevents typos at parameter-binding time.
Default: true. Pass --bind-on-open=false if your client binds
tool schemas itself.
The binding logic lives in the SDK-free core
internal/mcp/bind.go
(the per-session server mutation that consumes it lives in the go-sdk
adapter, mcp/gosdk/bind.go); see
Adding an MCP tool for the
LLM-facing implications.
–no-cohort-scan
At startup the server walks the data directory once and registers every
.pulse file it finds as an exact-match pulse://<path> resource, so
resources/list enumerates the available cohorts. On a large, remote or
volatile data root that walk is the most expensive thing startup does,
and the enumeration is not always worth it — a client that already knows
which cohort it wants never reads the list.
--no-cohort-scan (or PULSE_MCP_NO_COHORT_SCAN=1) skips the walk. The
pulse://{+path} resource template is registered either way, so
every cohort stays readable by URI:
resources/read pulse://surveys/2026.pulse # works with or without the scan
resources/list # lists cohorts only WITH the scan
Nothing else changes: the static pulse://schema resource, the
pulse-skill:// resources, every tool, and pulse_inspect (the tool
route to the same header+schema JSON) are unaffected.
Embedders reach the same knob as gosdk.Config{DisableCohortScan: true}
or mcpserve.Options{DisableCohortScan: true}; both default to scanning.
Wiring it into Claude Desktop
~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"pulse": {
"command": "/usr/local/bin/pulse",
"args": ["mcp"],
"env": {
"PULSE_DATA_DIR": "/var/data/pulse"
}
}
}
}
Restart the client. The Pulse tools (pulse_manifest, pulse_inspect,
pulse_predict, pulse_process, pulse_compose, pulse_sample,
pulse_facet, pulse_import, pulse_dedup, pulse_materialize, pulse_recommend, pulse_explain, pulse_drop, pulse_imports_list,
pulse_examples_search, pulse_examples_get, pulse_errors_lookup,
pulse_skills_list, pulse_skills_get) and resources
(pulse://*.pulse, pulse-skill://*) appear in the tool/resource
list. The per-cohort pulse://*.pulse entries come from the startup
scan — see --no-cohort-scan.
Wiring it into Claude Code
~/.claude.json (or per-project .claude.json):
{
"mcpServers": {
"pulse": {
"command": "/usr/local/bin/pulse",
"args": ["mcp"],
"env": { "PULSE_DATA_DIR": "/var/data/pulse" }
}
}
}
The full LLM-side recipe (including resource URIs and the schema binding details) is in Adding an MCP tool and Wiring an MCP client.
Exit codes
pulse mcp is a long-running process. It exits non-zero only on
fatal startup failure (missing data dir, transport error). Once
serving, an MCP client controls the lifecycle.
Examples
Foreground run for debugging
PULSE_DATA_DIR=/tmp/pulse-data ./bin/pulse mcp
# Stderr: pulse mcp: serving over stdio (data dir: /tmp/pulse-data, bind-on-open: true, cohort-scan: true, return: standard)
Disable schema binding
PULSE_DATA_DIR=/tmp/pulse-data ./bin/pulse mcp --bind-on-open=false
Skip the cohort scan on a large data root
PULSE_DATA_DIR=/mnt/cohorts ./bin/pulse mcp --no-cohort-scan
# Stderr: pulse mcp: serving over stdio (data dir: /mnt/cohorts, bind-on-open: true, cohort-scan: false, return: standard)
Or from an MCP client config, which sets env rather than args:
"env": { "PULSE_DATA_DIR": "/mnt/cohorts", "PULSE_MCP_NO_COHORT_SCAN": "1" }
Inspect what the server registers
# Manifest exposes the MCP tool list
pulse --json | jq '.data.mcp_tools[]'
Related
- How LLMs Use Pulse — the pointer table from this site into the skill pack
- Adding an MCP tool — LLM-side wiring, tool catalog, resource schemes, schema binding
- Deployment — production hardening notes
- Troubleshooting — common MCP failure modes