{
  "markdown": "<!-- mcp-name: io.github.anup-shesh/garmin-local-mcp -->\n\n# garmin-local-mcp\n\n**Local-first Garmin data warehouse with an analysis-grade MCP server.**\nSync once, analyze forever, even when the API is down.\n\n![garmin-local-mcp answering questions from a local store, including with the network disconnected](https://raw.githubusercontent.com/anup-shesh/garmin-local-mcp/main/docs/demo.gif)\n\n## Why another Garmin MCP?\n\nEvery existing Garmin MCP server follows the same design: a thin live wrapper\naround Garmin's rate-limited, unofficial API. Each question your AI assistant\nasks becomes one or more live API calls that return huge raw JSON blobs (a\nsingle raw sleep response runs around 230 KB). Multi-month questions like \"how\ndoes my sleep correlate with training load?\" are impractical, and when Garmin\nchanges its auth (as it did in March 2026, breaking the whole ecosystem), those\nservers go completely dark, even for data they already fetched yesterday.\n\nThis project inverts the architecture:\n\n- **Sync once, analyze forever.** Incremental sync into a local warehouse:\n  immutable raw JSON snapshots plus a SQLite database, in a directory you own.\n- **Server-side analysis, compact responses.** Trends, correlations, personal\n  baselines, and anomaly detection are computed locally and returned as small\n  columnar tables in a single tool call. Typical responses are under 2 KB, so\n  nothing floods the model's context.\n- **Offline resilience.** An API breakage pauses new syncs only. Every query\n  over already-synced history keeps working.\n- **A zero-auth fallback.** A standalone decoder for Garmin's undocumented\n  wellness FIT messages (sleep score, HRV, skin temperature, sleep stages,\n  naps) ingests manually exported bundles with no login at all. No other\n  Garmin MCP ships this.\n- **Curated tools.** 12 composable tools, not 110.\n\n| | garmin-local-mcp | Typical API-wrapper Garmin MCPs |\n|---|---|---|\n| Local data store you own | Yes (raw JSON + SQLite) | No |\n| Works offline after an API breakage | Yes (analysis over synced history) | No |\n| Server-side analysis (trends, correlations, baselines, anomalies) | Yes | No (raw JSON pass-through) |\n| Response size discipline | Compact columnar tables, typically < 2 KB | Raw payloads, up to hundreds of KB |\n| Zero-auth ingest path | Yes (FIT bundle import) | No |\n| Tool count | 12 curated | Often 20 to 110+ |\n\n## Try it without a Garmin account\n\nIf you don't own a Garmin, or just want to see what the tools return before\nhanding over credentials, seed a synthetic store:\n\n```\npip install garmin-local-mcp\ngarmin-local-mcp --data-dir ~/.garmin-mcp-demo demo\ngarmin-local-mcp --data-dir ~/.garmin-mcp-demo serve\n```\n\nThat generates 180 days across every table, then serves them over MCP. No\nlogin, no network, no account.\n\nThe data is generated rather than recorded, but it is not random. A latent\nrecovery factor drives HRV up while resting heart rate goes down, training\nload raises the *next* day's resting heart rate, a six-day illness window sits\nin the middle of the range, and a few sleep nights are deliberately missing. So\nthe analysis tools have something real to find:\n\n| Ask | Returns |\n|---|---|\n| `correlate(hrv, resting_hr)` | about −0.5, a genuine inverse relationship |\n| `correlate(training_load, resting_hr, scan_lags=True)` | ~0 at lag 0, **+0.45 at lag 1** — the effect is next-day |\n| `anomalies()` | the illness window, flagged across resting HR, HRV, skin temperature, SpO2 and sleep score at once |\n| `gaps()` | the missing sleep nights |\n\n`sync_status` reports `demo_store: true` on these stores, so an assistant can\nnever present generated numbers as real measurements. The generator is\ndeterministic — `--seed` reproduces a store exactly, and `--days` changes the\nrange. `demo` refuses to overwrite a database it did not generate.\n\n## Quickstart\n\nRequires Python 3.12+.\n\n```\npip install garmin-local-mcp\n```\n\nOr run it without installing, via [uv](https://docs.astral.sh/uv/):\n\n```\nuvx garmin-local-mcp --help\n```\n\n**1. Log in once** (MFA supported; tokens persist locally, so future runs never\nask for a password):\n\n```\ngarmin-local-mcp login\n```\n\n**2. Backfill your history.** The sync is resumable, safe to interrupt, and\nthrottled to be polite to Garmin's servers. A year of history is roughly 1,800\nrequests; for long backfills, start it and let it run (overnight works well).\nIf it gets rate limited or interrupted, re-run the same command and it resumes\nwhere it left off.\n\n```\ngarmin-local-mcp sync --from 2026-01-01\n```\n\n**3. Register the MCP server with your client** (see [Client setup](#client-setup)\nfor Claude Desktop, Cursor, and other clients):\n\n```\nclaude mcp add --scope user garmin -- garmin-local-mcp serve\n```\n\n**4. Ask questions.** Examples of what Claude can now answer from your local\nwarehouse in one or two tool calls:\n\n- \"How does my sleep score correlate with next-day resting HR?\"\n- \"What were my anomalous HRV days this quarter?\"\n- \"Show weekly training load vs sleep for the last 3 months.\"\n\n## Client setup\n\nThe server speaks stdio, so any MCP client works. `pip install garmin-local-mcp`\nfirst (or use the `uvx` variants below, which need nothing installed beyond\n[uv](https://docs.astral.sh/uv/)).\n\n**Claude Code**\n\n```\nclaude mcp add --scope user garmin -- garmin-local-mcp serve\n```\n\n**Claude Desktop, one-click:** download `garmin-local-mcp-x.y.z.mcpb` from the\n[latest release](https://github.com/anup-shesh/garmin-local-mcp/releases/latest),\nthen in Claude Desktop open Settings > Extensions > Advanced settings, click\n\"Install Extension…\", and select the file. Requires\n[uv](https://docs.astral.sh/uv/getting-started/installation/) on your PATH;\nthe extension installs and runs the server from PyPI via uvx, so no manual\nPython setup is needed. If the install dialog warns about a missing\n`Python >=3.12`, you can ignore it: uv provisions its own interpreter.\n\n**Claude Desktop, manual** (Settings, then Developer, then Edit Config; add to\n`claude_desktop_config.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"garmin\": {\n      \"command\": \"garmin-local-mcp\",\n      \"args\": [\"serve\"]\n    }\n  }\n}\n```\n\n**Cursor** (`~/.cursor/mcp.json`, or `.cursor/mcp.json` in a project):\n\n```json\n{\n  \"mcpServers\": {\n    \"garmin\": {\n      \"command\": \"garmin-local-mcp\",\n      \"args\": [\"serve\"]\n    }\n  }\n}\n```\n\n**Any other stdio client / no local install** (requires uv):\n\n```json\n{\n  \"mcpServers\": {\n    \"garmin\": {\n      \"command\": \"uvx\",\n      \"args\": [\"garmin-local-mcp\", \"serve\"]\n    }\n  }\n}\n```\n\nNote: `login` and the initial backfill `sync` are CLI steps (see\n[Quickstart](#quickstart)); the MCP server itself never prompts for\ncredentials.\n\n## The 12 tools\n\n| Tool | What it does |\n|---|---|\n| `auth_status` | Check whether stored Garmin Connect tokens exist (use before sync, or after an auth error). |\n| `sync` | Fetch up to 60 days from Garmin Connect into the local store (default: last 30 days ending yesterday; big backfills belong in the CLI). |\n| `sync_status` | Local data coverage per table, last sync time, and pending sync errors. |\n| `get_day` | One merged view of a single day: wellness, sleep, HRV, training status, performance scores, activities, and data-quality flags. |\n| `query_metrics` | Columnar time series for one or more metrics between two dates, with daily/weekly/monthly aggregation and optional stats. |\n| `correlate` | Pearson/Spearman correlation between two metrics, with day-lag support and an optional scan over lags -7..+7. |\n| `baselines` | Personal mean +/- sd band per metric over a trailing window (default 28 days), to judge what is normal for this user. |\n| `anomalies` | Outlier days (z-score deviations) and sustained streaks (5+ consecutive days on one side of the mean). |\n| `list_activities` | Recent activities newest-first as a compact table, filterable by type, date range, and minimum distance. |\n| `get_activity` | Full stored summary row for one activity (summary fields only, no GPS or sample streams). |\n| `gaps` | Missing days per table plus unresolved sync errors, to find holes worth re-syncing before drawing conclusions. |\n| `import_fit` | Zero-auth offline ingest of a manually exported Garmin wellness FIT bundle. |\n\nOnly `sync` and `import_fit` write anything, and only inside the data\ndirectory. The server never prompts: auth problems come back as structured\nerrors with a hint pointing at the login CLI.\n\nAvailable metric names include `resting_hr`, `sleep_score`, `hrv`, `steps`,\n`stress_avg`, `body_battery_high`, `skin_temp_dev_c`, `vo2max`, `fitness_age`,\n`achievable_fitness_age`, `training_load`, `endurance_score`, `hill_score`,\n`readiness_score`, `race_5k_s`, and about 35 more; any tool given an unknown\nname returns the full list.\n\n### Performance scores\n\nGarmin's periodic fitness scores land in their own `performance` table:\nendurance score, hill score (with its endurance and strength sub-scores),\ntraining readiness (score, level, recovery time) and race predictions for 5k,\n10k, half and full marathon (all in seconds).\n\nThese update on Garmin's own cadence rather than daily, so `performance` is\ndeliberately excluded from `gaps` — a day without a new endurance score is\nnormal, not a hole. Race predictions and hill score only move after qualifying\nrunning activity, so long stretches of nulls are expected for anyone whose\ntraining is mostly hiking, cycling or strength work.\n\n## Data layout and ownership\n\nEverything lives in one directory you own (default `~/.garmin-mcp`, override\nwith the `GARMIN_MCP_DATA_DIR` environment variable or `--data-dir`):\n\n```\n~/.garmin-mcp/\n├── config.toml                                  # optional settings\n├── tokens/                                      # Garmin Connect session tokens\n├── raw/daily/YYYY/YYYY-MM-DD/<endpoint>.json    # immutable raw API snapshots\n├── raw/activities/<activity_id>.json            # one snapshot per activity\n└── garmin.db                                    # SQLite warehouse\n```\n\nThe raw JSON snapshots are the source of truth and are never overwritten. The\nSQLite database is a derived, rebuildable index: `garmin-local-mcp reparse`\nrebuilds it from the raw snapshots entirely offline, which is the universal\nescape hatch for schema evolution and parser fixes. Your data never leaves\nyour machine.\n\n## Data quality note\n\nGarmin watches report a provisional on-device resting heart rate that can\ndiverge sharply from Garmin Connect's finalized value on nights with sparse\nsampling. A real observed case: the watch reported 69 bpm on-device while\nGarmin Connect later finalized the same night at 56 bpm.\n\nThis project handles that in two ways:\n\n- The API sync stores Garmin Connect's finalized value.\n- The FIT importer cross-checks the provisional on-device value against the\n  overnight heart-rate floor. A resting HR sitting more than 10 bpm above the\n  lowest overnight sample is a rate the watch never actually observed; it gets\n  flagged (`rhr_far_above_hr_floor`) and withheld, leaving the field for the\n  API to backfill rather than storing a misleading number.\n\nSparse sleep-stage logging is flagged the same way\n(`sparse_sleep_stage_logging`), and flags surface in `get_day` so the analysis\nlayer knows which numbers to trust.\n\n## Offline / fallback runbook\n\nIf Garmin breaks the unofficial API again (it has before):\n\n1. **Everything analytical keeps working.** All query, correlation, baseline,\n   anomaly, and gap tools run on your already-synced local history. Only new\n   syncs pause.\n2. **Keep ingesting without auth.** Download a daily FIT bundle from the\n   Garmin Connect website and import it locally (exact steps below).\n   `garmin-local-mcp import-fit <folder>` decodes the bundle with zero\n   authentication and fills the gap days. FIT-sourced rows never overwrite\n   API-sourced rows (unless you pass `--force`).\n3. **Resume when the community catches up.** Watch the\n   [python-garminconnect](https://github.com/cyberjunky/python-garminconnect)\n   project for a fix, upgrade, and run `garmin-local-mcp sync` again. Thanks\n   to resumable sync state, it picks up exactly where it stopped.\n\n### Downloading a wellness bundle, step by step\n\n1. Sign in at [connect.garmin.com](https://connect.garmin.com) in any\n   browser.\n2. Go directly to\n   **<https://connect.garmin.com/app/settings/accountInformation>**\n   (or click your avatar in the top-right corner, then **Settings**, then\n   **Account Information** in the left sidebar).\n3. Scroll to the bottom of the page, to the section titled\n   **Export Wellness Data** (\"Download your wellness FIT files from a\n   specific day. This includes data such as steps, sleep, stress, HRV and\n   more.\").\n4. Pick a date in the **Date** field and click **Export**. Your browser\n   downloads a small zip for that one day, containing roughly 12 to 15\n   binary `.fit` files (`*_WELLNESS.fit`, `*_SLEEP_DATA.fit`,\n   `*_HRV_STATUS.fit`, `*_SKIN_TEMP.fit`, `*_METRICS.fit`, and similar).\n5. Unzip it into a folder and run:\n\n   ```\n   garmin-local-mcp import-fit \"path/to/unzipped/folder\"\n   ```\n\n6. Repeat for each missing day (one bundle per date). The `gaps` tool or\n   `garmin-local-mcp status` tells you which days need filling.\n\nTwo things worth knowing:\n\n- **Overnight sleep belongs to the wake date.** To get last night's sleep,\n  export yesterday's date if you slept into this morning, i.e. the date you\n  woke up on.\n- This per-day export is instant and separate from Garmin's full account\n  export (the \"Data Management\" link on the same page), which is a bulk\n  archive that can take days to arrive by email and is not what\n  `import-fit` expects.\n\n## Configuration\n\nOptional `config.toml` in the data directory:\n\n| Key | Default | Meaning |\n|---|---|---|\n| `timezone` | system timezone | IANA name (e.g. `America/Denver`) used to compute \"yesterday\" for sync ranges |\n| `units` | `metric` | `metric` or `statute` |\n| `request_delay_seconds` | `1.0` | Delay between API requests during sync |\n| `baseline_window_days` | `28` | Default trailing window for the `baselines` tool |\n\nEnvironment variables:\n\n| Variable | Meaning |\n|---|---|\n| `GARMIN_MCP_DATA_DIR` | Override the data directory (default `~/.garmin-mcp`) |\n| `GARMINTOKENS` | Override the token store location (default `<data_dir>/tokens`) |\n| `GARMIN_EMAIL` / `GARMIN_PASSWORD` | Optional, for non-interactive re-login; when set, `garmin-local-mcp login` skips the prompts (MFA may still prompt if your account requires it) |\n\n## Development\n\n```\npython -m venv .venv\n.venv/bin/pip install -e .[dev]     # Windows: .venv\\Scripts\\pip install -e .[dev]\npytest\nruff check .\n```\n\nThe test suite runs fully offline against sanitized JSON fixtures and small\nFIT samples; CI never touches the live API.\n\n## Disclaimer\n\nThis project is not affiliated with, endorsed by, or supported by Garmin Ltd.\nIt uses the community [python-garminconnect](https://github.com/cyberjunky/python-garminconnect)\nlibrary with your own credentials to access your own data. Garmin's APIs are\nunofficial and can change or break at any time; when that happens, your synced\nhistory remains fully usable and the FIT import path keeps working.\n\nAll data stays on your machine. Nothing phones home: no telemetry, no\nthird-party services, no cloud. Treat your data directory like the personal\nhealth record it is, and never commit it to a repository.\n\n## License\n\nMIT\n",
  "bytes": 15345,
  "sha": "0a316e70c5972b8c9ba8ba3006abeb2ec90e7106a635f880b25b75ac20137082",
  "repo_slug": "anup-shesh/garmin-local-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_anup_shesh_garmin_local_mcp_15c622a0/readme"
}