vitals-mcp
Reads Android Health Connect export zips to ingest heart rate, sleep, SpO2, steps, and other health data for local analysis.
Works with Fitbit data written to Android Health Connect, enabling ingestion and analysis of heart rate, sleep, and SpO2 metrics.
Works with Garmin Connect data written to Android Health Connect, enabling ingestion and analysis of heart rate, sleep, and SpO2 metrics.
Syncs Health Connect export zips from a Google Drive folder via rclone, pulling the newest export before ingestion and analysis.
Ingests Google Takeout Fit JSON data to incorporate heart rate, sleep, steps, and other fitness metrics into the local database.
Works with Samsung Health data written to Android Health Connect, enabling ingestion and analysis of heart rate, sleep, and SpO2 metrics.
Provides read-only SQL querying, schema access, and metric retrieval from a local SQLite database of computed health data.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@vitals-mcp What was my recovery score and sleep last night?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
wannabe-fit
Whoop-style recovery, strain, and sleep metrics from a cheap watch, computed on your own machine from raw Android Health Connect exports. No cloud account, no subscription, no dashboard, no AI inference. The output is a SQLite database, CSV dumps, Markdown reports, and an MCP server so that Claude Code, OpenClaw, or any other agent can query your data.
Built for a CMF Watch Pro 2 that syncs through the Nothing X app. It works with any watch or app that writes heart rate, sleep, and SpO2 into Health Connect: Samsung Health, Garmin Connect, Zepp, Fitbit, Oura, Polar, Coros, and most budget wearables.
flowchart LR
W[Watch] -->|Bluetooth| APP[Vendor app]
APP -->|writes samples| HC[Android Health Connect]
HC -->|scheduled export zip| GD[Google Drive folder]
GD -->|rclone copy or manual drop| IN[data/inbox]
IN -->|vitals ingest| DB[(data/vitals.db)]
TK[Google Takeout Fit JSON] -->|vitals ingest| DB
DB -->|vitals compute| M[daily and workout_metrics tables]
M -->|vitals report| R[data/reports/latest.md]
M -->|vitals-mcp| C[Claude Code / OpenClaw / any MCP client]
M -->|vitals sql / export| CSV[CSV]Why this exists
Watches in the 50 to 100 dollar range collect the same raw signals as a Whoop band: continuous heart rate, sleep stages, SpO2, steps. What they lack is the analysis layer, and their apps keep the raw samples locked in. Health Connect changes that. Since Android 15 the phone can export its whole Health Connect database as a zip on a daily schedule. Inside is one unencrypted SQLite file with every sample every app ever wrote.
This tool reads that file and applies published sports-science formulas to it: Banister TRIMP, Karvonen zones, the impulse-response fitness model, Keytel calorie estimation, sleep debt, and a recovery score built from resting heart rate, sleep, and SpO2. Every number is reproducible from the raw tables, every constant is in one file, and every formula is documented below.
The Google Fit REST API is not an option any more. Google closed it to new sign-ups in May 2024 and ends it at the end of 2026. Health Connect exports are the durable path.
Related MCP server: hybridlog
What you get
A daily table with one row per local day:
Group | Columns |
Recovery |
|
Sleep |
|
Load |
|
Body |
|
Activity |
|
Quality |
|
A workout_metrics table with one row per session: duration, average and peak HR, percent of HR
max, zone minutes, TRIMP, strain, HR-derived kcal next to the device kcal, and 1-minute heart-rate
recovery. Plus the raw tables underneath, so you can compute anything else yourself.
Setup
Requirements: Python 3.12+, uv, an Android phone on Android 15 or newer, and a watch whose app writes to Health Connect.
git clone https://github.com/devsrijit/wannabe-fit.git
cd wannabe-fit
uv sync
cp config.example.yaml config.yaml # then edit: sex, birth year, weight, timezoneOn the phone:
In the watch app, turn on 24/7 heart rate, SpO2, and sleep tracking. Pick the shortest heart-rate interval it offers. Make sure the app is allowed to write to Health Connect.
Settings > Security and privacy > Health Connect > Manage data > Export. Set a daily schedule and pick a Google Drive folder.
Get the zip to your computer. Either:
Install rclone, run
rclone config create gdrive drive scope=drive.readonly, and setrclone.remote_path: "gdrive:<your folder>"inconfig.yaml.vitals syncthen pulls the newest zip before every run. rclone's shared Google client is being retired during 2026, so create your own OAuth client ID as described in the rclone docs.Or drop the zip into
data/inbox/by any means. Any file name works.
Then:
uv run vitals doctor # what the export contains, sample intervals, sources
uv run vitals sync # pull, ingest, compute, write data/reports/latest.md
uv run vitals today # the day cardIf doctor shows a table the watch should fill but did not, the fix is in the watch app's settings.
Run it in the background (macOS)
scripts/install-launchd.sh # every 6 hours
scripts/install-launchd.sh 3600 # every hourThis installs a launchd agent named com.wannabe-fit.sync that runs vitals sync and logs to
data/sync.log. The sync is idempotent: re-ingesting the same export changes nothing, and rclone only
transfers the zip when it changed. On Linux, a cron line running uv run --project <path> vitals sync
does the same job.
Web dashboard
web/ is a Next.js site over the same database: recovery ring, hypnogram, zone and load charts, per-workout heart-rate traces, and a glossary that explains every metric with its formula and source. See web/README.md.
cd web && bun install && bun run devInterfaces
Command line
Command | What it does |
| Inventory of an export and of the local db, sample gaps, last sync |
| Load a Health Connect zip or db, or a Takeout Fit zip or folder. Default: newest file in |
| Recompute |
| rclone pull (if configured) + ingest + compute + report |
| The latest day card |
| Markdown report with daily and workout tables |
| Read-only SQL, CSV to stdout |
| Every table as CSV |
| The db schema |
MCP server
uv run vitals-mcp starts a stdio MCP server with these tools: get_daily, get_workouts,
get_sleep, get_heart_rate, run_sql (read-only), get_schema, get_report, sync.
Register it in Claude Code:
claude mcp add --scope user vitals -- uv run --project /absolute/path/to/wannabe-fit vitals-mcpThen ask things like "compare my sleep on training days and rest days this month" or "show my heart rate during yesterday's run in 1-minute buckets". Any other MCP client takes the same command.
Files
data/vitals.db is plain SQLite. data/reports/latest.md is always the current report. Both are
gitignored, as is config.yaml, so a fork of this repo never carries personal data.
Tables
Raw tables keep epoch milliseconds in UTC. Zone offsets from Health Connect sit next to sessions.
daily uses the timezone from config.yaml. Sleep belongs to the day you woke up.
Table | Content |
| Every heart-rate sample: |
| Instant samples from the matching Health Connect tables |
| Interval totals, per source |
| Sessions and stage segments (1 awake, 4 light, 5 deep, 6 REM, 7 awake in bed) |
| Exercise sessions with the Health Connect type code, name, and source |
| Computed, rebuilt on every |
Several apps often write the same thing. Steps, calories, and distance take the largest single-source total per day rather than the sum. Auto-detected sessions from Google Fit are kept unless they overlap a session another app tracked for the same window. Both rules are configurable.
Formulas
The constants live in vitals/metrics.py.
HR max:
hr_maxin config, else Tanaka208 - 0.7 * age. Replace it with a measured peak when you have one. Every zone and strain number depends on it.Resting HR (
rhr,rhr_method): lowest 15-minute mean of HR inside the main sleep session. Fallbacks in order: lowest 15-minute mean of the whole day, the device's resting HR record, the 5th percentile of the day.Baselines:
rhr_base30andrhr_sd30are the trailing 30-day median and standard deviation, excluding today. Zones and TRIMP userhr_base30, so one bad night does not move every zone boundary.Zones: Karvonen heart-rate-reserve fractions from
config.yaml. z1 50-60%, z2 60-70%, z3 70-80%, z4 80-90%, z5 90-100%. Minutes come from each sample's gap to the next, capped athr_sample_cap_s(default 10 min) in the background and at 60 s inside workouts.TRIMP (Banister 1991):
sum(minutes * f * 0.64 * e^(1.92 f))for men,0.86 * e^(1.67 f)for women, wherefis the heart-rate-reserve fraction. Only samples withf >= 0.30count; sedentary time carries no load.trimp_edwardsissum(zone minutes * zone number).Strain (0-21):
21 * (1 - e^(-L / 155))withL = trimp_edwards. Logarithmic like Whoop, so the last points are the hardest to earn. About 60 minutes in z5 gives 18. This is not Whoop's private formula.HR calories (
kcal_hr, Keytel 2005): per minute, men(-55.0969 + 0.6309 HR + 0.1988 kg + 0.2017 age) / 4.184, only whilef >= 0.30. Validated for steady aerobic work; it overstates strength sessions. The device number sits next to it.Fitness, fatigue, form (
ctl,atl,tsb; Banister impulse-response): CTL is a 42-day exponential average of TRIMP, ATL a 7-day one, TSB is yesterday's CTL minus ATL.acwris the 7-day mean over the 28-day mean; above 1.5 is the injury-risk zone in the literature.Sleep:
sleep_asleep_minis the sum of light, deep, REM, and generic sleeping stages; time in bed is the session length.sleep_need_min = base + 60 * (yesterday's strain / 21) + min(60, 0.5 * 7-day debt).sleep_performanceis asleep over need, capped at 100.sleep_consistencyloses 25 points for each hour of circular standard deviation in bed and wake times over 7 days (the clock wraps at midnight).Recovery (0-100):
0.50 * RHR component + 0.35 * sleep performance + 0.15 * SpO2 component. The RHR component is70 - 20 * z, where z is today's RHR against the 30-day baseline. The SpO2 component maps the overnight minimum from 88% (0) to 95% (100). If HRV is present the weights become HRV 40, RHR 25, sleep 25, SpO2 10. Bands: green 67+, yellow 34-66, red below 34.VO2max (
vo2max_uth, Uth 2004):15.3 * HRmax / RHR. A population estimate, for trend only.Workout HR recovery (
hrr_1min): mean HR over the last 15 s of the session minus mean HR 50-75 s after it. Needs the watch to keep sampling after the workout ends. Above 20 bpm is good, below 12 poor.
Adapting it
Another watch: nothing to change if the app writes to Health Connect. Run
vitals doctoron the export to see which tables are filled. If the watch provides HRV, recovery picks it up on its own.Older history:
vitals ingestalso reads a Google Takeout export of Google Fit (the zip or theFit/All Datafolder). Fit auto-detected walks arrive as workouts with sourceFit.Different formulas: every constant is in
vitals/metrics.py, andcomputerebuilds the derived tables from raw samples, so changes apply to all history at once.Testing without a phone:
uv run python scripts/make_fixture.py /tmp/fixture.zipbuilds a synthetic 35-day export in the real Health Connect layout. Ingest it from/tmp, never fromdata/inbox, orsyncmixes synthetic rows into your real database.
Limits
Health Connect has no data type for vendor stress scores, so they do not export.
Background HR interval is whatever the watch app allows.
doctorprints the real median gap.Health Connect keeps 30 days of most data on the phone by default. Daily exports snapshot everything, so nothing is lost while the schedule runs.
The recovery score without HRV leans on resting HR, sleep, and SpO2. It is a trend instrument, not a diagnosis.
License
MIT.
Available Tools
8 toolsget_dailyB
Daily metrics (recovery, RHR, sleep, strain, TRIMP, CTL/ATL/TSB, SpO2, steps). ISO dates, newest first.
Inputs: limit, date_to, date_from. Returns: result
get_heart_rateB
Raw HR samples between two ISO timestamps (local tz), averaged into buckets. bucket_minutes=0 for raw.
Inputs: end_iso (required), start_iso (required), bucket_minutes. Returns: result
get_reportC
Markdown report: today's card, daily table, workouts, 7-day means.
Inputs: days. Returns: result
get_schemaA
CREATE TABLE statements for every table, plus the column list of daily and workout_metrics.
Inputs: no named parameters. Returns: result
get_sleepC
Sleep sessions with stage minutes, efficiency, need, performance, debt, consistency.
Inputs: days. Returns: result
get_workoutsC
Per-workout metrics: HR avg/max, zone minutes, TRIMP, strain, HR-derived kcal, 1-min HR recovery.
Inputs: days. Returns: result
run_sqlA
Read-only SQL against the vitals db. Use get_schema first.
Inputs: query (required). Returns: result
syncB
Ingest the newest export in the inbox, recompute metrics, and rewrite reports/latest.md.
Inputs: no named parameters. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 8 tools
Tools target distinct resources and actions: daily aggregates, workouts, sleep sessions, raw heart rate, schema, SQL, reports, and sync. Minor overlap exists because get_daily summarizes sleep and get_report aggregates daily/workout data, but descriptions make boundaries clear.
Six read tools follow get_<noun> (get_daily, get_workouts, get_sleep, get_heart_rate, get_schema, get_report), while run_sql and sync are action-oriented exceptions. Mostly consistent, though get_daily is slightly less noun-specific than the others.
Eight tools are well-scoped for a personal vitals and analytics MCP. The set covers core queries, schema inspection, reporting, and ingestion without bloat or obvious redundancy.
The surface covers daily metrics, workouts, sleep, raw HR, arbitrary SQL, schema inspection, reporting, and sync/ingest, leaving no obvious dead ends for read and analytics workflows. Manual CRUD is absent, but the server is designed around exports and recomputation.
Related MCP Connectors
Your swim, bike and run training: readiness, HRV, sleep, workouts and race predictions. Read-only.
Apple Health training load, recovery, HRV and workout detail for Claude, ChatGPT and any MCP client.
Read-only Apple Health data synced from your iPhone: sleep, heart rate, HRV, workouts and trends.
Multi-tenant hosted MCP server for Oura Ring — 21 read-only tools, OAuth per user.
Related MCP Servers
- AlicenseBqualityAmaintenanceDownloads all your Garmin health and fitness data into a local SQLite database and exposes 45 MCP tools for AI analysis, enabling assistants to query sleep, training load, HRV, and more.48104 PyPI173AGPL 3.0
- AlicenseNot gradedqualityBmaintenanceMCP server that mirrors your Garmin data into a personal database and exposes tools for health summaries, training load, muscle readiness, and race analysis, with optional chat-driven insights via stdio or HTTP.1MIT
- AlicenseNot gradedqualityDmaintenanceA read-only MCP server that synchronizes Garmin Connect summaries into a local SQLite cache and provides tools to query daily activity, recent activities, and recovery data.MIT
- AlicenseNot gradedqualityBmaintenanceEnables MCP hosts to query and analyze a local Samsung Health mirror, with tools for status, metadata, inventory, daily summaries, and aggregate analyses, plus optional bounded access to raw records and cloud synchronization.5MIT