All docs

Getting started

Connect AI to your data

Source: docs/ai.md on GitHub · edit this page · Plain Markdown

Use an AI assistant to ask questions about data already synced to your PulsHealth database. The read-only MCP server translates those questions into product API requests; it cannot write to Apple Health or the database.

Setting up a server first? Follow Set up your server, which includes a prompt for your coding assistant. llms.txt is the documentation index. Giving an assistant that URL does not grant access to your health data.

Choose your connection#

Your situationStart here
You use a hosted PulsHealth accountSign in to the hosted connector
Your assistant runs locally and can reach your serverLocal MCP or private HTTP connection
Your assistant connects from its provider's cloudPublic HTTPS with account sign-in
You want to try one question without a connectorPaste a summary

A connection lets the assistant's provider process the data it reads, including profile details. Choose the scope and provider deliberately. The iOS app syncs to your database; it does not connect directly to AI.

Connect an AI assistant to your PulsHealth account#

If you already sync to a hosted PulsHealth account, add this remote MCP URL in a client that supports OAuth sign-in:

https://mcp.pulshealth.com/mcp

Connect, sign in with your PulsHealth account, review the consent page, and choose Allow. Leave client ID and client secret fields empty when the client supports automatic registration. A client that only accepts a static token cannot use the hosted connection.

For Claude, use its custom remote connector flow. Available surfaces, menus and plan requirements are described in Claude's connector guide. For Claude Code:

claude mcp add --transport http --scope user pulshealth https://mcp.pulshealth.com/mcp

Run /mcp, select pulshealth, and authenticate. Then ask: “What data do you have about me, and how current is it?”

The grant covers all health data and profile details on your account, read-only. Revoke a connection under AI assistants on your account page. Refreshing stops immediately; an already issued access token can remain valid for up to 30 minutes. Changing your password or deleting the account also revokes its grants. See the privacy policy.

What is available today#

Ask aboutTool the assistant uses
Who has data on the server, if more than one person doeslist_users
Which data exists, how current it is, what day it islist_available_types
How the last week or month went, in one pageget_summary
Who the data belongs to (name, age, sex)get_profile
Current weight, resting heart rate, HRV, VO2 max, blood oxygen, ...get_latest_metrics
Daily steps, energy, distance, exercise minutes, heart-rate averages, weight trend, ...get_daily_metrics
Activity rings and goals per dayget_activity_rings
Workouts, filtered by date and activitylist_workouts
One workout's heart rate / power / pace statistics, laps, pausesget_workout
The second-by-second curves inside one workoutget_workout_series
Sleep by night: time asleep, time in bed, stagesget_sleep
The individual records of one type, rawget_samples
Logged moods and emotionsget_state_of_mind

Daily values are the deduplicated ones (no iPhone + Watch double counting), every value carries its unit, and requested dates use the selected account's reporting time zone. Phone aggregates and rings retain recorded phone calendar dates, which can differ from raw reporting dates during travel. Sleep follows Apple Health: a night is dated by the day you wake up, and where several devices recorded the same night nothing is summed across them. get_samples is the one tool that returns undeduplicated records; that is what makes it useful for looking at particular readings and useless for totals. The assistant can also read pulshealth://guide, a short manual on the data model and its traps, and two ready-made prompts (weekly_summary, compare_workouts).

When several phones sync to one server, every tool takes an optional user (a user_id from list_users); without it the assistant reads the API's default person. The API only honours another user when its PULS_MULTI_USER is on, and an MCP instance can be pinned to one person with PULS_USER_ID (PULS_MCP_USER_ID for the Compose service) so that a connector you hand to one household member can never be asked about another.

Not yet: GPS routes, medication doses, ECGs and heartbeat series. They are in the database; no tool serves them.

For a whole range as a file rather than an answer in a chat (a spreadsheet, a notebook, something to attach), use GET /v1/export or the puls-export CLI instead of a tool call: export.md.

No MCP at all: paste a summary#

After server setup, run this on the server from the repository root. Load the trusted local settings without pasting tokens into a chat:

set -a
. server/.env
set +a
curl --fail-with-body -sS \
  -H "Authorization: Bearer $PULS_API_TOKEN" \
  'http://127.0.0.1:8081/v1/summary?range=7d'

Read the result before sharing it. It contains health information. range accepts 7d, 14d, 30d or 90d; format=json returns the same figures as JSON. The summary names its date range, time zone, units and coverage. On a remote computer, replace the loopback address with your reachable API URL or use an SSH tunnel.

Two ways to connect#

  1. Local binary (stdio): your assistant launches pulshealth-mcp. It needs the product API URL and PULS_API_TOKEN. Keep the API private through a VPN or SSH tunnel; no public endpoint is required.
  2. Remote MCP (HTTP): use the Compose mcp service with a reachable /mcp URL and PULS_MCP_TOKEN. The client must support bearer headers. Cloud clients need a public HTTPS endpoint; clients requiring sign-in also need OAuth.

