healthfolder
README.md
# healthfolder
Read a Health Access folder of Apple Watch workouts and vitals, and
write training plans back into it — as plain files. The folder an app
writes from Apple Health (workouts, day files, an index, a profile) is
the whole API: this package is a local stdio MCP server plus a CLI over
that folder. No account, no network, nothing leaves the machine.
## How the folder is found
First hit wins:
1. `--dir <folder>` on the command line
2. the `HEALTHACCESS_DIR` environment variable (alias `TIDE_DIR`)
3. `dir = "..."` in `~/.config/healthaccess/config.toml` (the path can
be overridden with `HEALTHACCESS_CONFIG`; zone bounds can also be
set there as `zones = [133, 146, 158, 169]`)
4. discovery: iCloud Drive, then `~/Library/CloudStorage/*`,
`~/Dropbox`, `~/Google Drive`, `~/OneDrive*`, `~/Box`, then
`~/Documents`. A Health Access folder holds an `index.json` of
schema `healthaccess.index.v1` and a `workouts/` directory. If
discovery finds several, the server refuses to guess and lists them.
Run `healthfolder --check` to print the resolved folder, where it came
from, and the zone bounds in effect.
## CLI
```sh
npx -y @reneturcios/healthfolder --check
npx -y @reneturcios/healthfolder list --sport swim --limit 5
npx -y @reneturcios/healthfolder show 7A1C3E90
npx -y @reneturcios/healthfolder vitals --days 14
npx -y @reneturcios/healthfolder summary --weeks 4
npx -y @reneturcios/healthfolder rate 7A1C3E90 7 "legs heavy, breathing fine"
npx -y @reneturcios/healthfolder plan push ./s30.json
npx -y @reneturcios/healthfolder plan status 2026-10-24T20:22:05Z-s30
npx -y @reneturcios/healthfolder plan list
npx -y @reneturcios/healthfolder day 2026-05-14
npx -y @reneturcios/healthfolder days --from 2026-05-01 --to 2026-05-14 --groups heart,sleep
npx -y @reneturcios/healthfolder catalog
npx -y @reneturcios/healthfolder profile
npx -y @reneturcios/healthfolder ecgs
```
Every command prints pretty JSON. Exit codes: 0 success, 1 domain or
IO error, 2 usage error. `--dir` works on every subcommand.
## MCP server
```sh
npx -y @reneturcios/healthfolder mcp
```
MCP client config:
```json
{
"mcpServers": {
"healthfolder": {
"command": "npx",
"args": ["-y", "@reneturcios/healthfolder", "mcp"]
}
}
}
```
Add `"--dir", "<absolute path>"` to `args` to skip the folder search.
### Tools (13)
| Tool | What it does |
|---|---|
| `list_workouts` | Workouts from the index (deduplicated, newest first), one summary line each |
| `get_workout` | One workout in full: document, summary, parts text, zones, cardiac cost |
| `get_vitals` | Resting HR, HRV, body mass, sleep, steps, VO2max, blood pressure over the last N days |
| `training_summary` | One entry per ISO week: volume, zones, effort, cardiac cost, rings from day files |
| `rate_workout` | Store the athlete's own effort rating (1–10) in the workout file |
| `push_plan` | Validate a training plan and write `plans/<id>.json` for the app to put on the Watch |
| `get_plan_status` | A plan's state: pending, scheduled, failed or cancelled |
| `list_plans` | Every plan with its state and planned distance/time |
| `get_day` | One day file with every Health group it holds |
| `get_days` | Day files in a range (max 366 days), optionally reduced to named groups |
| `get_catalog` | `catalog.json` plus the export-coverage check |
| `get_profile` | `profile.json` as written |
| `list_ecgs` | Every ECG summary in `ecg/`, newest first |
All results are pretty-printed JSON text with floats rounded to one
decimal. Malformed arguments are JSON-RPC `InvalidParams` errors;
domain problems (unknown id, failed validation, a refusal) come back
as `isError` results with the reason in plain words.
Two safety rules are built in:
- `rate_workout` only ever stores a number the athlete said. A workout
that already carries Apple's own rating (`effort_source: "apple"`)
is refused unless the call passes `override: true` — the refusal
names the file, the existing rating, and the remedy, and nothing is
written.
- `push_plan` reports **every** validation violation in one message
(sport, name length, block/step shape, goal type and value, zone,
label length), so a plan can be fixed in one pass.
Resources: `healthaccess://index` (the deduplicated index) and the
template `healthaccess://workout/{id}` (one workout, heart-rate series
included). MCP prompts are not implemented in this build.
### Derived numbers
- **Zones.** Four bounds, five zones. Bounds come from config.toml,
else are estimated from the age in `profile.json`: max HR =
208 − 0.7 × age, bounds at 72/79/86/92 % of it (age 34 →
[133, 146, 158, 169]). Zone seconds are computed from the heart-rate
series: each sample's time until the next sample goes to that
sample's zone, only time inside the workout counts, gaps over 60 s
count as 60 s. With no bounds known, a file's own legacy zones are
shown, marked `bounds_source: "file"`.
- **Cardiac cost** (swims only) = avg HR × best freestyle 100 m pace
(s) / 60 = heart beats per 100 m; lower is better. The best 100 is
the fastest work step of at least 95 m whose overlapping laps were
at least half freestyle, normalised to 100 m; if no step qualifies
and the swim is at least 100 m, the whole-workout pace is used.
- **Summary lines** look like:
`2026-05-14 Pool swim "S12" · 1200 m · 36:06 · avg HR 129 (max 162) · Z1–2 93% · effort 6/10 (apple) · best FR 100 2:12 → 284 beats/100 m`
— each segment appears only when its data exists.
### Presence handshake
On initialize, and after tool calls at most once a minute, the server
writes `mcp/clients/<id>.json` in the folder so the app can show
"Connected". The id is the FNV-1a 64 hash of machine name + client
name; the file carries the client name/version from the initialize
request, the machine name (`HEALTHACCESS_MACHINE` overrides it), call
counts carried over between writes, and the last tool name — never
arguments or results. A failed write never fails a tool call. The CLI
writes the same file, as client `healthfolder-cli`, only from the
commands that write into the folder anyway (`rate`, `plan push`).
## Docker
```sh
docker run -i ghcr.io/cloudcorprecords/healthfolder # stdio MCP server
docker run --rm ghcr.io/cloudcorprecords/healthfolder list --dir /data
```
Mount the Health Access folder into the container and pass `--dir`
(or set `HEALTHACCESS_DIR`) so the server can find it.
## Development
```sh
npm install
npm run build # tsc -> dist/
npm test # tsx --test test/*.test.ts (synthetic folders, no network)
npm run dev # run the CLI from source
```
## License
MIT — Copyright (c) 2026 Rene Turcios.
This server cannot be deployed
Maintenance
ActivityNo data
ResponsivenessSyncing