Health Access MCP / Reference / Files

Reference

The folder is the API

Everything the MCP serves is a plain JSON file in the folder you picked, written by the phone, readable by anything. The MCP is a convenience on top. Here is the layout, an example file trimmed to its shape, and the rules.

Layout

Health Access/                            # the folder you picked, any name
  index.json                              # one row per workout, newest first; the app rewrites it after every export
  briefing.md                             # one page any AI can read
  workouts/
    2026-05-14_0712_PoolSwim_7A1C3E90.json   # <local date_time>_<Activity>_<first 8 of the HealthKit UUID>
    2026-05-12_0630_IndoorRow_3F8B2D41.json
  vitals/
    2026-05-14.json                       # one per day, rewritten when values change
  days/
    2026-05-14.json                       # one per day, every Health group (v0.4; vitals/ stays for one release)
  catalog.json                            # which Health types have data, and where each was exported (v0.4)
  profile.json                            # age, birth year, sex, blood type, move mode (v0.4)
  plans/
    2026-05-13T19:02:11Z-s12.json         # written by your AI (push_plan) or the CLI
    2026-05-13T19:02:11Z-s12.status.json  # written by the app: did it reach the Watch
  mcp/clients/
    <id>.json                             # presence file written by the MCP: client, machine, counts

Who writes what

