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 +15 -11
- package/README.md +42 -38
- package/demo.mjs +60 -0
- package/events.mjs +459 -17
- package/glama.json +3 -3
- package/health-export.mcpb +0 -0
- package/healthstore.mjs +360 -22
- package/llms.txt +3 -3
- package/manifest.json +26 -6
- package/package.json +2 -2
- package/prompts.mjs +6 -1
- package/server.json +6 -6
- package/server.mjs +106 -19
- package/test/integration.mjs +49 -6
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 --
|
|
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
|
-
|
|
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": ["
|
|
71
|
+
"command": ["npx", "-y", "health-export-mcp"],
|
|
68
72
|
"enabled": true,
|
|
69
|
-
"environment": { "HEALTH_DATA_DIR": "
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
19
|
+
claude mcp add health-export -- npx -y health-export-mcp
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 --
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
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
|
|
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` |
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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
|
}
|