health-export-mcp 1.7.3 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -17,15 +17,19 @@ After configuring, restart the client and ask: **"Use health-export: what's my H
17
17
 
18
18
  ## Claude Code
19
19
 
20
- One command, no clone:
20
+ One command, no clone, no environment variable on a Mac (1.7.3 finds the app's iCloud Drive folder itself):
21
21
 
22
22
  ```bash
23
- claude mcp add --transport stdio health-export \
24
- --env "HEALTH_DATA_DIR=$HOME/Library/Mobile Documents/iCloud~ai~healthexport~app/Documents" \
25
- -- npx -y health-export-mcp
23
+ claude mcp add health-export -- npx -y health-export-mcp
26
24
  ```
27
25
 
28
- Keep the double quotes (the folder name contains a space). Add `--scope user` to use it in every project. No export yet: `claude mcp add --transport stdio health-export-demo -- npx -y health-export-mcp --demo` serves synthetic data.
26
+ Windows, Linux, or an export outside iCloud Drive:
27
+
28
+ ```bash
29
+ claude mcp add health-export --env "HEALTH_DATA_DIR=/path/to/folder" -- npx -y health-export-mcp
30
+ ```
31
+
32
+ Keep the double quotes if the path has a space. Add `--scope user` to use it in every project. No export yet: `claude mcp add health-export-demo -- npx -y health-export-mcp --demo` serves synthetic data. If Agent pairing is on, do not pass `PAIRING_SECRET` on the command line (shell history, `ps`); add it to the entry's `env` in the config file.
29
33
 
30
34
  ## Claude Desktop
31
35
 
@@ -56,7 +60,7 @@ Run `node gen-deeplinks.mjs` and open the generated VS Code link, or add the ser
56
60
 
57
61
  ## opencode
58
62
 
59
- `opencode.json` (project root) or `~/.config/opencode/opencode.json`:
63
+ `opencode.json` (project root) or `~/.config/opencode/opencode.json`. Drop the `environment` line on a Mac using iCloud Drive; for a pinned checkout use `["node", "/ABSOLUTE/PATH/TO/server.mjs"]`:
60
64
 
61
65
  ```json
62
66
  {
@@ -64,9 +68,9 @@ Run `node gen-deeplinks.mjs` and open the generated VS Code link, or add the ser
64
68
  "mcp": {
65
69
  "health-export": {
66
70
  "type": "local",
67
- "command": ["node", "/ABSOLUTE/PATH/TO/health-export-mcp/server.mjs"],
71
+ "command": ["npx", "-y", "health-export-mcp"],
68
72
  "enabled": true,
69
- "environment": { "HEALTH_DATA_DIR": "~/Library/Mobile Documents/iCloud~ai~healthexport~app/Documents" }
73
+ "environment": { "HEALTH_DATA_DIR": "/path/to/folder" }
70
74
  }
71
75
  }
72
76
  }
@@ -74,15 +78,15 @@ Run `node gen-deeplinks.mjs` and open the generated VS Code link, or add the ser
74
78
 
75
79
  ## OpenClaw
76
80
 
77
- Add the `health-export` block (same shape as Claude Desktop's `mcpServers` entry) to your OpenClaw MCP servers config, then reload.
81
+ Add the `health-export` block (same shape as Claude Desktop's `mcpServers` entry: command `npx`, args `["-y", "health-export-mcp"]`, plus `HEALTH_DATA_DIR` off a Mac) to your OpenClaw MCP servers config, then reload.
78
82
 
79
83
  ## Hermes
80
84
 
81
- Add the `health-export` block to your Hermes agent's MCP servers config, then restart the daemon.
85
+ Add the same `health-export` block to your Hermes agent's MCP servers config, then restart the daemon.
82
86
 
83
87
  ## Non-MCP clients, ChatGPT · Gemini · Grok · n8n · Home Assistant
84
88
 
85
- These don't speak MCP, so they don't use this server. Instead, the [MetricBridge](https://www.healthexport.dev) app **POSTs your data to a webhook** (or your own endpoint) and your automation/agent reads that JSON. Every delivery is token-authenticated.
89
+ ChatGPT connects only to remote MCP servers (OpenAI's help centre: "Can I connect to a local MCP server? Not directly", checked 2026-10-27), and the others don't run local MCP servers either, so they don't use this one. Instead, the [MetricBridge](https://www.healthexport.dev) app **POSTs your data to a webhook** (or your own endpoint) and your automation/agent reads that JSON. Every delivery is token-authenticated.
86
90
 
87
91
  ---
88
92
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # health-export-mcp
2
2
 
3
- **An MCP server for your own Apple Health data.** 194 metrics, 16 read-only tools and 22 prompts over local stdio, with zero dependencies, no account and no server of ours in the path. Your agent gets JSON with units, date ranges and coverage, and answers from your numbers.
3
+ **An MCP server for your own Apple Health data.** 194 metrics, 21 tools (19 read-only, plus two that log events to their own file) and 23 prompts over local stdio, with zero dependencies, no account and no server of ours in the path. Your agent gets JSON with units, date ranges and coverage, and answers from your numbers.
4
4
 
5
5
  <p align="center">
6
6
  <a href="https://www.npmjs.com/package/health-export-mcp"><img alt="npm" src="https://img.shields.io/npm/v/health-export-mcp?color=cb3837" /></a>
@@ -11,35 +11,34 @@
11
11
  <a href="https://glama.ai/mcp/servers/PhilipAD/health-export-mcp"><img alt="Glama score" src="https://glama.ai/mcp/servers/PhilipAD/health-export-mcp/badges/score.svg" /></a>
12
12
  </p>
13
13
 
14
- ## Try it in 30 seconds (no iPhone, synthetic data)
14
+ Docs and per-client config: **[healthexport.dev/mcp](https://www.healthexport.dev/mcp/)** · agents can run the setup from **[SKILL.md](https://www.healthexport.dev/SKILL.md)** · privacy: the server reads a local file and makes no network requests, so your health data never reaches us or a cloud AI through it.
15
+
16
+ ## Install: one command
15
17
 
16
18
  ```bash
17
- claude mcp add --transport stdio health-export-demo -- npx -y health-export-mcp --demo
19
+ claude mcp add health-export -- npx -y health-export-mcp
18
20
  ```
19
21
 
20
- Ask *"Use health-export-demo: how did my HRV this week compare with last week?"* The tool call returns this (every demo answer is watermarked):
22
+ On a Mac that is the whole install: since 1.7.3 the server finds the MetricBridge app's iCloud Drive folder by itself. On Windows or Linux, or for an export in another folder, add `--env "HEALTH_DATA_DIR=/path/to/folder"` before the `--`. Needs Node.js 18 or later. **Claude Desktop:** open [`health-export.mcpb`](https://github.com/PhilipAD/health-export-mcp/releases/latest/download/health-export.mcpb) instead (no JSON, no Node on your PATH). Cursor, VS Code, opencode, OpenClaw, Hermes: [AGENTS.md](AGENTS.md).
23
+
24
+ Then ask *"Use health-export: how did my HRV this week compare with last week?"* The agent calls `get_trends` and gets numbers that carry their own provenance: unit, exact date ranges, whether each window was complete, and how much history the export holds. This is the real output on 1.7.3 (from the built-in `--demo` dataset, which is synthetic so it can be published; a real export answers with the same fields and without the watermark):
21
25
 
22
26
  ```json
23
27
  [SYNTHETIC DEMO DATA] {"metric":"heart_rate_variability","unit":"ms","window":7,"recent":50.859,"prior":51.306,
24
- "changePercent":-0.871,"direction":"flat","recentRange":{"from":"2026-08-12","to":"2026-08-18"},
25
- "windowSatisfied":true,"coverage":{"firstDate":"2025-07-15","lastDate":"2026-08-18","days":400}}
28
+ "change":-0.447,"changePercent":-0.871,"direction":"flat","recentRange":{"from":"2026-08-12","to":"2026-08-18"},
29
+ "recentDays":7,"priorDays":7,"windowSatisfied":true,"coverage":{"firstDate":"2025-07-15","lastDate":"2026-08-18","days":400}}
26
30
  ```
27
31
 
28
- ## Use your real data (about 5 minutes, needs an iPhone)
32
+ **No iPhone yet?** `claude mcp add health-export-demo -- npx -y health-export-mcp --demo` serves that watermarked synthetic dataset, so you can try every tool in 30 seconds before installing anything on a phone.
29
33
 
30
- 1. **Export.** Install [MetricBridge](https://apps.apple.com/app/apple-store/id6784185201?pt=129083290&ct=githubreadme&mt=8&ppid=2c28f88a-d94f-48f9-8e6a-74a8d89ce66e) on the iPhone, allow Health read access and tap **Export now**. It writes `.health-cache.json` to iCloud Drive (or a folder, or your LAN). The app has a free trial, then one purchase, no subscription. This server is MIT and free.
31
- 2. **Connect.** Claude Code:
32
- ```bash
33
- claude mcp add --transport stdio health-export -- npx -y health-export-mcp
34
- ```
35
- On a Mac the server finds the app's iCloud Drive folder by itself (since 1.7.3). Elsewhere, or for another folder, add `--env "HEALTH_DATA_DIR=/path/to/folder"`, with the quotes if the path has a space.
36
- **Claude Desktop:** download [`health-export.mcpb`](https://github.com/PhilipAD/health-export-mcp/releases/latest/download/health-export.mcpb) and open it; no JSON and no Node on your PATH needed. Cursor, VS Code, opencode, OpenClaw and Hermes: see [AGENTS.md](AGENTS.md) and the [quickstart](#connect-apple-health-to-your-ai-agent-quickstart).
37
- 3. **Check it arrived.** `npx -y health-export-mcp --doctor` prints the folder it chose and why, each file's age, and freshness. iCloud can take a few minutes to bring a new export to the Mac.
34
+ ## Use your real data (about 5 minutes the first time, needs an iPhone)
38
35
 
39
- Then ask *"Use health-export: compare my resting heart rate this month with last month."*
36
+ 1. **Export.** Install [MetricBridge](https://apps.apple.com/app/apple-store/id6784185201?pt=129083290&ct=githubreadme&mt=8&ppid=2c28f88a-d94f-48f9-8e6a-74a8d89ce66e) on the iPhone, allow Health read access and tap **Export now**. It writes `.health-cache.json` to iCloud Drive (or a folder, or your LAN). The app has a 7-day free trial, then one purchase ($24.99), no subscription. This server is MIT and free.
37
+ 2. **Connect.** The one command above (Claude Code), the `.mcpb` (Claude Desktop), or the block for your client in [AGENTS.md](AGENTS.md).
38
+ 3. **Check it arrived.** `npx -y health-export-mcp --doctor` prints the folder it chose and why, each file's age, and freshness. iCloud can take a few minutes to bring a new export to the Mac.
40
39
 
41
40
  <p align="center">
42
- <img src="assets/architecture.svg" alt="Apple Health exports to iCloud, a folder, or your LAN; health-export-mcp reads it and serves 16 query tools to your AI agent" width="100%" />
41
+ <img src="assets/architecture.svg" alt="Apple Health exports to iCloud, a folder, or your LAN; health-export-mcp reads it and serves 21 tools to your AI agent" width="100%" />
43
42
  </p>
44
43
 
45
44
  ---
@@ -83,12 +82,12 @@ The companion iOS app **[MetricBridge](https://www.healthexport.dev)** exports y
83
82
  **Fastest, one command (Claude Code):**
84
83
 
85
84
  ```bash
