
healthfolder
io.github.CloudCorpRecords/healthfolder
Read a Health Access folder of Apple Watch workouts and vitals; write training plans back as files.
Documentation
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:
--dir <folder>on the command line- the
HEALTHACCESS_DIRenvironment variable (aliasTIDE_DIR) dir = "..."in~/.config/healthaccess/config.toml(the path can be overridden withHEALTHACCESS_CONFIG; zone bounds can also be set there aszones = [133, 146, 158, 169])- discovery: iCloud Drive, then
~/Library/CloudStorage/*,~/Dropbox,~/Google Drive,~/OneDrive*,~/Box, then~/Documents. A Health Access folder holds anindex.jsonof schemahealthaccess.index.v1and aworkouts/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
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
npx -y @reneturcios/healthfolder mcp
MCP client config:
{
"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_workoutonly ever stores a number the athlete said. A workout that already carries Apple's own rating (effort_source: "apple") is refused unless the call passesoverride: true— the refusal names the file, the existing rating, and the remedy, and nothing is written.push_planreports 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, markedbounds_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
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
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.
@reneturcios/healthfoldernpm install @reneturcios/healthfolderghcr.io/cloudcorprecords/healthfolder:0.1.0docker pull ghcr.io/cloudcorprecords/healthfolder:0.1.0:undefinedRelated Servers
io.github.CloudCorpRecords/abandoncheck
Judge whether a dependency is maintained, from factual signals: releases, commits, archival.
io.github.CloudCorpRecords/actionbridge
Zapier exposed actions as MCP tools via a generic action runner.
io.github.CloudCorpRecords/actionpin
GitHub Actions supply-chain hardener: audit workflows for unpinned actions and build a SHA pin plan.