MCP server for Garmin Connect data and activity analysis
Project description
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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rogo_garmin_mcp-0.8.0.tar.gz.
File metadata
- Download URL: rogo_garmin_mcp-0.8.0.tar.gz
- Upload date:
- Size: 124.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
004bec38c4dae2fb0bf7ab357dbaa1867a9a825a748f4c454e3eff3bc7682396
|
|
| MD5 |
0eb1572c4c780077a55165c31a431c44
|
|
| BLAKE2b-256 |
73a1ac8f0603f900284e40fb2b87a6eeed272009152c98091f72643c7cbb3bf9
|
File details
Details for the file rogo_garmin_mcp-0.8.0-py3-none-any.whl.
File metadata
- Download URL: rogo_garmin_mcp-0.8.0-py3-none-any.whl
- Upload date:
- Size: 31.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0179c568ddd3d36ffe7705dcf84be55123dde280cbd1ffa2ca37da286fd93c55
|
|
| MD5 |
15ca8d10fa5a0bcca068ca2f8a09881c
|
|
| BLAKE2b-256 |
5eeb66341f891cdda499788036e1885cad6c23c78d1110d3996889866640772d
|