garmin-mcp

garmin-mcp

A local, single-user, read-only MCP server that gives Claude Code access to your Garmin health and training data, exposing tools for health snapshots, training status, run details, body metrics, and training analysis.

Category
访问服务器

README

garmin-mcp

CI Python 3.12+ License: MIT

A local, single-user, read-only MCP server that gives Claude Code access to your own Garmin health and training data. It exposes six composite tools — health snapshot, training status, recent runs, run detail, body metrics and training analysis — so Claude can ground recovery advice and workout recommendations in your actual sleep, HRV, training load, and run history instead of guessing. Everything Garmin returns is cached day-by-day in a local SQLite database, so repeated questions don't repeatedly hit Garmin's servers. A local dashboard renders the result.

The garmin-mcp dashboard, showing the verdict hero, split board and focus
cards

Synthetic demo data, not real health data — generated from a fabricated 8-week training block by scripts/make_demo_screenshot.py, which is committed and runnable end to end:

pip install -e '.[dev]'
playwright install chromium
python scripts/make_demo_screenshot.py   # writes docs/dashboard-demo.png

Every dependent figure in that data (efficiency factor, HR-zone shares, weekly/block rollups) is computed from a handful of free variables by this repo's own analysis code, not typed in independently — see scripts/demo_payloads.py and tests/test_demo_payloads.py. See tests/js/fixtures/ for the (separate, simpler) fixtures the browser test suite itself uses.

Requirements

  • Python 3.12 or newer (this machine runs 3.14)
  • A Garmin Connect account
  • Optionally, the claude CLI (to auto-register the MCP server — install.sh looks for it on PATH, then ~/.local/bin, /opt/homebrew/bin, and /usr/local/bin)

Install

git clone https://github.com/danielsuri/garmin-mcp.git
cd garmin-mcp
./install.sh

Then run the one-time login (details below):

.venv/bin/garmin-mcp login you@example.com

