Skip to main content
Glama
Garmin MCP for Claude

Garmin MCP for Claude

by kbsa00

Garmin MCP for Claude

Your own Garmin Connect data in Claude. This is a personal, self-hosted remote MCP server on Cloudflare Workers (free plan) that you add to claude.ai as a custom connector.

It covers:

  • Recovery: sleep, HRV, resting heart rate, Body Battery and stress.

  • Training: activities, laps, VO2 max, Fitness Age and training load.

  • Computed metrics: readiness, ACWR and recovery after hard sessions.

Typical use: ask Claude "How did I sleep, and how hard should I train today?", or schedule a daily morning brief.

  • Single user: you deploy your own instance with your own Garmin key, and nobody else can use it.

  • Uses the official Garmin Connect Personal API. Personal, non-commercial use only.

  • Not affiliated with or endorsed by Garmin or Anthropic.

What you need

Related MCP server: Garmin MCP Worker

Setup (about 10 minutes)

git clone https://github.com/<you>/garmin-mcp.git
cd garmin-mcp
npm install
cp .env.example .env           # then put your key in .env: GARMIN_API_KEY=...
npx wrangler login             # log in to YOUR Cloudflare account
npm run setup                  # creates everything and deploys (see below)
node scripts/backfill.mjs      # loads all your Garmin history into the database

npm run setup (scripts/setup-instance.mjs) is safe to run again; steps that are done are skipped. It:

  1. pins wrangler.jsonc to your Cloudflare account id, so it can never deploy into another account;

  2. creates the KV namespaces and the D1 database and writes their ids into wrangler.jsonc;

  3. applies the D1 migrations;

  4. uploads your Garmin key as a Worker secret, and creates a login passphrase in CONNECTOR_LOGIN.txt (local only, gitignored);

  5. registers a workers.dev subdomain if your account has none, sets the connector URL, and deploys.

At the end it prints your connector URL, e.g. https://garmin-mcp.<subdomain>.workers.dev/mcp.

Add it to Claude:

  1. Go to Settings → Connectors → Add custom connector.

  2. Paste the URL and leave the OAuth Client ID and Secret empty.

  3. Click Connect and enter the passphrase from CONNECTOR_LOGIN.txt.

After npm run setup, wrangler.jsonc contains your account and resource ids. They are not secrets, but if you publish a fork, keep your setup-modified wrangler.jsonc out of it.

Tools

tool

what it does

source

get_recovery(date?)

One day: sleep (score, duration, stages, times), HRV (overnight average, status, baseline range), resting HR, Body Battery (at wake, high/low), stress, plus a missing list

Garmin

get_recovery_trend(days ≤ 60)

Daily rows plus 7- and 28-day averages of HRV and resting HR

Garmin + averages

get_activities(from, to, type?)

Activities with distance, time, pace, HR, Training Effect, training load, cadence, power, stride and VO2 max, plus totals per type

Garmin

get_activity_detail(activity_id)

Per-lap data from the activity's FIT file: pace, HR, cadence (steps/min), step length, power, running dynamics

Garmin (FIT)

get_vo2max_trend(days ≤ 365)

The watch's VO2 max estimate per activity, read from the FIT file

Garmin (FIT)

get_fitness_age(date?)

Garmin Fitness Age with its components and targets

Garmin

get_training_load(days ≤ 90)

Daily load, acute (7d) and chronic (28d) load, ACWR and a status label

computed

get_readiness(date?)

Deterministic 0–100 readiness score with components and a one-line explanation

computed

get_recovery_after(date)

Nights until HRV and resting HR were back in your baseline band after that day's activity

computed

health_check()

Verifies the Garmin token exchange. Returns no health data

–

Garmin data vs. computed metrics

Every result starts with a provenance block:

  • garmin_provided: fields supplied by Garmin, passed through unchanged except for unit conversion.

  • computed_by_connector: fields this connector calculates itself.

The server's MCP instructions tell Claude to say which is which, and never to present a computed metric (such as readiness) as a Garmin feature. Tool descriptions are labelled [Garmin data] or [Connector's own calculation; not Garmin …].

Not available from the Garmin Personal API

  • Training Readiness and Training Status. get_readiness and get_training_load are this connector's own metrics instead, and they will differ from the watch.

  • Race predictions.

  • Lactate threshold.

  • Workout or course push: the API is read-only.

  • VO2 max as an API field. It is read from the FIT file instead; see Formulas.

See ENDPOINTS.md for the endpoints used and their response shapes.

How it works

