Skip to main content
Glama
Matt-y-Matt

Garmin Health MCP Server

by Matt-y-Matt

Garmin Health MCP Server

A local, private MCP server that gives Claude Desktop access to your Garmin Connect history. Nothing leaves your machine: a sync script pulls your data into a local SQLite file, and the MCP server serves that file read-only.

Garmin Connect ──garth──▶ sync_garmin.py ──▶ garmin_health.db ──▶ garmin_mcp_server.py ──▶ Claude Desktop
                          (you run this)      (local SQLite)       (read-only, offline)
                                ▲
                          garmin_ui.py  ← point-and-click setup, if you prefer

Separating ingestion from serving means the MCP server never holds your credentials, never makes a network call, and cannot be talked into hammering Garmin's API from inside a conversation.

Quick start (no terminal needed)

If you would rather not use the command line:

  • macOS / Linux -- double-click Start Garmin Setup.command

  • Windows -- double-click Start Garmin Setup.bat

The first run installs what it needs and opens a page in your browser that walks through three steps: sign in to Garmin, download your data, and connect it to Claude Desktop. That last step edits claude_desktop_config.json for you -- the step people most often get wrong by hand.

You only need Python 3.10+ installed first (python.org/downloads; on Windows tick "Add Python to PATH"). Everything else is automatic, and everything stays on your computer.

Prefer the terminal? Skip to Install. The rest of this README is the manual path.

Related MCP server: garmin-connect-mcp-server

Files

File

Purpose

garmin_ui.py

The friendly setup app: sign in, download, connect Claude Desktop

Start Garmin Setup.command / .bat

Double-clickable launchers for the above

sync_garmin.py

Phase 1 -- authenticate with Garmin, fetch every dataset, write to SQLite

garmin_mcp_server.py

Phase 2 -- expose the database to Claude Desktop as MCP tools

garmin_db.py

Shared schema, connection handling and queries used by both

garmin_tools.py

The eleven MCP tools, defined once and shared by both deployments

cloud/

Optional hosted version: Vercel + Supabase, for phone/web access (details)

tests/test_offline.py

End-to-end verification with synthetic data (no Garmin account needed)

tests/test_cloud.py

Verification of the hosted path against a real Postgres

garmin_db.py exists so the writer and the reader cannot drift apart on schema, and garmin_tools.py exists so the local and hosted servers cannot drift apart on tools.

Local or hosted?

The default is local: everything on your machine, nothing published. There is also an optional hosted deployment for reaching your data from claude.ai or your phone.

Local (default)

Hosted (cloud/)

Where the data lives

SQLite on your laptop

Supabase Postgres

Garmin credentials

~/.garth, on your machine

A row in your database

Reachable from

Claude Desktop, that machine

claude.ai, phone, any Claude client

Sync

When you run it, or a cron job

Daily, automatic

Protection

It is not on a network

One bearer token

The hosted version trades the local version's main safety property -- your health data and Garmin session never leaving your computer -- for convenience. Worth it for some people, not for others; make it a decision rather than a default.

About the setup app

garmin_ui.py is a small local web app built on Python's standard library -- it adds no dependencies. Some notes on how it behaves:

  • It binds to 127.0.0.1 only, so nothing on your network can reach it.

  • Every request must carry a one-time key generated at startup and embedded in the URL it opens. A web page in another tab can make your browser POST to localhost, so the key is what actually stops anything else driving it.

  • Your Garmin password is used once to sign in and never written to disk. Only Garmin's own session tokens are saved, to ~/.garth.

  • Two-factor codes are handled in the page, so no terminal prompt is involved.

  • Writing to Claude Desktop's config makes a .backup copy first and only touches the garmin-health key -- any other MCP servers you have are left exactly as they were. A config file that is not valid JSON is reported and left alone rather than overwritten.

Install

git clone <this repo> && cd Garmin-MCP
python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt

Requires Python 3.10+ (the code uses X | None type syntax).

Phase 1 -- sync your data

Do a dry run first. It authenticates and fetches, but writes nothing:

python sync_garmin.py --login --dry-run

You will be prompted for your Garmin email, password and (if enabled) an MFA code. On success, OAuth tokens are saved to ~/.garth with 0600 permissions, and later runs reuse them -- no password needed:

python sync_garmin.py                       # 10 activities + 7 days
python sync_garmin.py --activities 50 --days 30
python sync_garmin.py --no-details          # skip splits/HR zones (much cheaper)
python sync_garmin.py --db ~/data/garmin.db

One run fetches activities, sleep, body battery and readiness, training load, VO2max/endurance/hill scores, personal records, body composition and gear. Splits, heart-rate zones and training effect need three extra requests per activity, so they are fetched once per activity, only for activities not yet enriched, capped at --detail-limit (default 25) per run. A first sync of a long history therefore fills in details over several runs rather than issuing hundreds of requests at once.

To avoid typing credentials at all, export them first:

export GARMIN_EMAIL="you@example.com"
export GARMIN_PASSWORD='...'                # leading space keeps it out of shell history in bash/zsh
python sync_garmin.py --login

Running it safely

  • Every write is an upsert keyed on activity ID or calendar date, so re-running the same window updates rows in place. It will never duplicate or delete.

  • --dry-run prints what would be written and touches nothing.

  • The token directory holds live credentials for your Garmin account. Treat it like an SSH key; .gitignore already excludes it and the database.

  • Be gentle with --days. Several metrics cost one request per day, and Garmin will rate-limit or temporarily lock an account that hammers the API. A daily sync of the default window is well within normal app behaviour; a first-time backfill of a year is not — do it in chunks, or start with --no-details.

  • Schema changes are applied in place. Upgrading this project adds the new columns and tables to your existing database without losing rows, so you never need to delete and re-sync.

  • If one metric fails (older watches have no Training Readiness or HRV), that dataset is logged as an error and the rest of the sync still completes.

Schedule it daily if you like -- but run --login interactively once first so tokens exist, since MFA cannot be answered from cron:

0 7 * * *  cd /path/to/Garmin-MCP && .venv/bin/python sync_garmin.py >> sync.log 2>&1

Phase 2 -- the MCP server

Eleven tools are exposed.

Workouts

Tool

Signature

Returns

get_recent_activities

limit: int = 5 (max 50)

Type, start time, duration, distance, pace, avg/max HR, calories, ascent, training effect

search_activities

activity_type, start_date, end_date, min_distance_km, limit

The same, filtered -- "my runs in July", "every ride over 50 km"

get_activity_details

activity_id: int

Per-lap splits, time in each HR zone, power, cadence, stride, SWOLF

compare_activities

activity_ids: list[int] (2-5)

Aligned side-by-side table of the key metrics

Recovery, load and fitness

Tool

Signature

Returns

get_sleep_and_readiness

days: int = 7

Sleep score, deep/light/REM/awake, sleep HR, SpO2, body battery, training readiness, resting HR, HRV

get_training_load

days: int = 14

Acute (fatigue) and chronic (fitness) load, acute:chronic ratio, training status, weekly load vs optimal range

get_fitness_scores

days: int = 30

VO2max, fitness age, endurance score, hill score, and the change across the window

get_weekly_summary

weeks: int = 4

Volume, load and elevation next to the week's average sleep, resting HR and HRV

Records, body and kit

Tool

Signature

Returns

get_personal_records

--

Lifetime PRs with the activity that set each

get_weight_history

days: int = 30

Weight, BMI, body fat, muscle mass, and the change across the window

get_gear

--

Shoe/bike mileage, activity count, and life left against the wear limit

Every tool prepends the last sync timestamp so Claude knows how fresh the data is, and returns a clear "run sync_garmin.py first" message when the database is missing or empty. Metrics your watch does not record show as n/a rather than being guessed at.

On training load

get_training_load reports Garmin's own acute and chronic load rather than a reimplemented TSS model. They are the direct equivalents of what other platforms label CTL/ATL/TSB:

Garmin

Elsewhere

Meaning

Acute load (7-day)

ATL

Fatigue

Chronic load (28-day)

CTL

Fitness

Acute:chronic ratio

TSB / form

Balance between the two, and Garmin's injury-risk signal

Garmin only recomputes these when the watch syncs, so consecutive days can repeat and unsynced days are absent. The sync stores what Garmin returns and never interpolates.

Verify it runs before wiring it up:

python garmin_mcp_server.py     # sits waiting on stdio; Ctrl-C to exit

Phase 3 -- register with Claude Desktop

