Garmin Health MCP Server
Provides tools to access Garmin Connect health and fitness data, including activities, sleep, training load, fitness scores, personal records, and more.
Click on "Install 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., "@Garmin Health MCP ServerHow did I sleep this week?"
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.
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)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.
Files
File | Purpose |
| Phase 1 -- authenticate with Garmin, fetch every dataset, write to SQLite |
| Phase 2 -- expose the database to Claude Desktop as MCP tools |
| Shared schema, connection handling and queries used by both |
| End-to-end verification with synthetic data (no Garmin account needed) |
garmin_db.py exists so the writer and the reader cannot drift apart on schema.
Related MCP server: Garmin Connect MCP Server
Install
git clone <this repo> && cd Garmin-MCP
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtRequires 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-runYou 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.dbOne 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 --loginRunning 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>&1Phase 2 -- the MCP server
Eleven tools are exposed.
Workouts
Tool | Signature | Returns |
|
| Type, start time, duration, distance, pace, avg/max HR, calories, ascent, training effect |
|
| The same, filtered -- "my runs in July", "every ride over 50 km" |
|
| Per-lap splits, time in each HR zone, power, cadence, stride, SWOLF |
|
| Aligned side-by-side table of the key metrics |
Recovery, load and fitness
Tool | Signature | Returns |
|
| Sleep score, deep/light/REM/awake, sleep HR, SpO2, body battery, training readiness, resting HR, HRV |
|
| Acute (fatigue) and chronic (fitness) load, acute:chronic ratio, training status, weekly load vs optimal range |
|
| VO2max, fitness age, endurance score, hill score, and the change across the window |
|
| Volume, load and elevation next to the week's average sleep, resting HR and HRV |
Records, body and kit
Tool | Signature | Returns |
| -- | Lifetime PRs with the activity that set each |
|
| Weight, BMI, body fat, muscle mass, and the change across the window |
| -- | 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 exitPhase 3 -- register with Claude Desktop
Edit claude_desktop_config.json:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:
%APPDATA%\Claude\claude_desktop_config.jsonLinux:
~/.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 |
|
| SQLite location |
|
| Saved OAuth tokens |
| unset | Non-interactive login |
|
|
|
|
| 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.pyThis 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 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.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityDmaintenanceThis Model Context Protocol (MCP) server connects to Garmin Connect and exposes your fitness and health data to Claude and other MCP-compatible clients.1MIT
- Alicense-qualityDmaintenanceAn MCP server that gives Claude access to your Garmin Connect fitness and health data, including steps, sleep, activities, heart rate, and more.1MIT
- AlicenseAqualityBmaintenanceA read-only MCP server that gives Claude Desktop access to your Garmin Connect data — daily health metrics, sleep, activities, training status, and body composition.6MIT
- Alicense-qualityCmaintenanceRead-only MCP server that exposes Garmin Connect health data from a local SQLite database to AI assistants, enabling queries for recent days, sleep, weekly trends, and a guarded SQL query surface.MIT
Related MCP Connectors
Garmin data in Claude: 135 tools — activities, sleep, HRV, training, workouts. Free, open source.
Garmin data in Claude & ChatGPT via the Garmin Health API. OAuth sign-in, no password sharing.
MCP server for Withings health data — sleep, activity, heart, and body metrics.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Matt-y-Matt/Garmin-MCP'
If you have feedback or need assistance with the MCP directory API, please join our Discord server