86
- claude mcp add --transport stdio health-export \
87
- --env "HEALTH_DATA_DIR=$HOME/Library/Mobile Documents/iCloud~ai~healthexport~app/Documents" \
88
- -- npx -y health-export-mcp
85
+ claude mcp add health-export -- npx -y health-export-mcp
86
+ # Windows, Linux, or an export outside iCloud Drive:
87
+ claude mcp add health-export --env "HEALTH_DATA_DIR=/path/to/folder" -- npx -y health-export-mcp
89
88
  ```
90
89
 
91
- Everything before `--` is an option for `claude mcp add`, everything after it is the command that starts the server. The path is double-quoted because the iCloud folder name contains a space, and `$HOME` is used because a quoted `~` is not expanded by the shell. Exporting somewhere else? Replace the path with your own folder. Add `--scope user` to make the server available in every project (the default scope is the current project only). Check it with `claude mcp list`.
90
+ Everything before `--` is an option for `claude mcp add`, everything after it is the command that starts the server. On a Mac the server reads the app's iCloud Drive folder when `HEALTH_DATA_DIR` is unset (1.7.3+); if you set it to that folder by hand, double-quote it (the folder name contains a space) and use `$HOME`, because a quoted `~` is not expanded by the shell. Add `--scope user` to make the server available in every project (the default scope is the current project only). Check it with `claude mcp list`.
92
91
 
93
92
  **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`), and any client that takes an `mcpServers` block:
