garmin-mcp
Provides read-only access to Garmin Connect health data, including sleep, HRV, body battery, stress, training readiness, activities, and more.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@garmin-mcp how did I sleep last night?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
garmin-mcp
An MCP server that gives an LLM read-only access to your own Garmin Connect health data — sleep, HRV, body battery, stress, training readiness, activities and more.
Runs locally over stdio; switches to authenticated HTTP for remote use with environment variables only.
Heads up. Garmin's official Health API is partner-only and rejects personal-use applications, so this uses Garmin's private Connect API via
python-garminconnect. That's against Garmin Connect's terms of service and can break without notice — it did in March 2026, when Garmin tightened its bot protection. Fine for personal use; don't build anything load-bearing on it.
Requirements
Python 3.12+ (
garminconnectrequires it —uvhandles this for you)A Garmin Connect account
Related MCP server: health-mcp
Setup
uv sync
uv run garmin-mcp-loginRun the login from a normal terminal, not an elevated/Administrator one. On Windows an elevated process writes the token file with permissions that exclude your ordinary user account. Login appears to succeed, then every client that isn't elevated fails with
Access is denied— see Troubleshooting. The command warns and asks for confirmation if it detects this.
garmin-mcp-login asks for your email, password and MFA code once, then writes session tokens to ~/.garminconnect/. Your password is used to obtain those tokens and is never stored.
This is a separate command for a reason: Garmin logins can require an MFA code, and an MCP server talking JSON-RPC over a pipe has nowhere to prompt. The server only ever reads the token file, and garminconnect refreshes the tokens on its own — you shouldn't need to log in again unless the refresh token expires or you revoke access.
Re-running the command is safe; it checks the existing tokens first and exits without asking for anything if they still work. Don't re-run it speculatively when something breaks — Garmin rate-limits login attempts per IP, and repeated tries make it worse. Almost every failure has a cause other than a dead token; check the log first.
Use it with Claude Code
Copy .mcp.json.example to .mcp.json and set the absolute path to your checkout (.mcp.json is gitignored, since it is machine-specific). Or register it globally:
claude mcp add garmin -- uv --directory /path/to/garmin-mcp run garmin-mcpUse it with Claude Desktop
Add it to claude_desktop_config.json — on Windows %APPDATA%\Claude\, on macOS ~/Library/Application Support/Claude/:
{
"mcpServers": {
"garmin": {
"command": "/absolute/path/to/uv",
"args": ["--directory", "/absolute/path/to/garmin-mcp", "run", "garmin-mcp"],
"env": { "GARMINTOKENS": "/absolute/path/to/home/.garminconnect" }
}
}
}Three things matter here, each of which will otherwise fail silently:
Use an absolute path to
uv. Claude Desktop does not inherit your shellPATH, so a bareuvis not found.which uv/(Get-Command uv).Sourcegives you the path.Set
GARMINTOKENSto an absolute path.~resolves against the environment of whatever launched the process, which is not necessarily your shell's.You do not run the server yourself. Desktop spawns it on launch and kills it on quit; in stdio mode it has no port and nothing to attach to. After changing config, fully quit Desktop (tray icon → Quit) — closing the window leaves it running with the old config.
Then ask things like "how did I sleep last night?", "am I recovered enough to train hard today?", or "how did my resting heart rate trend over the last month?"
To check it by hand instead:
uv run mcp dev src/garmin_mcp/server.py # MCP InspectorTools
Date arguments are deliberately permissive, because a model writes dates the way a person would:
keywords —
today,yesterday,last night,last week,last monthoffsets —
-7d,7d,2 weeks ago,a month ago(unsigned means the past; only+3dlooks forward)absolute —
2026-08-02,2026/08/02,2026-08-02T22:15:00Z
Sleep is recorded against the morning it ends, so last night's sleep is date=today (and last night maps there too).
A rejected date fails the call before any network request, which is invisible in the response — so unrecognised values are logged with their arguments rather than failing silently.
Tool | What it returns |
| Summary + sleep + HRV + body battery + readiness for one day, in one call |
| Steps, distance, floors, calories, intensity minutes, resting HR, stress |
| Sleep score and components, stage durations, overnight SpO2 and respiration |
| Overnight HRV, weekly average, baseline range, HRV status |
| Average/max stress and the rest/low/medium/high split |
| Resting, min and max HR, seven-day resting average |
| Readiness score plus the factors behind it |
| Daily high/low, charge and drain, over a range |
| Daily step totals, goal and distance, over a range |
| Recorded workouts in a range, with IDs |
| Full detail for one workout by ID |
| Weigh-ins: weight, BMI, body fat, muscle and bone mass |
| Name, height, weight, and your measurement system |
| Any Garmin API path directly — the escape hatch |
Every tool is annotated readOnlyHint: true. Nothing here can modify your Garmin account.
Why responses look trimmed
Garmin returns per-minute sample series; a single get_sleep_data call can exceed 100KB of JSON. By default those series are replaced with a marker like {"_omitted": "480 samples omitted; call again with detail='full' to include them"} and long lists are truncated to 8 entries.
Pass detail="full" when the individual data points actually matter.
Adding more endpoints
garminconnect exposes ~140 methods; 13 are wired up above. Adding another is one row in src/garmin_mcp/tools.py:
ToolSpec(
name="garmin_get_spo2",
method="get_spo2_data", # any method on the garminconnect client
title="Pulse oximetry",
shape=Shape.DAILY, # NONE | DAILY | RANGE
description="Get overnight blood oxygen saturation for one day: ...",
),Date parsing, payload trimming, read-only annotations, JSON serialisation and error handling all come from the registry (src/garmin_mcp/registry.py) — there is no function to write.
The three shapes map to how the underlying method takes arguments:
Shape | Signature | Tool parameters |
|
|
|
|
|
|
|
|
|
Endpoints that don't fit — anything taking an ID, or fanning out across several calls — are written as ordinary @mcp.tool() functions in server.py; garmin_get_activity is the example to copy.
A test asserts every method in the table exists on the Garmin client, so a typo fails the suite rather than surfacing at call time.
Explore before you commit to a row. Use garmin_api_request to try an endpoint, and promote it once you find yourself reaching for it repeatedly.
Running remotely
Nothing in the code is stdio-specific. Set the environment and it serves HTTP instead:
export GARMIN_MCP_TRANSPORT=streamable-http
export GARMIN_MCP_AUTH_TOKEN="$(openssl rand -hex 32)"
export GARMIN_MCP_HOST=0.0.0.0
uv run garmin-mcpClients then send Authorization: Bearer <token> to http://host:8000/mcp.
Variable | Default | Purpose |
|
|
|
|
| HTTP bind address |
|
| HTTP port |
| — | Required for HTTP. Shared bearer secret |
|
| Token store path — mount as a secret in a container |
|
| Log verbosity (stderr) |
Notes for when you deploy:
The server refuses to start over HTTP without
GARMIN_MCP_AUTH_TOKEN. Otherwise a missing variable would quietly publish your health data. The token is compared in constant time.HTTP runs stateless with plain JSON responses, so there's no session affinity to preserve — restart or replicate freely.
Run
garmin-mcp-loginsomewhere interactive and ship the resulting token file as a mounted secret; the server never needs your password.Terminate TLS in front of it. The bearer token is a shared secret and plain HTTP would leak it.
For multi-user or proper OAuth, the SDK's
AuthSettings/token_verifierhooks are untouched and additive — swap out_BearerAuthinserver.py.
Development
uv run pytest108 tests, fully offline — no credentials or network required. They cover date parsing, payload trimming, config validation, all three registry shapes, tool registration, the health-snapshot fan-out, and the HTTP bearer guard.
Diagnosing problems
Read the log before changing anything. The server writes every failure to stderr, including the tool name, the exception, and the arguments that caused it. MCP clients don't surface arguments anywhere, so this log is usually the only place the real cause appears.
Claude Desktop captures it per server:
# Windows
Get-Content "$env:APPDATA\Claude\logs\mcp-server-garmin.log" -Tail 30 -Wait# macOS
tail -f ~/Library/Application\ Support/Claude/logs/mcp-server-garmin.logmcp.log alongside it covers connection-level problems. Under HTTP transport the same output goes to your terminal instead.
Two signals worth knowing:
A tool that fails in a few milliseconds never reached Garmin. A real API call takes 100ms+; anything faster failed during argument parsing or before authentication.
Garmin session establishedappears in the log on every successful login. Its absence means authentication never succeeded, regardless of what the error says.
Troubleshooting
"Access is denied" / "not permitted to read it" — the token store was written by an elevated (Administrator) terminal, so its permissions exclude your normal user account. It then works from that terminal and fails everywhere else, which makes it look intermittent. Re-authenticating does not fix this — it would rewrite a file the client still can't read. Grant your account access instead, which keeps your existing token:
icacls "$env:USERPROFILE\.garminconnect" /grant "$env:USERNAME:(OI)(CI)F" /TThen run garmin-mcp-login from a non-elevated terminal in future.
"session could not be refreshed just now" — the token is intact and this is usually Garmin rate-limiting the connection. Wait a minute; the server also retries once automatically. Don't re-authenticate unless it persists.
"no token store" — run uv run garmin-mcp-login.
Any authentication error, generally — garminconnect reports an expired token, a rate-limited refresh, and an unreadable token file with the same message (Username and password are required), because it performs the refresh network call inside the same error handler as the token file read. This server classifies them and tells you which one it actually is. Trust that classification over the raw library message.
Login fails mentioning Cloudflare or a 403 — Garmin likely changed its bot protection. Upgrade first: uv sync --upgrade-package garminconnect.
"Garmin is rate limiting" — wait a minute. Prefer one wide date range over many single-day calls.
A date argument is rejected — the parser accepts a wide range of forms, but not everything. The log line names the exact value it received.
A metric comes back empty or errors inside garmin_get_health_snapshot — usually means your device doesn't record it, or hasn't synced. The snapshot deliberately reports per-metric errors instead of failing the whole call. Note that a night with no recorded sleep returns a valid response with null fields, which is data absence, not an error.
Available Tools
14 toolsgarmin_api_requestRaw Garmin API requestARead-onlyIdempotent
Call any Garmin Connect API path directly. This is the escape hatch for the ~130 endpoints without a dedicated tool (SpO2, respiration, VO2 max, race predictions, gear, workouts, badges, and so on). Prefer a dedicated tool when one exists -- their output is easier to read and their arguments are validated. Paths look like '/usersummary-service/stats/daily/2026-08-01/2026-08-03'; the response is returned as-is.
Inputs: path (required), detail, params.
garmin_get_activityActivity detailARead-onlyIdempotent
Get the full record of one recorded workout by its activity ID: type, timing, distance, pace and speed, heart rate zones, cadence, power, elevation, calories, training effect and recovery time. Find the ID with garmin_list_activities first.
Inputs: activity_id (required), detail.
garmin_get_body_batteryBody batteryARead-onlyIdempotent
Get body battery energy levels across a date range: the daily high and low, plus charge and drain totals. Body battery rises with rest and falls with activity and stress, so a multi-day range shows whether energy is trending down faster than it recovers.
Inputs: detail, end_date, start_date.
garmin_get_body_compositionBody compositionARead-onlyIdempotent
Get weigh-ins over a date range: weight, BMI, body fat and water percentage, muscle and bone mass, plus the range averages. Only returns data for days with a recorded weigh-in, so an empty result usually means no measurement rather than an error.
Inputs: detail, end_date, start_date.
garmin_get_daily_summaryDaily summaryARead-onlyIdempotent
Get the all-round wellness summary for one day: total and goal steps, distance, floors, calories (active, resting, consumed), intensity minutes, resting heart rate, average stress, and body battery high/low. Start here for broad questions like 'how active was I on Tuesday' -- it answers in one call what would otherwise take several.
Inputs: date, detail.
garmin_get_health_snapshotHealth snapshotARead-onlyIdempotent
Get an all-in-one recovery picture for one day: daily summary, sleep, HRV, body battery and training readiness together. Prefer this for open questions like 'how did I sleep', 'how am I recovering' or 'should I train today' -- it answers in one call what would otherwise take five, and each metric is trimmed to its headline numbers. Metrics your device does not record are reported as errors within the result rather than failing the whole call.
Inputs: date.
garmin_get_heart_rateHeart rateARead-onlyIdempotent
Get heart rate for one day: resting, minimum and maximum heart rate, and the last seven days' resting average. Use detail='full' for the continuous all-day heart rate series. For heart rate during a specific workout, use garmin_get_activity instead.
Inputs: date, detail.
garmin_get_hrvHeart rate variabilityARead-onlyIdempotent
Get overnight heart rate variability for one day: the weekly average, last night's average, the personal baseline range, and Garmin's HRV status (balanced, unbalanced, low, poor). HRV is the most sensitive single indicator of accumulated stress or illness, so prefer this when asked about recovery trends rather than a single night's sleep.
Inputs: date, detail.
garmin_get_sleepSleepARead-onlyIdempotent
Get sleep for the night ENDING on the given date: overall sleep score and its component ratings, time in deep/light/REM/awake, sleep and wake times, average overnight SpO2, respiration and resting heart rate. Ask for 'today' to get last night. With detail='full' this also returns per-minute sleep stage, movement and heart rate series, which are large.
Inputs: date, detail.
garmin_get_stepsDaily stepsARead-onlyIdempotent
Get daily step totals, step goal and distance for each day in a range. Cheap and compact -- prefer this over repeated daily-summary calls when the question is only about step counts over time.
Inputs: detail, end_date, start_date.
garmin_get_stressStressARead-onlyIdempotent
Get stress for one day: the average and maximum stress score, plus how the day divided between rest, low, medium and high stress. Use detail='full' for the underlying per-interval stress series.
Inputs: date, detail.
garmin_get_training_readinessTraining readinessARead-onlyIdempotent
Get Garmin's training readiness score for one day (0-100) together with the factors behind it: sleep score and history, recovery time, HRV status, acute training load and stress history. This is the tool for 'should I train hard today' -- it is Garmin's own synthesis of the recovery signals rather than a raw metric.
Inputs: date, detail.
garmin_get_user_profileUser profileARead-onlyIdempotent
Get the account profile: name, birth date, height, weight, gender, activity level and -- importantly -- the measurement system. Worth checking once before interpreting units in other tools, since Garmin returns metric values regardless of what the app displays.
Inputs: detail.
garmin_list_activitiesList activitiesARead-onlyIdempotent
List recorded workouts in a date range, newest first, with activity ID, type, name, start time, duration, distance, pace, average and max heart rate, calories, elevation and training effect. Use this to find an activity, then pass its activityId to garmin_get_activity for the full detail of a single session.
Inputs: detail, end_date, start_date.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 14 tools
Each tool targets a distinct metric or resource (steps, sleep, HRV, stress, activities, etc.), with clear usage guidance in descriptions. Even overlapping tools like daily_summary and health_snapshot are differentiated by purpose (activity vs recovery), and the escape-hatch api_request is explicitly subordinated to dedicated tools.
The vast majority follow a consistent garmin_ get_<noun> pattern, and list_activities is a natural exception. The only real outlier is garmin_api_request, which breaks the verb_noun convention but is a generic escape hatch rather than a resource-specific operation.
14 tools is well within the ideal range. The set covers a broad health/fitness domain with dedicated tools for the most common queries, and the single escape hatch prevents the count from exploding into dozens of niche endpoints.
The dedicated tools cover all major data categories (daily summary, sleep, HRV, stress, heart rate, readiness, body battery, steps, body composition, activities), and the api_request tool provides access to the remaining ~130 endpoints. This ensures no dead ends while keeping the primary surface focused.
Maintenance
Related MCP Connectors
MCP server for Withings health data — sleep, activity, heart, and body metrics.
Unofficial Garmin Connect access: sleep, HRV, training, activities, workouts and weight.
Multi-tenant hosted MCP server for Oura Ring — 21 read-only tools, OAuth per user.
Remote MCP server for training, nutrition, wellness, and performance data with OAuth 2.0.
Related MCP Servers
- AlicenseAqualityCmaintenanceMCP server for Garmin Connect that provides read-only access to daily health metrics, activities, workouts, and body composition data.32MIT
- AlicenseNot gradedqualityDmaintenanceExposes personal Garmin wellness data through MCP tools for accessing summary, sleep, HRV, heart rate, stress, body battery, and historical data.MIT
- AlicenseAqualityDmaintenanceMCP server for reading and querying Garmin Connect data, including activities, strength history, recovery, trends, and optionally creating workouts.12MIT
- AlicenseNot gradedqualityCmaintenanceMCP server for Garmin Connect that enables users to access and manage their personal health and fitness data, including daily summaries, heart rate, sleep, HRV, stress, body composition, activities, and training readiness, with secure per-user authentication.Apache 2.0