Garmin Health MCP Server
This MCP server provides read-only access to your local Garmin Connect history, enabling Claude to query workouts, sleep, training load, fitness scores, records, and gear via natural language. It uses a sync script to pull data into a local SQLite database, ensuring privacy and offline operation.
README
Garmin Health MCP Server
A local, private MCP server that gives Claude Desktop access to your Garmin Connect history. Nothing leaves your machine: a sync script pulls your data into a local SQLite file, and the MCP server serves that file read-only.
Garmin Connect ──garth──▶ sync_garmin.py ──▶ garmin_health.db ──▶ garmin_mcp_server.py ──▶ Claude Desktop
(you run this) (local SQLite) (read-only, offline)
▲
garmin_ui.py ← point-and-click setup, if you prefer
Separating ingestion from serving means the MCP server never holds your credentials, never makes a network call, and cannot be talked into hammering Garmin's API from inside a conversation.
Quick start (no terminal needed)
If you would rather not use the command line:
- macOS / Linux -- double-click
Start Garmin Setup.command - Windows -- double-click
Start Garmin Setup.bat
The first run installs what it needs and opens a page in your browser that
walks through three steps: sign in to Garmin, download your data, and connect
it to Claude Desktop. That last step edits claude_desktop_config.json for
you -- the step people most often get wrong by hand.
You only need Python 3.10+ installed first (python.org/downloads; on Windows tick "Add Python to PATH"). Everything else is automatic, and everything stays on your computer.
Prefer the terminal? Skip to Install. The rest of this README is the manual path.
Files
| File | Purpose |
|---|---|
garmin_ui.py |
The friendly setup app: sign in, download, connect Claude Desktop |
Start Garmin Setup.command / .bat |
Double-clickable launchers for the above |
sync_garmin.py |
Phase 1 -- authenticate with Garmin, fetch every dataset, write to SQLite |
garmin_mcp_server.py |
Phase 2 -- expose the database to Claude Desktop as MCP tools |
garmin_db.py |
Shared schema, connection handling and queries used by both |
garmin_tools.py |
The eleven MCP tools, defined once and shared by both deployments |
cloud/ |
Optional hosted version: Vercel + Supabase, for phone/web access (details) |
tests/test_offline.py |
End-to-end verification with synthetic data (no Garmin account needed) |
tests/test_cloud.py |
Verification of the hosted path against a real Postgres |
garmin_db.py exists so the writer and the reader cannot drift apart on schema,
and garmin_tools.py exists so the local and hosted servers cannot drift apart
on tools.
Local or hosted?
The default is local: everything on your machine, nothing published. There is also an optional hosted deployment for reaching your data from claude.ai or your phone.
| Local (default) | Hosted (cloud/) |
|
|---|---|---|
| Where the data lives | SQLite on your laptop | Supabase Postgres |
| Garmin credentials | ~/.garth, on your machine |
A row in your database |
| Reachable from | Claude Desktop, that machine | claude.ai, phone, any Claude client |
| Sync | When you run it, or a cron job | Daily, automatic |
| Protection | It is not on a network | One bearer token |
The hosted version trades the local version's main safety property -- your health data and Garmin session never leaving your computer -- for convenience. Worth it for some people, not for others; make it a decision rather than a default.
About the setup app
garmin_ui.py is a small local web app built on Python's standard library --
it adds no dependencies. Some notes on how it behaves:
- It binds to
127.0.0.1only, so nothing on your network can reach it. - Every request must carry a one-time key generated at startup and embedded in the URL it opens. A web page in another tab can make your browser POST to localhost, so the key is what actually stops anything else driving it.
- Your Garmin password is used once to sign in and never written to disk. Only
Garmin's own session tokens are saved, to
~/.garth. - Two-factor codes are handled in the page, so no terminal prompt is involved.
- Writing to Claude Desktop's config makes a
.backupcopy first and only touches thegarmin-healthkey -- any other MCP servers you have are left exactly as they were. A config file that is not valid JSON is reported and left alone rather than overwritten.
Install
git clone <this repo> && cd Garmin-MCP
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
Requires Python 3.10+ (the code uses X | None type syntax).
Phase 1 -- sync your data
Do a dry run first. It authenticates and fetches, but writes nothing:
python sync_garmin.py --login --dry-run
You will be prompted for your Garmin email, password and (if enabled) an MFA
code. On success, OAuth tokens are saved to ~/.garth with 0600
permissions, and later runs reuse them -- no password needed:
python sync_garmin.py # 10 activities + 7 days
python sync_garmin.py --activities 50 --days 30
python sync_garmin.py --no-details # skip splits/HR zones (much cheaper)
python sync_garmin.py --db ~/data/garmin.db
One run fetches activities, sleep, body battery and readiness, training load,
VO2max/endurance/hill scores, personal records, body composition and gear.
Splits, heart-rate zones and training effect need three extra requests per
activity, so they are fetched once per activity, only for activities not yet
enriched, capped at --detail-limit (default 25) per run. A first sync of a
long history therefore fills in details over several runs rather than issuing
hundreds of requests at once.
To avoid typing credentials at all, export them first:
export GARMIN_EMAIL="you@example.com"
export GARMIN_PASSWORD='...' # leading space keeps it out of shell history in bash/zsh
python sync_garmin.py --login
Running it safely
- Every write is an upsert keyed on activity ID or calendar date, so re-running the same window updates rows in place. It will never duplicate or delete.
--dry-runprints what would be written and touches nothing.- The token directory holds live credentials for your Garmin account. Treat it
like an SSH key;
.gitignorealready excludes it and the database. - Be gentle with
--days. Several metrics cost one request per day, and Garmin will rate-limit or temporarily lock an account that hammers the API. A daily sync of the default window is well within normal app behaviour; a first-time backfill of a year is not — do it in chunks, or start with--no-details. - Schema changes are applied in place. Upgrading this project adds the new columns and tables to your existing database without losing rows, so you never need to delete and re-sync.
- If one metric fails (older watches have no Training Readiness or HRV), that dataset is logged as an error and the rest of the sync still completes.
Schedule it daily if you like -- but run --login interactively once first so
tokens exist, since MFA cannot be answered from cron:
0 7 * * * cd /path/to/Garmin-MCP && .venv/bin/python sync_garmin.py >> sync.log 2>&1
Phase 2 -- the MCP server
Eleven tools are exposed.
Workouts
| Tool | Signature | Returns |
|---|---|---|
get_recent_activities |
limit: int = 5 (max 50) |
Type, start time, duration, distance, pace, avg/max HR, calories, ascent, training effect |
search_activities |
activity_type, start_date, end_date, min_distance_km, limit |
The same, filtered -- "my runs in July", "every ride over 50 km" |
get_activity_details |
activity_id: int |
Per-lap splits, time in each HR zone, power, cadence, stride, SWOLF |
compare_activities |
activity_ids: list[int] (2-5) |
Aligned side-by-side table of the key metrics |
Recovery, load and fitness
| Tool | Signature | Returns |
|---|---|---|
get_sleep_and_readiness |
days: int = 7 |
Sleep score, deep/light/REM/awake, sleep HR, SpO2, body battery, training readiness, resting HR, HRV |
get_training_load |
days: int = 14 |
Acute (fatigue) and chronic (fitness) load, acute:chronic ratio, training status, weekly load vs optimal range |
get_fitness_scores |
days: int = 30 |
VO2max, fitness age, endurance score, hill score, and the change across the window |
get_weekly_summary |
weeks: int = 4 |
Volume, load and elevation next to the week's average sleep, resting HR and HRV |
Records, body and kit
| Tool | Signature | Returns |
|---|---|---|
get_personal_records |
-- | Lifetime PRs with the activity that set each |
get_weight_history |
days: int = 30 |
Weight, BMI, body fat, muscle mass, and the change across the window |
get_gear |
-- | Shoe/bike mileage, activity count, and life left against the wear limit |
Every tool prepends the last sync timestamp so Claude knows how fresh the data
is, and returns a clear "run sync_garmin.py first" message when the database
is missing or empty. Metrics your watch does not record show as n/a rather
than being guessed at.
On training load
get_training_load reports Garmin's own acute and chronic load rather than a
reimplemented TSS model. They are the direct equivalents of what other
platforms label CTL/ATL/TSB:
| Garmin | Elsewhere | Meaning |
|---|---|---|
| Acute load (7-day) | ATL | Fatigue |
| Chronic load (28-day) | CTL | Fitness |
| Acute:chronic ratio | TSB / form | Balance between the two, and Garmin's injury-risk signal |
Garmin only recomputes these when the watch syncs, so consecutive days can repeat and unsynced days are absent. The sync stores what Garmin returns and never interpolates.
Verify it runs before wiring it up:
python garmin_mcp_server.py # sits waiting on stdio; Ctrl-C to exit
Phase 3 -- register with Claude Desktop
Edit claude_desktop_config.json:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
{
"mcpServers": {
"garmin-health": {
"command": "/path/to/your/project/.venv/bin/python",
"args": ["/path/to/your/garmin_mcp_server.py"],
"env": {
"GARMIN_DB_PATH": "/path/to/your/project/garmin_health.db",
"GARMIN_UNITS": "metric"
}
}
}
}
Use absolute paths throughout, and point command at the virtualenv's
Python (not a bare python) so the mcp and garth packages resolve. Restart
Claude Desktop completely, then look for the tools under the 🔨 icon.
Ask things like "How did I sleep this week?", "Am I overtraining?", "Is my VO2max trending up?", "Compare my last three long runs", "How many km are on my shoes?", or "Given my load and recovery, what should I do today?"
Configuration
| Variable | Default | Meaning |
|---|---|---|
GARMIN_DB_PATH |
garmin_health.db beside the scripts |
SQLite location |
GARMIN_TOKEN_DIR |
~/.garth |
Saved OAuth tokens |
GARMIN_EMAIL / GARMIN_PASSWORD |
unset | Non-interactive login |
GARMIN_UNITS |
metric |
imperial switches to miles |
GARMIN_MCP_LOG_LEVEL |
INFO |
Server log verbosity (stderr only) |
GARMIN_DB_PATH defaults to a path beside the script rather than the working
directory, because Claude Desktop launches servers with an unpredictable CWD.
Verifying without a Garmin account
python tests/test_offline.py
This pushes realistic Garmin JSON through the real parsers, schema and tools,
then launches the server over stdio and calls all eleven tools the way Claude
Desktop does. It also exercises the setup app: its one-time-key gate, its
config merge (other MCP servers survive, a backup is written, corrupt JSON is
refused) and its plain-language error messages. It also checks that re-syncing does not duplicate rows, that a
failed endpoint does not blank out previously stored values, and that an older
database migrates in place without losing data. Verified against mcp 1.29.0
and 2.0.0.
Notes and caveats
garthis deprecated. It prints a notice on import and is no longer actively maintained, though it still works against the current Connect API. Auth is isolated inauthenticate()and fetching in thefetch_*functions, so swapping the client later means touching onlysync_garmin.py.- FastMCP moved in SDK 2.0.
mcp.server.fastmcp.FastMCPbecamemcp.server.mcpserver.MCPServer. The decorator andrun()APIs are identical, so the server tries the 1.x path first and falls back to 2.x. - Sleep is parsed from raw JSON, not garth's model. In garth 0.8.0 the
pulse-ox fields never populate: its
camel_to_snake_dictconvertsaverageSpO2HRSleeptoaverage_sp_o_2_hr_sleep, butDailySleepDTOdeclaresaverage_sp_o2_hr_sleep, so the key silently fails to match and the value is dropped. Average sleep HR and SpO2 would always beNULL. The sync script calls the same endpoint throughgarth.connectapiand maps the fields explicitly. Everything else uses garth's typed models. - Fitness scores are read from the raw endpoints too.
garth.GarminScoresDatarequires both the hill-score and endurance-score endpoints to return data and pops fixed keys from each, so a watch reporting VO2max but no hill score yields nothing at all. The sync fetches the three independently instead. - Not every metric exists on every watch. Training status, readiness, HRV,
pulse-ox, hill and endurance scores are device-dependent; weight needs a
connected scale and gear must be registered in Garmin Connect. Missing values
are shown as
n/a, never guessed. - Personal-record labels are best-effort. Garmin identifies records by a
numeric type ID. The newer
prTypeLabelKeystring is used when present; otherwise a documented fallback table names the common running, cycling and step records. An unrecognised ID is shown asRecord type Nrather than mislabelled. - Deliberately not included: device inventory, women's health, and workout upload. The first two add little to a coaching conversation and the third writes to your Garmin account, which this project avoids entirely — the sync only ever reads.
- This is not medical advice. It is your own data, summarised.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
E2B
Using MCP to run code via e2b.