install.sh creates .venv, installs the package into it, finds the claude CLI and registers the server at user scope if found, and symlinks the garmin-insights skill into ~/.claude/skills/. It's idempotent — safe to re-run any time; it never clobbers an existing venv, registration, or skill symlink it doesn't own. Pass --dev to also install the test extras (pytest, playwright — skip this if you're only going to use the server), --dry-run to preview every action without doing it, or --help for the full rundown.

The login step is deliberately separate and manual: Garmin requires an interactive login the first time (email, password, and an MFA code if you have two-factor enabled), and it must run in a real terminal — it reads your password with getpass, which needs an actual TTY and fails with EOFError under a non-interactive shell or piped subprocess. install.sh will offer to run it for you if your shell is interactive, and skips it cleanly otherwise so a scripted/CI install never hits an unannounced password prompt.

On success, tokens are written to ~/.garminconnect. They're good for roughly a year; after that (or if Garmin invalidates them) re-run login.

Check the install

.venv/bin/garmin-mcp doctor

doctor checks three things: that the stored tokens are accepted, that a live call to Garmin actually succeeds, and that the cache directory (~/.garmin-mcp/) is writable. Healthy output looks like:

auth: OK (tokenstore accepted)
live call: OK (userProfileId=12345678)
cache path: OK (/Users/you/.garmin-mcp/cache.db)

Any FAIL line points at what to fix — usually re-running login if auth fails, or a permissions problem if the cache path fails.

Register with Claude Code

install.sh does this automatically when it can find the claude CLI. To do it by hand instead (or if it couldn't find claude):

claude mcp add -s user garmin -- <your/install/dir>/garmin-mcp/.venv/bin/garmin-mcp serve

This runs the server over stdio. Once registered, a fresh Claude Code session will have the six tools below available — MCP servers are loaded at startup, so the session you register from won't see them.

-s user matters: without it claude mcp add defaults to local scope, and the tools appear only when you're working inside this repo. User scope makes them available in every project, which is what you want for a personal health server.

Note the registration is an absolute path into this repo's venv, so moving or deleting the repo breaks it (re-run install.sh, or the command above, after moving it). Tokens (~/.garminconnect) and the cache (~/.garmin-mcp/cache.db) live outside the repo and are unaffected by scope.

The six tools

  • get_health_snapshot(days=7) — sleep (total/deep/light/REM/awake minutes, sleep score), HRV, body battery (the day's true high and low), resting heart rate, and stress over the window, plus rolling baselines. Use this to judge how recovered you are.
  • get_training_status(weeks=4) — acute and chronic training load, the acute:chronic load ratio, VO2max, and training readiness score per day. Use this to judge whether training load is ramping safely.
  • get_recent_runs(n=10) — your last n runs with pace (average, moving, grade-adjusted, best), heart-rate zones, training effect and training load, VO2max, fastest splits, PR flag, calories, body-battery cost and elevation, plus weekly mileage rollups and race-time predictions. Use this to spot trends and prescribe a specific run.
  • get_run_detail(activity_id=None) — one run in full: everything get_recent_runs reports for that run, plus running dynamics (ground contact time and balance, vertical oscillation and ratio, stride length), power (average/normalized/max watts and power zones), respiration (average/max/min breaths per minute), min/max temperature, lap count, step count, activity name, and run/walk/idle time detection (run_walk: seconds spent running vs. walking vs. stopped, from Garmin's typed splits). Omit activity_id for the most recent run found in the last 56 days. Use this to deep-dive a specific run. Costs one extra Garmin API call beyond get_recent_runs (typed splits, for run/walk detection) — each of the other fields comes from the same activity record get_recent_runs already fetches.
  • get_body_metrics(days=30) — weight, body fat, steps, calories, floors climbed, and intensity minutes, with simple weight and step trends.
  • get_training_analysis(weeks=8) — the marathon-block view: within-run form indicators (efficiency factor, aerobic decoupling, ground-contact-time drift, duty factor, split pattern) for every run in the window, plus across-block trends (weekly mileage, ramp rate with the base it was computed from, HR-zone polarization, efficiency and decoupling trend, recovery coupling, VO2max trend). It computes; it does not advise. Costs two extra Garmin API calls per run it hasn't seen before (laps and typed splits), both cached permanently. This is what the dashboard's split board and eight-week panel are drawn from.

Arguments are clamped to a range the server can actually serve — days to 1–365, weeks to 1–52, n to 1–100, activity_id to a non-negative 64-bit value — so an out-of-range value narrows or widens to the nearest bound (or, for an activity_id that matches nothing, yields a clean all-null result) rather than failing.

Every tool returns a partial: true flag when it couldn't be certain all of its data is fresh or complete — see Troubleshooting below.

The dashboard

.venv/bin/garmin-mcp dashboard            # opens http://127.0.0.1:8765/
.venv/bin/garmin-mcp dashboard --port 9000 --no-open

A single page that answers "do I run today, and what": the verdict and prescription in large type at the top, then the split board — every run in the last eight weeks as a bar whose width is its distance and whose colour is its effort, cold (#5FD0E0) for a Z1–2 easy run through to warm (#FFB454) for a Z4–5 session. Hover or tab to a bar for its date, distance, pace, efficiency factor and decoupling. Below that, the eight-week block numbers and the focus notes.

It binds 127.0.0.1 only and has no authentication, because it serves your complete health record to anything that can reach it. Don't put it behind a tunnel or change the bind host.

Three read-only JSON endpoints back the page, and they're useful on their own:

Endpoint Contents
/api/today get_health_snapshot + get_training_status
/api/runs get_recent_runs + get_training_analysis
/api/insights the insights file below, or null

The page is deliberately blunt about the limits of its own data: a missing value renders as a dash and never as zero, a ramp percentage is always shown next to the mileage it was computed from (a "+947.7%" week off a 1.6 km base is not the emergency it looks like), and a comparison the analysis layer refused to make for want of a sample says so — "needs 8 paired nights, has 5".

Installing the insights skill

The skill that writes the insights file is vendored in this repo at .claude/skills/garmin-insights/SKILL.md, so a schema change and its skill update land in the same diff. Claude Code loads skills from user scope (~/.claude/skills/), so it needs to be reachable from there too — install.sh symlinks it in for you. To do it by hand instead:

mkdir -p ~/.claude/skills
ln -s <your/install/dir>/garmin-mcp/.claude/skills/garmin-insights ~/.claude/skills/garmin-insights

Symlinking rather than copying keeps the repo copy the single source of truth. /garmin-insights becomes available in any new Claude Code session after that (skills are loaded at session start, same as MCP servers above).

Refreshing the insights

The numbers on the page are live from your Garmin record. The sentences — today's verdict, the prescription, the focus notes — come from ~/.garmin-mcp/insights.json, which Claude Code writes. There's no scheduler and nothing automatic: ask for it.

  1. In a Claude Code session, run /garmin-insights — or just ask "refresh my dashboard insights". The skill carries the schema plus the rules that keep the prose honest: every figure must trace to the payload, the engine's refusals are respected rather than talked over, and thin samples are stated rather than hidden.

  2. Claude calls get_training_analysis, get_health_snapshot and get_recent_runs, then writes ~/.garmin-mcp/insights.json in this shape:

    {"schema_version": 1,
     "generated_at": "2026-08-08T06:00:00Z",
     "today": {"verdict": "Go easy — yesterday's long run is still in your legs.",
               "prescription": "35 min recovery @ 7:40–8:00 /km",
               "evidence": ["HRV balanced at 44 ms", "Load ratio 1.11"]},
     "focus": [{"title": "…", "detail": "…", "metric": "decoupling_pct"}],
     "next_run": {"title": "…", "detail": "…"},
     "week": {"title": "…", "detail": "…"}}
    
  3. Reload the dashboard.

The page always shows how old that file is ("insights generated 2 h ago"), and once it passes 24 hours it says so plainly instead of presenting yesterday's reasoning as this morning's. If the file is missing or malformed, the page shows an empty state explaining this workflow rather than an error — the live numbers below it still render.

Capturing fixtures

.venv/bin/garmin-mcp capture-fixtures

This calls each underlying Garmin endpoint directly and writes the raw JSON responses to tests/fixtures/. It's a diagnostic tool for verifying Garmin's actual response shapes against what the assemblers expect (see docs/api-field-map.md). The files it writes contain real health data and are gitignored (tests/fixtures/*.json) — never commit them, and don't print their contents anywhere shareable.

Testing

.venv/bin/pytest                 # everything: Python + the two front-end tiers below
node --test 'tests/js/**/*.test.js'   # front-end-only, no Python/pytest needed

The suite has three layers:

  • Python (tests/*.py, excluding the two below) — the MCP tools, cache, fetcher, assemblers and dashboard server. No network, no browser.

  • Tier 1, JS units (tests/js/*.test.js, wired in via tests/test_js_units.py) — pure-function tests for src/garmin_mcp/dashboard/static/app.js (formatters, date parsing, effort calibration) run under node's built-in node:test. Zero dependencies: no npm install, no package.json. node --test 'tests/js/**/*.test.js' runs this layer standalone for a fast front-end-only loop while iterating on app.js — a bare directory path (node --test tests/js) does not reliably resolve on node v26, so the glob form is what both this command and tests/test_js_units.py use. Skips (not fails) if node isn't on PATH.

  • Tier 2, browser layout (tests/test_dashboard_browser.py) — real layout checks (bar widths, grid wrapping, 200% text-zoom overflow) driven with Playwright against a throwaway local server serving the static page plus fixtures from tests/js/fixtures/ (synthetic, committed — no health data). Requires the dev extra and a one-time browser download:

    .venv/bin/pip install -e ".[dev]"
    .venv/bin/playwright install chromium   # one-time, ~150 MB download
    

    Skips cleanly (not fails) if playwright isn't installed, or if it's installed but playwright install chromium hasn't been run yet — both a fresh clone and a CI box without either dependency still pass the suite, they just see fewer tests run. This layer binds 127.0.0.1 on an OS-assigned ephemeral port (never a fixed port, never 8765) and never calls Garmin — see the module docstring for why it's the one place in the suite that opens a socket at all.

Troubleshooting

  • Auth errors / "Garmin rejected the stored tokens" — your tokens expired or were revoked. Re-run garmin-mcp login you@example.com from a real terminal.
  • partial: true in a tool's response — Garmin rate-limited a request or a connection failed transiently. The tool still returns whatever it has cached; treat the numbers as possibly incomplete or stale and try again later rather than assuming Garmin has no data.
  • Weight fields are null — this is normal on any day without a weigh-in. weight_kg, body_fat_pct, and the weight trend fields in get_body_metrics only populate on days you actually stepped on a connected scale.

Licence

MIT — see LICENSE.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选