The API is port 8081; MCP is 8082. Neither uses the phone's ingest token. See MCP configuration for all settings.

Get the binary#

Only needed for stdio. With Go 1.26 or newer, run from the repository root:

mkdir -p "$HOME/.local/bin"
(cd server/mcp && go build -o "$HOME/.local/bin/pulshealth-mcp" .)
"$HOME/.local/bin/pulshealth-mcp" --version

Use the full binary path in your client configuration. There is no Go module at the repository root. Build from the same revision as your server.

To reach a remote API privately, keep this command running in another terminal (replace user@server with your SSH destination):

ssh -N -L 8081:127.0.0.1:8081 user@server

Then use PULS_API_URL=http://127.0.0.1:8081 on your computer.

Claude Desktop#

For a local MCP configuration, use the JSON below in the client settings that support launching a command. Replace the path and token locally; keep the file out of version control. Desktop, chat and mobile support differ; consult Claude's local and remote connector guide.

{
  "mcpServers": {
    "pulshealth": {
      "command": "/absolute/path/to/pulshealth-mcp",
      "env": {
        "PULS_API_URL": "http://127.0.0.1:8081",
        "PULS_API_TOKEN": "YOUR_API_TOKEN"
      }
    }
  }
}

Claude Code#

With PULS_API_TOKEN already loaded securely in your local shell:

claude mcp add --scope user \
  --env PULS_API_URL=http://127.0.0.1:8081 \
  --env "PULS_API_TOKEN=$PULS_API_TOKEN" \
  --transport stdio pulshealth -- "$HOME/.local/bin/pulshealth-mcp"

For an HTTP connector, replace the example hostname and load its MCP token:

claude mcp add --transport http --scope user \
  --header "Authorization: Bearer $PULS_MCP_TOKEN" \
  pulshealth https://mcp.example.com/mcp

Use one configuration for the name pulshealth. claude mcp list and /mcp show its status. See Claude Code's MCP guide for configuration and authentication commands.

Cursor#

Use the stdio JSON above in your private ~/.cursor/mcp.json, adding "type": "stdio" inside the pulshealth entry. Alternatively, configure an HTTP connector (see Cursor MCP configuration):

{
  "mcpServers": {
    "pulshealth": {
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer YOUR_MCP_TOKEN" }
    }
  }
}

Remote connector: the Compose service over HTTPS#

The bootstrap starts mcp with its own generated token. From the repository root:

docker compose -f server/docker-compose.yml ps mcp api
curl --fail-with-body -sS http://127.0.0.1:8082/healthz

Expect {"api":true,"ok":true}. Keep port 8082 on loopback and forward your HTTPS endpoint to it. For an existing Tailscale network, one option is:

tailscale serve --bg --https=8445 http://localhost:8082

Use the HTTPS URL printed by Tailscale, with /mcp appended. This is private to that network, so it works only when the client can join it. Server networking covers other proxies. Set TRUST_PROXY_HEADERS=true only when a trusted proxy is the sole entry point.

The token presented by the client is PULS_MCP_TOKEN; the MCP service keeps PULS_API_TOKEN for its own API calls. A health check verifies dependencies, not client authentication. Finish by connecting the assistant and asking it to list available data and its latest date.

Hosted connectors (OAuth): clients that sign in instead of taking a token#

For your own cloud-accessible connector, configure:

  • The viewer in accounts mode, with each person's account bound to their synced user ID.
  • A public HTTPS viewer URL and a public HTTPS MCP URL. A private tailnet URL cannot be reached by a cloud client.
  • These settings in server/.env (generate a fresh secret locally):
WEB_ACCOUNTS=true
WEB_PUBLIC_URL=https://viewer.example.com
PULS_MCP_OAUTH_SECRET=REPLACE_WITH_A_RANDOM_SECRET_OF_AT_LEAST_32_CHARACTERS
PULS_MCP_URL=https://mcp.example.com/mcp

Compose passes WEB_PUBLIC_URL to MCP as PULS_MCP_OAUTH_ISSUER. Recreate web and mcp using the same Compose files used for installation. Verify https://mcp.example.com/.well-known/oauth-protected-resource/mcp names your viewer, then connect and sign in through your assistant.

For users other than the API's default person, enable PULS_MULTI_USER=true on the API. Also pin the static MCP credential with PULS_MCP_USER_ID=<intended-user-uuid> so it cannot select every person. OAuth calls always use the signed-in person's ID, regardless of that pin.

The grant is health:read over all of that person's data. Revocation stops refreshes immediately; access tokens expire within 30 minutes. Rotating the shared OAuth secret invalidates existing access tokens, but does not revoke refresh grants. See the viewer OAuth reference and MCP security model.

ChatGPT: the product API as a custom GPT Action#

An existing private GPT Action can call the product API with its OpenAPI schema and a bearer API key. Availability and configuration depend on the client and workspace; see OpenAI's current Action documentation. For a new connection, use an MCP-capable client where available, or paste a summary. Actions require a public API URL reachable by the provider.

1. Publish the API on a public HTTPS URL#

Forward a TLS proxy to 127.0.0.1:8081. Keep the database private. With a trusted proxy as the only entry point, set TRUST_PROXY_HEADERS=true so the API uses its forwarded host and scheme. Check:

curl --fail-with-body -sS https://api.example.com/healthz
curl --fail-with-body -sS https://api.example.com/openapi.json

Verify servers[0].url names that public endpoint, not loopback.

2. Import it#

Import https://api.example.com/openapi.json in the Action's schema editor. Use the schema from your deployed server, which matches its API version.

3. Configure the token#

Choose API key authentication with Bearer, and enter PULS_API_TOKEN locally. This is the read token, not PULS_TOKEN or PULS_MCP_TOKEN.

4. Check it#

Ask which data is available and how current it is. Check that the result names the expected person and real sync dates. latest means the newest uploaded record, not today's date. Ask the assistant to use /v1/users for the selected person's reporting zone and /v1/metrics/daily for daily totals.

Caveats#

Keep the GPT private: anyone able to use its stored credential can read the data it authorizes. With PULS_MULTI_USER=true, that credential can read every person. Use short date ranges and JSON endpoints; use the export CLI for files. When retiring the integration, rotate the API token, recreate api and mcp with the installation's Compose files, and remove unneeded public routes.

Try these#

  • "What data do you have about me, and how current is it?" One list_available_types call; a good first question, it also tells the assistant today's date.
  • "How have I been doing this month?" One get_summary call with range: 30d; the same page GET /v1/summary serves, so it is also the thing to paste into a chat that has no connector.
  • "How did I sleep last week?" One get_sleep call for the seven days; each row is a night, dated by the morning you woke up, with time asleep, time in bed and the core / deep / REM split in minutes. The weekly_summary prompt includes it.
  • "When exactly did my heart rate spike during yesterday's meeting?" get_samples with HKQuantityTypeIdentifierHeartRate for that day; the individual readings, not a daily average.
  • "Show me how my heart rate moved through Saturday's run." list_workouts for the day, then get_workout_series with the uuid.
  • "Compare my runs this month to last month." Two list_workouts calls with activity_type: running, then totals, averages and pace; get_workout on a few for heart rate. The compare_workouts prompt does exactly this.
  • "Did I close my rings yesterday?" get_activity_rings for one day; closed means value ≥ goal.
  • "What's my resting heart rate trend over the last 90 days?" get_daily_metrics with HKQuantityTypeIdentifierRestingHeartRate and HKQuantityTypeIdentifierHeartRateVariabilitySDNN.
  • "How much did I weigh at the start of the year versus now?" get_latest_metrics for now, get_daily_metrics for January.

How the server keeps the model honest#

Daily tools carry units and use calendar dates in the selected person's zone. They prefer the phone's aggregates, with server rollups where supported; raw samples remain undeduplicated. Missing data stays missing rather than becoming zero. The embedded assistant guide explains these rules and provides question-to-tool recipes.

Security#

  • Read tokens expose health and profile data. Keep client configuration and server/.env private; use HTTPS for remote HTTP connections.
  • An unpinned static MCP token can select other people only when the API enables multi-user reads. OAuth is always pinned to the signed-in account.
  • Read-only access still sends returned data to the assistant's provider. Review its data-use settings before connecting.

Troubleshooting#

SymptomCheck
API 401 in a tool resultMCP's PULS_API_TOKEN must match the API.
MCP 401 before tools loadThe client's MCP token is wrong, or OAuth needs sign-in.
API unreachable or MCP health 503Check the API container, its database, and PULS_API_URL.
Cloud client cannot connectIts provider must reach the public HTTPS MCP URL; a private VPN address is insufficient.
OAuth discovery 404Check all OAuth settings; the viewer also requires accounts mode.
API 403 after sign-inThe account differs from the default user and PULS_MULTI_USER is off.
No data or stale dataConfirm the selected user and a completed phone sync. The catalog can lag uploads because it is cached.
Wrong calendar dayCheck the account's reporting zone. Rings and phone aggregates keep their recorded phone dates; travel can differ from raw-day grouping.
Time-zone lookup failsRestore API connectivity; changing a fallback zone does not repair an unavailable API.
Local process fails to startRun the same binary with the same environment; errors go to stderr. --version checks installation only.