94
93
 
@@ -97,14 +96,13 @@ Everything before `--` is an option for `claude mcp add`, everything after it is
97
96
  "mcpServers": {
98
97
  "health-export": {
99
98
  "command": "npx",
100
- "args": ["-y", "health-export-mcp"],
101
- "env": { "HEALTH_DATA_DIR": "~/Library/Mobile Documents/iCloud~ai~healthexport~app/Documents" }
99
+ "args": ["-y", "health-export-mcp"]
102
100
  }
103
101
  }
104
102
  }
105
103
  ```
106
104
 
107
- > `npx -y` fetches the published package at run time without an install prompt (npm verifies package integrity), so it runs anywhere with no clone or absolute path. The server expands a leading `~` in `HEALTH_DATA_DIR` itself, so the JSON form can keep it. Prefer to pin a vetted local checkout instead? Use `"command": "node", "args": ["REPLACE_WITH_ABSOLUTE_PATH/server.mjs"]`, get the path with `node -e "console.log(process.cwd()+'/server.mjs')"` inside the repo.
105
+ > Off a Mac, or for another folder, add `"env": { "HEALTH_DATA_DIR": "/path/to/folder" }` to the entry (a leading `~` is expanded by the server). `npx -y` fetches the published package at run time without an install prompt (npm verifies package integrity), so it runs anywhere with no clone or absolute path. The server expands a leading `~` in `HEALTH_DATA_DIR` itself, so the JSON form can keep it. Prefer to pin a vetted local checkout instead? Use `"command": "node", "args": ["REPLACE_WITH_ABSOLUTE_PATH/server.mjs"]`, get the path with `node -e "console.log(process.cwd()+'/server.mjs')"` inside the repo.
108
106
  > Or skip JSON entirely, drag **`health-export.mcpb`** into **Claude Desktop → Settings → Extensions**.
109
107
 
110
108
  **Auto-configure from a clone:**
@@ -144,26 +142,31 @@ Restart the client and try:
144
142
 
145
143
  ---
146
144
 
147
- ## The 16 MCP tools
145
+ ## The 21 MCP tools
148
146
 
149
147
  Full reference with request/response examples: **[healthexport.dev/mcp](https://www.healthexport.dev/mcp/#tools)**.
150
148
 
151
149
  | Tool | What it does |
152
150
  |---|---|
153
151
  | `get_mcp_status` | Health check: source, metric/workout counts, which context files exist, latest data date, and a `freshness` summary. **Call first.** |
154
- | `get_freshness` | How current the export is: `stale` against a stated threshold (26 h by default, `HEALTH_STALE_AFTER_HOURS`, or `maxAgeHours`), `age_hours`, `as_of`, `last_data_date`, per-file write times. Same rule as the `status --max-age` cron gate. |
152
+ | `get_freshness` | How current the export is: `stale` judged on the core files (daily cache, sleep sessions) against a stated threshold (26 h by default, `HEALTH_STALE_AFTER_HOURS`, or `maxAgeHours`), `age_hours`, `as_of`, `last_data_date`, per-file write times. `state: "stale_copy"` (with a `stale_copy` list) when the phone's `.health-manifest.json` or iCloud says this Mac holds an older copy of a core file than the phone wrote. Same rule as the `status --max-age` cron gate. |
155
153
  | `list_metrics` | Every available metric with unit, day count, and date range. |
156
- | `get_health_metrics` | Daily values for a metric (or all) over a date range + an aggregate (avg/sum/min/max/latest). Supports `filterDays` (restrict to days covered by logged events, with `negate`). |
154
+ | `get_health_metrics` | Daily values for a metric (or all) over a date range + an aggregate (avg/sum/min/max/latest). Supports `filterDays` (restrict to days covered by logged events, with `negate`). Blood pressure at day granularity also lists the individual `readings` (`health-readings.json`). |
157
155
  | `get_trends` | Recent N-day window vs the prior N days: change, % change, direction. Supports `excludeTravelDays`. |
158
156
  | `compare_periods` | A metric across two arbitrary date periods (A vs B), or around a logged event via `anchor: {eventId, days}` (the event day excluded from both sides). |
159
157
  | `get_structured_export` | Clean JSON for chosen metrics/range, paginated with a cursor. |
160
- | `get_intraday` | Today's hour-by-hour window from the app's hourly automations (`health-intraday.json`, app 1.4+), replaced each run. |
158
+ | `get_intraday` | The hour-by-hour window from the app's hourly automations (`health-intraday.json`, app 1.4+): yesterday and today, replaced each run, with each hour's `Min`/`Max` for discrete metrics when the app writes them. |
161
159
  | `query_health_data` | Natural-language convenience: *"average HRV last month"* routed to structured results. |
162
- | `list_events` | Logged context events (medication, habit, visit, life, shift, episode, travel) with type/tag/date filters. |
160
+ | `list_events` | Logged context events (medication, habit, visit, life, shift, episode, travel) with type/tag/date filters, from the app and from `add_event`, each labelled `source: "app"` or `"agent"`. |
161
+ | `add_event` | Log an event the user states (a medication start, a trip) into `health-events-agent.json`, the one file this server writes. Idempotent by `clientEventId`. |
162
+ | `remove_event` | Remove an event logged with `add_event`. Events logged in the iOS app are edited there. |
163
163
  | `get_profile` | Opted-in context fields (conditions, medications, goals, allergies, notes) plus `presentFields`. Absent fields were withheld, never "none". |
164
164
  | `get_workouts` | Workouts by activity/date with pagination; includes heart rate, running dynamics, cycling power, intervals and `hasRoute` when exported. |
165
- | `get_sleep_sessions` | Clustered sleep sessions attributed to the waking day; split nights returned as-is. |
165
+ | `get_sleep_sessions` | Clustered sleep sessions attributed to the waking day; split nights returned as separate sessions, duplicate copies of one night collapsed (`collapsedDuplicates`). |
166
+ | `get_strength_log` | The strength log typed into the iOS app (`health-strength.json`, opt-in, off by default): each session with every set as entered, `volumeKg`, `workingSets`, an Epley `bestEstimateKg` per exercise, all-time records, and the user's saved routines. Filter by `exercise`, `start`, `end`. Missing file → `available:false`. |
167
+ | `get_check_ins` | Morning check-ins the user chose to share (`health-check-ins.json`, off by default in the app): self-rated feeling 1 to 5, note, intention and whether it happened, plus a summary. A self-rating, not a measurement. |
166
168
  | `get_cycle_context` | Day-in-cycle and coarse phase derived from logged period starts. Never predictive. |
169
+ | `list_tags` | Tags logged in the app (`health-tags.json`, opt-in there and off by default): lifestyle tags and supplements by name, never an amount. `{kind: all, supplement or lifestyle, start, end}`; one row per tag plus the names per day. |
167
170
  | `correlate_metrics` | Pearson correlation between two metrics with a 0..3 day lag. Association, not causation, always stated. |
168
171
  | `resolve_metric` | Canonical name and unit for any spelling (`steps`, `StepCount`, `Body Weight`, `hrv`), unit variants with exact formulas, an optional `{value, unit}` conversion, and whether the export holds it. Ambiguous words come back with candidates, never guessed. The data tools accept the same aliases and report `resolvedFrom`. When the export carries per-source attribution (app builds that write `src`/`by` on daily points and a `health-sources.json`, cache schema minor 2), the answer adds a `sources` block naming the devices that fed the export, which one won each metric and the priority rules, and `sourceMerge` reports disagreement as visible; older exports report it as not available. |
169
172
 
@@ -171,23 +174,23 @@ Full reference with request/response examples: **[healthexport.dev/mcp](https://
171
174
 
172
175
  ### Data files
173
176
 
174
- The daily cache (`.health-cache.json`) and workouts cache are joined by optional context files, all read-only and all optional: `health-events.json` (the logging surface), `health-profile.json` (opt-in context), `health-sessions.json` (sleep sessions), `health-cycles.json` (observed cycle starts), `health-days.json` (timezone change log). An absent file is reported as `available:false` with a note, never as "no data". Formats: [docs/SCHEMA-CONTRACTS.md](docs/SCHEMA-CONTRACTS.md).
177
+ The daily cache (`.health-cache.json`) and workouts cache are joined by optional context files, all optional: `health-events.json` (the logging surface), `health-profile.json` (opt-in context), `health-sessions.json` (sleep sessions), `health-cycles.json` (observed cycle starts), `health-days.json` (timezone change log), `health-readings.json` (individual blood pressure readings), `health-check-ins.json` (morning check-ins, opt-in), `health-strength.json` (the strength log, when its export is switched on), `health-tags.json` (logged tags with supplements marked, opt-in in the app) and `.health-manifest.json` (what the phone last wrote, for the stale-copy check). The server reads all of them and writes only `health-events-agent.json` (events logged with `add_event`). An absent file is reported as `available:false` with a note, never as "no data". Formats: [docs/SCHEMA-CONTRACTS.md](docs/SCHEMA-CONTRACTS.md).
175
178
 
176
- Two patterns worth knowing: [query a parent's Apple Health from your own AI](docs/caregiver-setup.md) (consent-first, the parent's phone is the authority) and [logging data into Apple Health via Shortcuts](docs/shortcuts-write-bridge.md) (the server and app stay strictly read-only).
179
+ Two patterns worth knowing: [query a parent's Apple Health from your own AI](docs/caregiver-setup.md) (consent-first, the parent's phone is the authority) and [logging data into Apple Health via Shortcuts](docs/shortcuts-write-bridge.md) (the server never writes to Apple Health).
177
180
 
178
181
  ### MCP prompts
179
182
 
180
- The server ships 22 prompts over `prompts/list` / `prompts/get`: daily brief, weekly review, doctor visit prep, what changed since my last visit, sleep quality and regularity, HRV trend, training and race week reviews, zone minutes, n-of-1 experiment, medication before/after, GLP-1 dose-step compare, sobriety milestone, shift block compare, travel-honest monthly review, cycle-aware trend read, caregiver check-in, glucose day summary, long-term activity narrative, data coverage audit, and a profile-aware context bootstrap. Every prompt leads with coverage honesty and never turns data into diagnosis.
183
+ The server ships 23 prompts over `prompts/list` / `prompts/get`: daily brief, weekly review, doctor visit prep, what changed since my last visit, sleep quality and regularity, HRV trend, training and race week reviews, zone minutes, n-of-1 experiment, medication before/after, GLP-1 dose-step compare, sobriety milestone, shift block compare, travel-honest monthly review, cycle-aware trend read, caregiver check-in, glucose day summary, long-term activity narrative, data coverage audit, a nutrition week summary, and a profile-aware context bootstrap. Every prompt leads with coverage honesty and never turns data into diagnosis.
181
184
 
182
185
  ### Demo mode
183
186
 
184
- `node server.mjs --demo` (or `HEALTH_DEMO=1`) serves a deterministic synthetic dataset: ~15 metrics over 400 days, 30 workouts with intervals, 8 events, a profile, 60 sleep sessions, cycle starts and a timezone change, anchored to a fixed date with a seeded PRNG. Every answer carries `demo: true` and a `[SYNTHETIC DEMO DATA]` text prefix, so demo output can never pass for a real export.
187
+ `node server.mjs --demo` (or `HEALTH_DEMO=1`) serves a deterministic synthetic dataset: ~15 metrics over 400 days, 30 workouts with intervals, 8 events, a profile, 60 sleep sessions, cycle starts, a timezone change and 30 days of tags, anchored to a fixed date with a seeded PRNG. Every answer carries `demo: true` and a `[SYNTHETIC DEMO DATA]` text prefix, so demo output can never pass for a real export.
185
188
 
186
189
  ### CLI
187
190
 
188
191
  ```bash
