Skip to main content
Glama
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.