claude.ai ──OAuth 2.1 (DCR + PKCE)──▶ Cloudflare Worker
                                      ├─ workers-oauth-provider: /.well-known/*, /register, /token, /authorize (passphrase login)
                                      ├─ /mcp (token required) → MCP server (Streamable HTTP)
                                      │     ├─ recovery service   D1 first (completed days), Garmin API for the rest
                                      │     └─ activity service   activities, FIT laps, load / readiness / recovery
                                      └─ cron 05:30 local → nightly sync of the last 3 days into D1
Garmin client → personal.connectapi.garmin.com (token cached in KV; 401 → refresh + retry; 429 → backoff)
FIT parsing → @garmin/fitsdk (unneeded messages are stripped first to stay within the free plan's CPU limit)

storage

content

Worker secrets

GARMIN_API_KEY, LOGIN_PASSPHRASE_HASH (PBKDF2)

D1

daily_recovery, activities (incl. VO2 max), activity_laps, sync_state

KV OAUTH_KV

OAuth clients, grants, tokens

KV APP_KV

Cached Garmin access token, login rate limits, login state

Security

  • Protected endpoint: only /mcp serves data, and it requires a valid OAuth access token.

  • Login: a passphrase checked against a PBKDF2 hash. The login form has a one-time CSRF state bound to a __Host- cookie.

  • Brute-force protection: 5 failed logins per IP per 15 minutes, and 20 per hour in total.

  • Secrets: they live only in your local .env and in Worker secrets. The scripts pipe them in and never print them.

  • Logs: they contain counts only, never health data.

Formulas

  • Training load / ACWR:

    • Daily load = the sum of Garmin's per-activity activityTrainingLoad.

    • Acute load = the 7-day mean; chronic load = the 28-day mean; ACWR = acute / chronic.

    • Status: < 0.8 detraining, < 1.0 maintaining, ≤ 1.3 productive, above that overreaching_risk. It is insufficient_data with less than 28 days of history or very low load.

  • Readiness (0–100):

    component

    weight

    score

    HRV

    30%

    70 + 15·z vs your 60-day baseline

    Resting HR

    20%

    70 + 15·z, inverted (lower is better)

    Sleep score

    20%

    Garmin sleep score

    Sleep duration

    15%

    100 · min(1, h/8)

    Load

    15%

    ACWR through yesterday: ≤ 1.0 → 100, 1.3 → 70, 1.5 → 30, ≥ 2.0 → 0

    • Missing parts are dropped and the weights renormalized. The same inputs always give the same score.

  • Recovery after a hard day:

    • Bands: HRV uses Garmin's "balanced" range; resting HR uses your 60-day mean ± 1 sd.

    • Counting: only the first night after the activity decides whether a value left its band (day 2 if day 1 has no data). If it did, the count runs to the first night back in band.

  • Body Battery at wake: the 15-minute reading closest to the end of sleep. Garmin's Body Battery report covers roughly the last 4 months.

  • VO2 max:

    • Source: the watch stores its estimate in each outdoor activity's FIT file, in the undocumented message 140, field 7: VO2 max = raw × 3.5 / 65536.

    • Validation: the format is community-documented and was validated against a watch, which shows the value rounded to an integer.

    • Treadmill: treadmill activities don't update it, so they are stored as null.

Configuration

  • Time zone: dates are interpreted in Europe/Oslo. To change it:

    • set TIMEZONE in src/dates.ts;

    • adjust the two UTC crons in wrangler.jsonc so that one of them hits 05:30 local time in summer and winter;

    • adjust the hour check in isOsloSyncHour (src/sync/sync.ts).

  • Worker name / URL: change name in wrangler.jsonc before running npm run setup.

  • Strength training: strength sessions are listed and count towards training load, but set, rep and weight details are not parsed.

Maintenance

task

command

Redeploy

npm test && npm run type-check && npx wrangler deploy

New migrations

npx wrangler d1 migrations apply <database_name> --remote

New Garmin key (when it expires)

put it in .env, then npm run update-garmin-key

New login passphrase

npm run rotate-passphrase

Reload all history

node scripts/backfill.mjs (idempotent)

Local development

npx wrangler d1 migrations apply <database_name> --local, then node scripts/new-passphrase.mjs --no-deploy --dev /tmp/dev-hash.env --login-file /tmp/dev-login.txt, then npm run dev -- --hash-file /tmp/dev-hash.env

Tests: npm test runs the unit and integration tests. They use mocked Garmin responses, FIT fixtures and D1 on node:sqlite. npm run type-check checks the types.

Contributing

Issues and pull requests are welcome. Please:

  • run npm test and npm run type-check;

  • never commit .env, CONNECTOR_LOGIN.txt or real health data (test fixtures must be synthetic).

License

MIT. Use of the Garmin Connect Personal API is subject to Garmin's own terms (personal, non-commercial use).

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    A read-only remote MCP server that lets Claude query your own Garmin Connect data — sleep, HRV, Body Battery, stress, heart rate, and activities — through Garmin's official Personal API. It runs on Cloudflare Workers with OAuth sign-in, so it works from both the Claude mobile app and desktop.
    MIT
  • A
    license
    Not graded
    quality
    Not graded
    maintenance
    A read-only remote MCP server that connects Claude to your own Garmin Connect account through Garmin's official Personal API, running on Cloudflare Workers with OAuth sign-in so it also works in the Claude mobile app. It lets you ask natural-language questions about sleep, HRV, resting heart rate, Body Battery, stress, daily summaries and activities across date ranges.
    MIT
  • A
    license
    Not graded
    quality
    Not graded
    maintenance
    Enables Claude and other remote MCP clients to query your own read-only Garmin Connect data — recovery trends, sleep, HRV, Body Battery, stress, heart rate, and activities — through Garmin's official Personal API, with OAuth sign-in that works from the mobile apps as well as desktop. Deploys on Cloudflare Workers' free tier with no password sharing or scraping.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables connecting your own Garmin Connect data to Claude or another MCP client so you can ask about sleep, training, heart rate, weight, and more through read-only tools running on a Cloudflare Worker.
    378 npm
    1
    MIT