Install
$ agentstack add mcp-marcelfortmann-garmin-mcp ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Garmin → Claude MCP server
> Analyze your own Garmin Connect data — activities, sleep, HRV, Body Battery, > training readiness — right inside Claude Desktop.
A fully local, read-only MCP server that lets you analyze your own Garmin Connect data in Claude Desktop. The server runs as a local child process of Claude Desktop and talks to it over stdio.
Guiding principles
- 100% local — nothing is hosted or exposed to the network (stdio).
- Read-only — there is no write endpoint to Garmin.
- No third party — the only network contact is Garmin. No telemetry, no external logging.
- Passwordless at runtime — the password is never stored; after the one-time login only local tokens are used.
Requirements
- Node.js ≥ 20 (tested with Node 22 LTS), npm.
- A Garmin Connect account.
Setup (4 steps)
# 1) Install dependencies
npm install
# 2) Milestone test: does the client get through Garmin's Cloudflare protection?
npm run smoke
# Expectation: "Status 200" and "Challenge: no".
# 3) One-time interactive login (prompts for email, hidden password, MFA if enabled)
npm run login
# Creates ~/.garmin-mcp/tokens.json (mode 0600). The password is NOT stored.
# 4) Compile TypeScript to build/
npm run build
Connecting Claude Desktop
Claude Desktop → Settings → Developer → Edit Config. Add the server with its absolute path (claude_desktop_config.json):
{
"mcpServers": {
"garmin": {
"command": "node",
"args": ["/absolute/path/to/garmin-mcp/build/mcp/server.js"]
}
}
}
Then restart Claude Desktop. Try it in chat, e.g.:
> "Am I logged in to Garmin? Use whoami." > "Show me my daily summary and my recent activities."
Available tools
13 read-only tools. Date parameters are YYYY-MM-DD (default = today); ranges default to the last ~4 weeks. Several tools take a metrics[] / include[] selector, so one tool covers many data types (this keeps the tool list small for good tool selection). Long time series in responses are truncated to stay compact.
| Tool | Description | |---|---| | whoami | Connection check + account profile | | get_daily_health | Daily wellness for a date — metrics[]: summary, sleep, stress, heartrate, hrv, spo2, respiration, hydration, steps, floors, intensityminutes, bodybattery, bodybatteryevents, statsandbody | | get_training | Training for a date — metrics[]: readiness, morningreadiness, status, vo2max, fitnessage | | get_fitness | Fitness/performance — metrics[]: racepredictions, cyclingftp, lactatethreshold, personalrecords, endurancescore, hillscore, restingheartrate, weeklyintensityminutes (startDate/endDate) | | get_weight | Body weight & composition over a range (include_raw also returns weigh-ins) | | get_steps_history | Step totals over a range — granularity: daily or weekly | | get_activity | One activity by activityId — include[]: summary, splits, weather, details, hrzones, exercise_sets | | list_activities | Recent activities, or by date range/type (limit/startDate/endDate/type) | | get_devices | Paired Garmin devices | | get_user_profile | User profile & settings (units, preferences) | | get_goals | Goals (status: active/future/past) | | get_workouts | Saved workouts (limit) | | get_scheduled_workouts | Scheduled workouts / calendar (year/month) |
How it works
The server registers each Garmin data method as an MCP tool and talks to Claude Desktop over stdio. Every call goes through a generic connectapi() request to connectapi.garmin.com with a bearer token, sent via cycletls so the TLS fingerprint looks like a real browser (Garmin sits behind Cloudflare).
Authentication happens once (npm run login): a login cascade — mobile iOS JSON login first, the classic widget/CSRF flow as fallback — yields a CAS service ticket, which is exchanged in Garmin's DI-OAuth2 flow for an access + refresh token. The tokens are cached locally and the access token is refreshed automatically on expiry; your password is never stored.
Security model
- The password is never stored — it is only used for the one-time login.
- Tokens live at
~/.garmin-mcp/tokens.jsonwith file mode0600(only you can read them). - The access token is renewed automatically via the refresh token when it expires.
- Read-only: there is no code path that changes anything on Garmin.
- No telemetry, no external logging. Diagnostics go to
stderronly (never over the stdio MCP channel).
Architecture (layers)
src/http/impersonate.ts— TLS impersonation viacycletls(JA3 fingerprint) + cookie jar, so Garmin's bot protection lets us through.src/http/smoke.ts— milestone test againstsso/embed.src/garmin/auth.ts— login cascade (mobile iOS JSON login → widget/CSRF fallback) + MFA + DI-OAuth2 ticket exchange + refresh.src/garmin/tokens.ts— local token cache (0600).src/garmin/client.ts— genericconnectapi()+ typed, read-only data methods.src/mcp/server.ts— MCP server, tool registration, stdio transport.src/cli/login.ts— one-time interactive login.
Maintenance & risks (named honestly)
This uses Garmin's unofficial internal API, ported from the open-source Python package python-garminconnect. Expected breakage points and how to fix them:
- Unofficial API — endpoint paths can change. On errors, compare the paths in
src/garmin/client.tsagainst the currentpython-garminconnectsource. - JA3 / User-Agent — on a
403or a "Just a moment …" Cloudflare page, update the JA3/User-Agent pair insrc/http/impersonate.tsto a current Chrome (both together). - Client ids — the DI-OAuth2 client ids (
GARMIN_CONNECT_MOBILE_ANDROID_DI_*) rotate; update the list insrc/garmin/auth.tson auth errors. garthis discontinued — the previously common auth librarygarthis no longer maintained; the authoritative reference ispython-garminconnect.- Keep the token folder private —
~/.garmin-mcp/holds valid session tokens; don't share it, don't commit it. - Terms of service — access via the unofficial API may be in tension with Garmin's terms of service. Intended for private personal use with your own data.
Troubleshooting
403or a "Just a moment …" page — Garmin's Cloudflare is blocking the TLS fingerprint. Update the JA3/User-Agent pair insrc/http/impersonate.tsto a current Chrome (both together), then re-runnpm run smoke.🔒 Not logged in/ session expired — runnpm run loginagain.⏳rate limited (HTTP 429) — Garmin is throttling; wait a bit and retry.- First run is slow / cycletls errors —
cycletlsdownloads a small Go helper binary on first use; make sure it can execute and isn't blocked by the OS.
Re-login
If tools report 🔒 Not logged in (token expired/invalid):
npm run login
License
[MIT](LICENSE) © Marcel Fortmann
Disclaimer
This is an unofficial, community project. It is not affiliated with, endorsed by, or sponsored by Garmin. Garmin® and Garmin Connect™ are trademarks of Garmin Ltd. or its subsidiaries.
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: marcelfortmann
- Source: marcelfortmann/garmin-mcp
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.