Edit claude_desktop_config.json:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

  • Windows: %APPDATA%\Claude\claude_desktop_config.json

  • Linux: ~/.config/Claude/claude_desktop_config.json

{
  "mcpServers": {
    "garmin-health": {
      "command": "/path/to/your/project/.venv/bin/python",
      "args": ["/path/to/your/garmin_mcp_server.py"],
      "env": {
        "GARMIN_DB_PATH": "/path/to/your/project/garmin_health.db",
        "GARMIN_UNITS": "metric"
      }
    }
  }
}

Use absolute paths throughout, and point command at the virtualenv's Python (not a bare python) so the mcp and garth packages resolve. Restart Claude Desktop completely, then look for the tools under the 🔨 icon.

Ask things like "How did I sleep this week?", "Am I overtraining?", "Is my VO2max trending up?", "Compare my last three long runs", "How many km are on my shoes?", or "Given my load and recovery, what should I do today?"

Configuration

Variable

Default

Meaning

GARMIN_DB_PATH

garmin_health.db beside the scripts

SQLite location

GARMIN_TOKEN_DIR

~/.garth

Saved OAuth tokens

GARMIN_EMAIL / GARMIN_PASSWORD

unset

Non-interactive login

GARMIN_UNITS

metric

imperial switches to miles

GARMIN_MCP_LOG_LEVEL

INFO

Server log verbosity (stderr only)

GARMIN_DB_PATH defaults to a path beside the script rather than the working directory, because Claude Desktop launches servers with an unpredictable CWD.

Verifying without a Garmin account

python tests/test_offline.py

This pushes realistic Garmin JSON through the real parsers, schema and tools, then launches the server over stdio and calls all eleven tools the way Claude Desktop does. It also exercises the setup app: its one-time-key gate, its config merge (other MCP servers survive, a backup is written, corrupt JSON is refused) and its plain-language error messages. It also checks that re-syncing does not duplicate rows, that a failed endpoint does not blank out previously stored values, and that an older database migrates in place without losing data. Verified against mcp 1.29.0 and 2.0.0.

Notes and caveats

  • garth is deprecated. It prints a notice on import and is no longer actively maintained, though it still works against the current Connect API. Auth is isolated in authenticate() and fetching in the fetch_* functions, so swapping the client later means touching only sync_garmin.py.

  • FastMCP moved in SDK 2.0. mcp.server.fastmcp.FastMCP became mcp.server.mcpserver.MCPServer. The decorator and run() APIs are identical, so the server tries the 1.x path first and falls back to 2.x.

  • Sleep is parsed from raw JSON, not garth's model. In garth 0.8.0 the pulse-ox fields never populate: its camel_to_snake_dict converts averageSpO2HRSleep to average_sp_o_2_hr_sleep, but DailySleepDTO declares average_sp_o2_hr_sleep, so the key silently fails to match and the value is dropped. Average sleep HR and SpO2 would always be NULL. The sync script calls the same endpoint through garth.connectapi and maps the fields explicitly. Everything else uses garth's typed models.

  • Fitness scores are read from the raw endpoints too. garth.GarminScoresData requires both the hill-score and endurance-score endpoints to return data and pops fixed keys from each, so a watch reporting VO2max but no hill score yields nothing at all. The sync fetches the three independently instead.

  • Not every metric exists on every watch. Training status, readiness, HRV, pulse-ox, hill and endurance scores are device-dependent; weight needs a connected scale and gear must be registered in Garmin Connect. Missing values are shown as n/a, never guessed.

  • Personal-record labels are best-effort. Garmin identifies records by a numeric type ID. The newer prTypeLabelKey string is used when present; otherwise a documented fallback table names the common running, cycling and step records. An unrecognised ID is shown as Record type N rather than mislabelled.

  • Deliberately not included: device inventory, women's health, and workout upload. The first two add little to a coaching conversation and the third writes to your Garmin account, which this project avoids entirely — the sync only ever reads.

  • This is not medical advice. It is your own data, summarised.

Maintenance

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    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.
    1
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    This MCP server exposes Garmin Connect health data—sleep, heart rate, HRV, stress, VO2max, and activities—to Claude through local tools, enabling natural language queries, data syncing, and statistical analysis like correlations and night-out detection.
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Matt-y-Matt/Garmin-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server