189
192
  node server.mjs --doctor # diagnostics: files, sizes, schema, freshness, pairing
190
- node server.mjs status --max-age 24 # exit 0 if data is fresh, 1 if stale (cron gate)
193
+ node server.mjs status --max-age 24 # exit 0 if data is fresh, 1 if stale or a stale iCloud copy (cron gate)
191
194
  node server.mjs receive # standalone LAN receiver (binds 127.0.0.1 by default)
192
195
  node server.mjs --help
193
196
  ```
@@ -251,7 +254,7 @@ with exactly two shapes: `{"state": "trial", "daysRemaining": N}` or `{"state":
251
254
  export from a trial adds one sentence to `get_freshness` naming when the trial ends; an unlocked export
252
255
  adds nothing. No network call is involved and nothing is sent anywhere.
253
256
 
254
- - **Read-only.** The server only reads your exported data, it never touches HealthKit and never writes back.
257
+ - **Reads your export, never Apple Health.** The server never touches HealthKit. Its only write is `health-events-agent.json`, the events an agent logs with `add_event` / `remove_event`; it never modifies a file the app writes. Both tools are refused in demo mode and on a read-only folder, and the file is sealed like the rest of an encrypted export.
255
258
  - **Local-first.** It runs on your machine over stdio. There is **no developer server** in the path.
256
259
  - **Optional pairing.** Set `PAIRING_SECRET` to the code the iOS app shows (Settings → Agent pairing) to gate access.
257
260
  - **Encrypted exports (optional).** If you switch on Export encryption in the iOS app, set `HEALTH_EXPORT_PASSPHRASE` (or `HEALTH_EXPORT_PASSPHRASE_FILE`) to the same passphrase and the server decrypts on your machine. See [Encrypted exports](#encrypted-exports).
@@ -352,6 +355,7 @@ For your own data, yes: the server reads the file the MetricBridge app writes an
352
355
  - **"No metrics found":** run `npx -y health-export-mcp --doctor`. It prints the folder the server chose and why (`HEALTH_DATA_DIR`, the iCloud default on a Mac, or the current directory) and whether `.health-cache.json` is there. Confirm the app has exported at least once; `get_mcp_status` shows the same source and the latest date.
353
356
  - **Server not visible in the client:** run `npx -y health-export-mcp --doctor` in a terminal. If it works there, the client can't find `npx` (common for Claude Desktop with Node from nvm or Homebrew): use the `.mcpb`, or set `"command"` to the full path from `which npx`. Then fully quit and reopen the client.
354
357
  - **Exports stopped after a week:** the app's free trial ended. `get_freshness` reports the export as stale and links the unlock page; once the app is unlocked, exports resume with no change on this side.
358
+ - **Answers miss last night, or `state: "stale_copy"`:** this Mac's iCloud Drive has an older copy of the export than the phone wrote. Wait a few minutes, or open the MetricBridge folder in iCloud Drive in Finder and choose Download Now on the file (Remove Download drops only this Mac's local copy, and it downloads again). `--doctor` lists each stale copy.
355
359
  - **Locked data error:** the export file is protected until first unlock after reboot; unlock your device once.
356
360
 
357
361
  ---
@@ -372,7 +376,7 @@ stdio transport (newline-delimited JSON-RPC 2.0), the universal MCP transport. O
372
376
 
373
377
  ## How it works
374
378
 
375
- The iOS app reads Apple Health (read-only) and writes a compact `.health-cache.json` (plus optional context files) to the destination you choose. This server reads those files and exposes the 16 tools above over MCP. No bridge, no Docker, no database: just files and stdio.
379
+ The iOS app reads Apple Health (read-only) and writes a compact `.health-cache.json` (plus optional context files) to the destination you choose. This server reads those files and exposes the 21 tools above over MCP. No bridge, no Docker, no database: just files and stdio.
376
380
 
377
381
  ```
378
382
  Apple Health → MetricBridge (iOS) → .health-cache.json → health-export-mcp → MCP client → you
@@ -388,7 +392,7 @@ Other Apple Health MCP servers in the ecosystem, worth a look depending on your
388
392
  - **[the-momentum/apple-health-mcp-server](https://github.com/the-momentum/apple-health-mcp-server):** an MCP server for analyzing Apple Health data exported from the Health app.
389
393
  - **[HealthyApps/health-auto-export-mcp-server](https://github.com/HealthyApps/health-auto-export-mcp-server):** an MCP server for the Health Auto Export app's data.
390
394
 
391
- **How `health-export-mcp` differs:** the iPhone app keeps the export current in the background, so there's no manual `export.xml` and no re-import; the server has zero dependencies and needs no Docker, database or Mac app; every answer carries its units, date range and coverage; and it's read-only end to end.
395
+ **How `health-export-mcp` differs:** the iPhone app keeps the export current in the background, so there's no manual `export.xml` and no re-import; the server has zero dependencies and needs no Docker, database or Mac app; every answer carries its units, date range and coverage; and it never writes to Apple Health: its only write is the events file your agent logs to.
392
396
 
393
397
  ---
394
398
 
package/demo.mjs CHANGED
@@ -211,10 +211,68 @@ function buildSessions(rng) {
211
211
  return { schema: 1, writtenAt: WRITTEN_AT, sessions };
212
212
  }
213
213
 
214
+ // Three weeks of a simple squat, bench and row programme, three sessions a week, adding 2.5 kg
215
+ // each week (the same shape as the app's screenshot seed). Built from fixed numbers, no PRNG, so
216
+ // the derived volume and Epley estimates are easy to check by hand.
217
+ function buildStrength() {
218
+ const epley = (kg, reps) => (reps === 1 ? kg : Math.round(kg * (1 + reps / 30) * 10) / 10);
219
+ const backs = [20, 18, 16, 13, 11, 9, 6, 4, 2];
220
+ const sessions = backs.map((back, i) => {
221
+ const add = Math.floor(i / 3) * 2.5;
222
+ const lift = (name, reps, kg, rpe, warmup) => ({
223
+ name,
224
+ volumeKg: Math.round(3 * reps * kg * 10) / 10,
225
+ sets: [...(warmup ? [{ reps: 8, kg: warmup, kind: 'warmup' }] : []),
226
+ ...[0, 1, 2].map(() => ({ reps, kg, kind: 'working', rpe }))],
227
+ bestEstimateKg: epley(kg, reps),
228
+ });
229
+ const exercises = [
230
+ lift('Back squat', 5, 80 + add, 8, 40),
231
+ lift('Bench press', 6, 60 + add, 8.5),
232
+ lift('Barbell row', 8, 50 + add, 7.5),
233
+ ];
234
+ return {
235
+ id: `demo-str-${i + 1}`,
236
+ start: `${day(back)}T18:00:00+01:00`,
237
+ day: day(back),
238
+ volumeKg: Math.round(exercises.reduce((t, e) => t + e.volumeKg, 0) * 10) / 10,
239
+ workingSets: 9,
240
+ exercises,
241
+ minutes: 55,
242
+ effort: 7,
243
+ };
244
+ });
245
+ const last = sessions[sessions.length - 1];
246
+ const records = last.exercises.map((e) => ({
247
+ exercise: e.name,
248
+ sessions: sessions.length,
249
+ lastDay: last.day,
250
+ bestEstimateKg: e.bestEstimateKg,
251
+ bestEstimateDay: sessions.find((s) => s.exercises.some((x) => x.name === e.name && x.bestEstimateKg === e.bestEstimateKg)).day,
252
+ heaviestKg: e.sets[e.sets.length - 1].kg,
253
+ }));
254
+ return { schema: 1, app: '1.21', writtenAt: WRITTEN_AT, unit: 'kg', formula: 'strength-v1', sessions, records };
255
+ }
256
+
214
257
  function buildCycles() {
215
258
  return { schema: 1, writtenAt: WRITTEN_AT, cycleStarts: [day(77), day(49), day(20)] };
216
259
  }
217
260
 
261
+ // The tag log as the app exports it with "Include tags in exports" on: creatine daily for the last
262
+ // 30 days, magnesium on alternate days, sauna every fifth day, and a few evenings of alcohol.
263
+ function buildTags() {
264
+ const days = {};
265
+ const add = (back, name) => { (days[day(back)] ??= []).push(name); };
266
+ for (let i = 0; i < 30; i++) {
267
+ add(i, 'Creatine');
268
+ if (i % 2 === 0) add(i, 'Magnesium');
269
+ }
270
+ for (let i = 0; i < 90; i += 5) add(i, 'Sauna');
271
+ for (const i of [3, 10, 17, 24, 31, 45]) add(i, 'Alcohol');
272
+ for (const d of Object.keys(days)) days[d].sort();
273
+ return { schema: 1, writtenAt: WRITTEN_AT, days, supplements: ['Creatine', 'Magnesium'] };
274
+ }
275
+
218
276
  function buildDaysFile() {
219
277
  return {
220
278
  schema: 1,
@@ -254,6 +312,8 @@ export function demoData() {
254
312
  sessions: buildSessions(rng),
255
313
  cycles: buildCycles(),
256
314
  days: buildDaysFile(),
315
+ strength: buildStrength(),
316
+ tags: buildTags(),
257
317
  };
258
318
  return _demo;
259
319
  }