PathWritten byRead by
workouts/*.jsonThe app, once per workout. Never overwritten by export. The MCP or CLI may update exactly two keys, effort and feel, through rate_workout.MCP, CLI, your AI, you
vitals/*.jsonThe app, today and yesterday on every run; 30-day backfill from Settings.MCP, CLI, your AI
days/*.json, catalog.json, profile.jsonThe app, from v0.4: every Health group per day, the list of types with data and where each was exported, the athlete's characteristics.MCP, CLI (healthaccess day, catalog, profile; doctor checks the coverage), your AI
index.jsonThe app, after every export and every rating. The MCP updates a row's effort after rate_workout, never adds rows.MCP (for listing), CLI
plans/<id>.jsonThe MCP (push_plan) or the CLI (healthaccess plan push). Your AI can also write it directly.The app
plans/<id>.status.jsonThe app, after "Put on Watch".MCP (get_plan_status, list_plans)
briefing.mdThe app, after every sync.Any AI, a coach, you
mcp/clients/<id>.jsonThe MCP, on start and after tool calls, at most once a minute: client name and version, machine name, call count, last tool name. Never arguments or results.The app, to show "Connected · Claude Code on Alex's MacBook Pro · last seen 2 min ago"

Nothing else writes. Nothing ever deletes: a duplicate workout (same start within 2 seconds, same source) stays on disk and is left out of index.json. All JSON is UTF-8 with sorted keys; all timestamps are ISO 8601 in UTC with a Z; the local time is in the file name and in metadata.HKTimeZone.

An example workout, trimmed

This is workouts/2026-05-14_0712_PoolSwim_7A1C3E90.json from the synthetic example folder in the repository (generated by a script, not a real person): a 1,200 m pool swim, swum from the plan S12 on the plans page. The full file has 24 steps, 24 lengths and 431 heart-rate samples; here three steps, two lengths and three samples stand in for them. Everything else is as written; keys are sorted on disk.

{
  "schema": "healthaccess.workout.v1",
  "healthaccess_version": "0.4.0",
  "id": "7A1C3E90-5B2D-4F61-9C8E-2A7D40B6E1F3",
  "activity": "pool_swim",
  "source": "Apple Watch",
  "start": "2026-05-14T05:12:04Z",
  "end": "2026-05-14T05:48:10Z",
  "duration_s": 2166,
  "distance_m": 1200,
  "active_kcal": 338.6,
  "avg_hr": 129,
  "max_hr": 162,
  "pool_length_m": 50,
  "plan_ref": {"plan_id": "2026-05-13T19:02:11Z-s12", "name": "S12"},
  "steps": [
    {"key": "0.0.0", "block": 0, "rep": 0, "step": 0, "kind": "warmup", "label": "FR easy, find your breathing",
     "start": "2026-05-14T05:12:04Z", "end": "2026-05-14T05:16:43Z", "duration_s": 279,
     "distance_m": 200, "avg_hr": 100, "pace_s_per_100m": 139.5, "goal_met": true},
    {"key": "1.0.0", "block": 1, "rep": 0, "step": 0, "kind": "work", "label": "DRILL 6-3-6: 6 kicks side, 3 strokes",
     "start": "2026-05-14T05:16:43Z", "end": "2026-05-14T05:18:08Z", "duration_s": 85,
     "distance_m": 50, "avg_hr": 125, "pace_s_per_100m": 170.0, "goal_met": true},
    {"key": "2.3.0", "block": 2, "rep": 3, "step": 0, "kind": "work", "label": "FR 100 steady, breathe every 3",
     "start": "2026-05-14T05:34:44Z", "end": "2026-05-14T05:36:56Z", "duration_s": 132,
     "distance_m": 100, "avg_hr": 151, "pace_s_per_100m": 132.0, "goal_met": true}
  ],
  "laps": [
    {"start": "2026-05-14T05:12:07Z", "end": "2026-05-14T05:13:14Z", "duration_s": 67, "stroke": "freestyle", "swolf": 88.0},
    {"start": "2026-05-14T05:13:14Z", "end": "2026-05-14T05:14:22Z", "duration_s": 68, "stroke": "freestyle", "swolf": 90.0}
  ],
  "hr_series": [["2026-05-14T05:12:05Z", 84], ["2026-05-14T05:12:10Z", 85], ["2026-05-14T05:48:06Z", 115]],
  "recovery_hr": {"end": 115, "1min": 104, "2min": 96},
  "effort": 6,
  "effort_source": "apple",
  "effort_estimated": 5,
  "feel": null,
  "metadata": {"HKAverageMETs": "6.84 kcal/hr·kg", "HKIndoorWorkout": "0", "HKLapLength": "50 m", "HKSwimmingLocationType": "1", "HKTimeZone": "Europe/Berlin"}
}

What the fields mean

activity
pool_swim, open_water_swim, indoor_row, run, walk, cycle, or other:<HealthKit raw type>.
zones
Not written any more. Zone seconds are computed by the MCP and the command line from hr_series, with the four upper bounds from zone_bounds in ~/.config/healthaccess/config.toml or, without them, an estimate from the age in profile.json (max heart rate 208 minus 0.7 times age; zones end at 72, 79, 86 and 92 percent of it). Every zone result says which it used (bounds_source). Files from before v0.4 may still carry app-computed zones; they are accepted.
steps
One per interval of a structured workout. key is Apple's block.rep.step, with 0.0.0 the warm-up and a trailing single step the cool-down. label is the step name from the plan your AI wrote; Apple drops step names on the way to HealthKit, so the app joins them back from the matching plan. A free workout has steps: [].
laps
One per pool length: stroke (freestyle, backstroke, breaststroke, butterfly, mixed, kickboard) and SWOLF.
hr_series
Heart rate about every 5 seconds, at least 4 seconds apart. The MCP leaves it out unless asked.
recovery_hr
Heart rate at the end, one minute and two minutes after. null right after a background export; filled in on the next run.
effort, feel
Your rating, 1 to 10, and a line of text. The rating comes from your Watch, which asks after a workout; Apple exports it and the file says effort_source: apple. The app never asks. rate_workout writes ai after you told the AI a number; files from older builds may say app. effort_estimated is Apple's own estimate and is never used as your effort.
plan_ref
The plan this session was started from, matched by the WorkoutKit id the app recorded, else by sport and scheduled time within two hours. null for a workout started on the Watch by hand.
metadata
Apple's raw workout metadata as strings. HKLapLength is where the pool length comes from.

Vitals, one file per day

{
  "schema": "healthaccess.vitals.v1",
  "date": "2026-05-14",
  "resting_hr": 54,
  "hrv_sdnn_ms": 58.3,
  "sleep_s": 26160,
  "body_mass_kg": 78.4,
  "steps": 9412,
  "vo2max": 44.1,
  "bp": null
}

Every field can be null; a day on which only steps were recorded gives a file full of nulls. Sleep is credited to the morning it ended and summed from the one source with the most sleep, so Watch and iPhone are not double-counted. HRV is the day's average; resting heart rate, weight, VO2max and blood pressure are the day's latest. A day with no values gets no file.

days/, one file per day with everything v0.4

From v0.4 the app writes days/<date>.json next to vitals/, with every Health group that has data that day: activity (the rings, steps, distances, energy, exercise and stand minutes, daylight), heart, respiratory, sleep (stages and sessions), body, blood, mobility, hearing, nutrition, mind, symptoms, cycle, medications, hygiene, uv, wheelchair. A group is present only when it has data; inside a present group, absent values are an explicit null. The example day, trimmed to three groups:

{
  "schema": "healthaccess.day.v1",
  "date": "2026-05-14",
  "activity": {
    "rings": {
      "exercise_min": {
        "actual": 47,
        "goal": 30
      },
      "move_kcal": {
        "actual": 712.3,
        "goal": 650
      },
      "move_mode": "active_energy",
      "stand_hours": {
        "actual": 13,
        "goal": 12
      }
    },
    "steps": 9412,
    "distance_walking_running_m": 7180.4,
    "distance_swimming_m": 1200,
    "active_energy_kcal": 712.3,
    "exercise_min": 47,
    "time_in_daylight_min": 63
  },
  "heart": {
    "resting_hr": 54,
    "hr": {
      "avg": 72,
      "max": 162,
      "min": 47,
      "samples": 1184
    },
    "hrv_sdnn_ms": 58.3,
    "vo2max": 44.1
  },
  "sleep": {
    "asleep_s": 26160,
    "deep_s": 4980,
    "rem_s": 5760
  }
}

Two more files come with it. catalog.json lists every HealthKit type the app knows, with has_data, the sample count over 90 days, first and last date, and exported_in (workouts, days, ecg, profile or none), so healthaccess doctor can say "a type with data was exported nowhere" the day it happens. profile.json carries age and birth year (not the birthday), biological sex, blood type, wheelchair use, move mode and skin type. The full contract: docs/export-coverage.md.

index.json

{
  "schema": "healthaccess.index.v1",
  "updated": "2026-05-14T06:03:17Z",
  "workouts": [
    {"id": "7A1C3E90-5B2D-4F61-9C8E-2A7D40B6E1F3", "file": "workouts/2026-05-14_0712_PoolSwim_7A1C3E90.json",
     "activity": "pool_swim", "start": "2026-05-14T05:12:04Z", "duration_s": 2166, "distance_m": 1200,
     "avg_hr": 129, "effort": 6, "effort_source": "apple", "plan_name": "S12", "source": "Apple Watch"},
    {"id": "3F8B2D41-9E6C-4A17-B2D8-5C3E7F9A1B04", "file": "workouts/2026-05-12_0630_IndoorRow_3F8B2D41.json",
     "activity": "indoor_row", "start": "2026-05-12T04:30:11Z", "duration_s": 1680, "distance_m": null,
     "avg_hr": 137, "effort": 7, "effort_source": "apple", "plan_name": "R4", "source": "Apple Watch"}
  ]
}

Newest first, no series. The MCP lists from the index and opens a workout file only on demand. If the index lags behind the files, which happens, ids are also resolved by scanning workouts/.

Plans and their status

Your AI writes plans/<id>.json; the app answers with plans/<id>.status.json. The format, the rules and the "Put on Watch" flow have their own page. The status file for the plan behind the example swim, as the app wrote it after WorkoutKit scheduled the session for the next morning:

{
  "plan_id": "2026-05-13T19:02:11Z-s12",
  "state": "scheduled",
  "at": "2026-05-13T19:04:40Z",
  "method": "schedule",
  "workoutkit_id": "D4E7A2C1-6F3B-4E8A-9B5C-1A2F3E4D5C6B",
  "scheduled_for": "2026-05-14T05:00:00Z",
  "error": null
}

mcp/clients/<id>.json

The one file the MCP writes that is not a plan: a presence note, so the app can show that an AI is connected. Written on server start and after tool calls, at most once a minute, one file per machine and client, overwritten in place. It never contains tool arguments or results.

{
  "schema": "healthaccess.client.v1",
  "id": "9f3c1a7e5b2d4c80",
  "client": {
    "name": "Claude Code",
    "version": "2.1.0"
  },
  "machine": "Alex's MacBook Pro",
  "mcp_version": "0.2.0",
  "first_seen": "2026-05-09T17:20:41Z",
  "last_seen": "2026-05-14T06:11:02Z",
  "last_tool": "list_workouts",
  "calls": 123
}

The app shows "Connected · Claude Code on Alex's MacBook Pro · last seen 2 min ago" when any file in the folder is newer than 7 days, else "Not connected yet".

briefing.md

One page of Markdown the app rewrites after every sync, so an AI without the MCP (ChatGPT, a Claude Project, a local model, a human coach) still gets the picture. Sections, in this order: the title with the athlete's name and the time written; Last 4 weeks, one line per ISO week with sessions, distance, time and mean effort; Recent sessions, newest first, at most ten, one line each; Vitals, the latest resting heart rate, HRV, sleep and weight with their dates and the 21-day direction; Next session, the newest plan not yet on the Watch; Open, unrated sessions and plans waiting for a tap; Limits, the two fixed sentences about unlocking and pool length. No charts, no JSON, under 60 lines. An example: examples/briefing.md.

Files from before the rename

The project was called "Tide" until 2026-10-25. Files written before that carry "schema": "tide.workout.v1" (or tide.workout.v0, the first format), tide.vitals.v1, tide.index.v1, tide.plan.v1, and the key tide_version instead of healthaccess_version. Every reader accepts both for good; nothing is rewritten just to change a tag. The same goes for the environment variable TIDE_DIR and the resource URIs tide://index and tide://workout/<id>.

The complete contract, with every field and the deviations the app documents, is docs/schema.md in the repository.