Metadata-Version: 2.4
Name: rogo-garmin-mcp
Version: 0.8.0
Summary: MCP server for Garmin Connect data and activity analysis
Project-URL: Homepage, https://gitlab.com/therogoman-public/garmin-mcp
Project-URL: Repository, https://gitlab.com/therogoman-public/garmin-mcp
Project-URL: Changelog, https://gitlab.com/therogoman-public/garmin-mcp/-/blob/main/CHANGELOG.md
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: fastmcp>=2.0
Requires-Dist: garminconnect>=0.2.19
Requires-Dist: pydantic>=2.0
Requires-Dist: requests>=2.28
Description-Content-Type: text/markdown

# garmin-mcp

[![PyPI version](https://img.shields.io/pypi/v/rogo-garmin-mcp.svg)](https://pypi.org/project/rogo-garmin-mcp/)
[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/downloads/)
[![pipeline status](https://gitlab.com/therogoman-public/garmin-mcp/badges/main/pipeline.svg)](https://gitlab.com/therogoman-public/garmin-mcp/-/pipelines)
[![coverage report](https://gitlab.com/therogoman-public/garmin-mcp/badges/main/coverage.svg)](https://gitlab.com/therogoman-public/garmin-mcp/-/pipelines)

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`](https://github.com/cyberjunky/python-garminconnect) library
— see "Unofficial API — risks & rate limits" below. See [Privacy](#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`](https://github.com/cyberjunky/python-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

```bash
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):

```json
"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`:

```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](#2-add-to-claude-code-claudesettingsjson) (`~/.claude/settings.json`), or `claude mcp add`. |
| Claude Desktop | Documented / used | Config shown in [Setup](#3-add-to-claude-desktop-claude_desktop_configjson). |
| 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_backfill` then `analyze_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)`:

```json
[
  {
    "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()`:

```json
{
  "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()`:

```json
{
  "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](#1-one-time-interactive-login)). |
| 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:**

```bash
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

```bash
uv run pytest                         # run tests with coverage
uv run pytest --cov-report=html       # HTML coverage report
```

## License

Apache-2.0
