garmin-mcp
Read-only MCP server for Garmin Connect: exposes live data tools plus analyze_*
tools that cache activity data locally and compute aggregated statistics. Built on the
unofficial garminconnect library
— see "Unofficial API — risks & rate limits" below. See Privacy for
what's read, cached and stored locally.
Tools
Live data
| Tool | Description |
|---|---|
list_activities |
Recent activities (newest first, pageable), enriched summary metrics |
list_activities_by_date |
Activities in a date range, optional type filter |
get_activity_details |
Deep per-activity data: summary, HR time-in-zones, exercise sets, splits, laps (real per-lap distance/speed/HR/elevation), weather, gear |
get_training_readiness |
Training Readiness score + factors (sleep, recovery, load balance/ACWR, stress, HRV) |
get_training_status |
VO2max, training-status phrase, acute/chronic load + ACWR, load focus, heat acclimation |
get_fitness_metrics |
VO2max (running + cycling), fitness age (with chronological and achievable fitness age) |
get_race_predictions |
Predicted finish times (seconds) for 5K / 10K / half marathon / marathon |
get_personal_records |
All-time personal records: fastest times, longest distances/durations, most steps |
get_daily_stats |
Daily summary: steps, calories, resting HR, stress, body battery |
get_sleep |
Sleep summary: total/deep/light/REM/awake, SpO2, respiration |
get_hrv |
HRV summary: last-night average, status, baseline |
get_spo2 |
Pulse-ox (SpO2): on-demand/sleep average, lowest, latest reading, 7-day average |
get_respiration |
Respiration rate: waking/sleep average, lowest/highest breaths per minute |
get_body_composition |
Weigh-in history in a date range: weight, body-fat %, muscle mass, BMI |
Analysis (cache + aggregates)
| Tool | Description |
|---|---|
analyze_backfill |
Backfill the last N days into the local cache; rebuild aggregates. Returns counts |
analyze_day |
Sync one day with deep details; rebuild aggregates; return that day's summary |
analyze_brief |
Refresh today + return the morning brief (yesterday, today's recovery/readiness, 7/14/30-day trends); appends to the dated brief history |
list_activities* returns enriched fields per activity: training effect (aerobic/
anaerobic), training load, intensity minutes, HR time-in-zones, moving/elapsed time,
speed, elevation, respiration, body-battery delta, lap count and location.
The analyze_* tools cache raw daily data under ~/.garmin-activity/days/ and write
aggregated stats (daily/weekly summaries, morning brief + dated history) to
GARMIN_OUTPUT_DIR (defaults to ~/.garmin-mcp/output).
Requirements
- Python >= 3.12 (managed by
uv/uvx) - A Garmin Connect account
- Any OS — no platform-specific credential storage
Unofficial API — risks & rate limits
Not affiliated with, endorsed by, or supported by Garmin Ltd.
Garmin has no official API for individuals. This server talks to Garmin Connect
through the reverse-engineered
garminconnect library,
which drives the same endpoints the website and mobile app use. That has
consequences worth understanding before you rely on it:
- It can break without warning. Garmin can change or retire these endpoints
at any time; when they do, tools may start returning errors until
garminconnect(and then this server) is updated. Pin versions if you need stability, and expect occasional breakage. - Terms of Service. Automated access to Garmin Connect is not covered by an official individual-developer agreement. Use this only against your own account, for personal use, and review Garmin's Terms of Service — you are responsible for how you use it. Abusive or high-volume use risks account rate-limiting or suspension.
- Built-in pacing. Bulk history sync (
analyze_backfill) sleeps ~0.3s between days to stay gentle on Garmin's servers. Individual live-data tools issue only a few requests per call. - Keep request volume modest. Prefer incremental syncs (
analyze_day/analyze_brief) over repeatedly backfilling large ranges. Backfill once for history, then refresh a day at a time. Avoid tight loops or scheduling many concurrent syncs — no official rate limit is published, so err on the side of fewer, spaced-out requests.
Setup
1. One-time interactive login
uvx --from rogo-garmin-mcp rogo-garmin-mcp-login
Prompts for your Garmin email, password and the emailed MFA code. Your
credentials are used only to obtain OAuth tokens and are never stored;
only the resumable tokens are written to ~/.garmin-mcp/tokens (0700). The
headless server resumes from those tokens and refreshes them silently, so it
never needs your email or password again. Re-run this login if the tokens
eventually expire.
2. Add to Claude Code (~/.claude/settings.json)
Installed from PyPI (recommended):
"mcpServers": {
"garmin": {
"type": "stdio",
"command": "uvx",
"args": ["rogo-garmin-mcp"]
}
}
Or from a source checkout: uv sync, then use
"command": "uv", "args": ["run", "--project", "/path/to/garmin-mcp", "rogo-garmin-mcp"].
3. Add to Claude Desktop (claude_desktop_config.json)
Edit (or create) the config file — macOS:
~/Library/Application Support/Claude/claude_desktop_config.json; Windows:
%APPDATA%\Claude\claude_desktop_config.json:
{
"mcpServers": {
"garmin": {
"command": "uvx",
"args": ["rogo-garmin-mcp"]
}
}
}
Restart Claude Desktop to pick up the new server.
Client compatibility
This is a standard stdio MCP server launched with uvx rogo-garmin-mcp, so any
MCP client that supports the stdio transport should work. Only the Claude Code
and Claude Desktop setups are documented and exercised here; the others use the
same command and are expected to work but are not separately verified.
| Client | Status | Notes |
|---|---|---|
| Claude Code | Documented / used | Config shown in Setup (~/.claude/settings.json), or claude mcp add. |
| Claude Desktop | Documented / used | Config shown in Setup. |
| Cursor | Expected to work | Register as an MCP server (stdio): command uvx, args ["rogo-garmin-mcp"]. |
| Antigravity | Expected to work | Register as an MCP server (stdio): command uvx, args ["rogo-garmin-mcp"]. |
| Generic MCP clients | Expected to work | Any client speaking the MCP stdio transport; point it at uvx rogo-garmin-mcp. |
What you can ask
Once configured, you talk to your agent in plain language and it picks the right tool. A few prompts that map cleanly onto the tools above:
- "Summarize my last 5 runs." →
list_activities/list_activities_by_date - "Am I recovered enough to train hard today?" →
get_training_readiness - "What's my training status, VO2max and fitness age?" →
get_training_status/get_fitness_metrics - "How did I sleep last night, and what was my HRV?" →
get_sleep/get_hrv - "What are my all-time personal records and predicted race times?" →
get_personal_records/get_race_predictions - "Backfill the last 90 days, then give me my morning brief." →
analyze_backfillthenanalyze_brief
Dates: all date parameters use YYYY-MM-DD. For single-date tools
(get_training_readiness, get_sleep, get_hrv, analyze_brief, etc.) it's
optional — omit it and it defaults to today. Tools that take a date range or
a specific day (list_activities_by_date, get_body_composition,
analyze_day) require the date(s) to be passed explicitly.
Example output
Tools return plain JSON. The samples below are trimmed for illustration —
real payloads carry more fields (see the tables above). The health-summary
tools (get_daily_stats, get_sleep, get_hrv) additionally omit any null
fields.
list_activities(limit=2):
[
{
"id": 12345678901,
"name": "Morning Run",
"type": "running",
"start": "2026-07-06 19:12:44",
"duration_min": 41.8,
"distance_km": 7.35,
"calories": 512,
"avg_hr": 152,
"max_hr": 176,
"aerobic_te": 3.4,
"anaerobic_te": 0.6,
"te_label": "TEMPO",
"training_load": 118,
"location": "Riverside"
},
{
"id": 12344556677,
"name": "Strength Training",
"type": "strength_training",
"start": "2026-07-05 08:03:10",
"duration_min": 24.5,
"calories": 268,
"avg_hr": 138,
"vigorous_intensity_min": 12,
"training_load": 74
}
]
get_training_readiness():
{
"date": "2026-07-07",
"score": 78,
"level": "READY",
"feedback": "READY_TO_TRAIN",
"recovery_time_min": 180,
"acute_load": 302,
"hrv_weekly_avg": 31,
"factors": {
"sleep": { "pct": 90, "rating": "GOOD" },
"recovery_time": { "pct": 100, "rating": "GOOD" },
"load_balance": { "pct": 65, "rating": "MODERATE" },
"hrv": { "pct": 80, "rating": "GOOD" }
}
}
get_daily_stats():
{
"calendarDate": "2026-07-07",
"totalSteps": 9432,
"totalDistanceMeters": 7120,
"totalKilocalories": 2568,
"restingHeartRate": 52,
"averageStressLevel": 34,
"bodyBatteryMostRecentValue": 61
}
Troubleshooting
| Symptom | Fix |
|---|---|
| Error: "the saved session is invalid or expired" | Re-run the one-time login: uvx --from rogo-garmin-mcp rogo-garmin-mcp-login (see Setup). |
| Error: "too many requests" / rate-limited | Garmin is rate-limiting the account. Wait a while, then prefer analyze_day / analyze_brief over repeated large analyze_backfill calls (see "Unofficial API — risks & rate limits" above). |
| Error: "could not reach Garmin Connect" | Network/connectivity issue — check your connection and retry. |
A metric comes back null instead of a value |
Expected, not an error — Garmin has nothing recorded for it. VO2max (get_fitness_metrics, get_training_status) and race predictions (get_race_predictions) need a compatible watch and enough recorded runs; HRV (get_hrv) and the HRV factor in get_training_readiness need overnight wear; SpO2, respiration and body-battery fields are device-dependent and may simply be absent on some hardware or days. |
get_body_composition returns an empty list |
No weigh-ins recorded in that date range — expected, not an error. |
Privacy
Your data stays on your machine. This server only ever reads from Garmin
Connect; the analyze_* tools cache what they fetch locally so trends can be
computed offline. Nothing is uploaded anywhere, and there is no telemetry.
What is stored, and where:
| Path | Contents |
|---|---|
~/.garmin-mcp/tokens (dir, 0700) |
Resumable Garmin OAuth tokens. Your email/password are never stored — only these tokens. |
~/.garmin-activity/days/YYYY-MM-DD.json |
Raw per-day cache: activities, sleep, HRV, recovery, stress, readiness. |
$GARMIN_OUTPUT_DIR (default ~/.garmin-mcp/output) |
Aggregated stats: daily/weekly summaries, morning brief and dated brief history. |
Set GARMIN_OUTPUT_DIR to relocate the aggregate output (e.g. to a directory
your agent reads from).
Retention: files are kept until you delete them. The cache only grows as you backfill or analyze more days; it is never pruned automatically.
How to wipe everything:
rm -rf ~/.garmin-activity ~/.garmin-mcp
# and, if you set a custom output dir:
rm -rf "$GARMIN_OUTPUT_DIR"
Deleting ~/.garmin-mcp/tokens also signs the server out; re-run the
interactive login to use it again.
Development
uv run pytest # run tests with coverage
uv run pytest --cov-report=html # HTML coverage report
License
Apache-2.0
Metadata
Release files for rogo-garmin-mcp 0.8.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rogo_garmin_mcp-0.8.1.tar.gz | 127.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rogo_garmin_mcp-0.8.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 158.9 kB
Release files / rogo_garmin_mcp-0.8.1.tar.gz
| Download URL | rogo_garmin_mcp-0.8.1.tar.gz |
|---|---|
| Size | 127.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9269d583b3f4de1913d715240c7dd9209e79f16d4ce35ca3fc9a8002bc55b583
|
|
BLAKE2b-256 checksum How to use checksums |
44f9e32f2528bc730f43c095ca07fb1dd57452c65ca38c58e68094dff3ee127c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.2.0 CPython/3.12.12
|
Release files / rogo_garmin_mcp-0.8.1-py3-none-any.whl
| Download URL | rogo_garmin_mcp-0.8.1-py3-none-any.whl |
|---|---|
| Size | 31.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
12a04af5004e650d6af29c8a756a9a732e50e6d921b27922ac416c0ea35873a6
|
|
BLAKE2b-256 checksum How to use checksums |
1448e1af72f8005352440c47bdaf4cf9660ee4db1d4b1f98fe086c7ace397e85
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.2.0 CPython/3.12.12
|