Garmin Health MCP Server

Garmin Health MCP Server

This MCP server provides read-only access to your local Garmin Connect history, enabling Claude to query workouts, sleep, training load, fitness scores, records, and gear via natural language. It uses a sync script to pull data into a local SQLite database, ensuring privacy and offline operation.

Category
Visit Server

README

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.

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.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured