Skip to main content
Glama
odysseaspapadimas

Garmin Running MCP

Garmin Running MCP

Self-hosted Garmin Connect → ChatGPT/AI-agent integration for a personal running account. No Strava, subscription sync service, or public activity files. Built with a Cloudflare Worker, D1, private R2 and Alchemy. Garmin Connect's consumer API is unofficial and may change. This is an independent project, not affiliated with Garmin.

What it does

  • Cloudflare Access Managed OAuth limits the /mcp endpoint to one exact email address. The Worker verifies the signed Access assertion as well. No anonymous data tools or Garmin write actions.

  • Every six hours, the Worker incrementally caches running activities, laps, HR zones and available recovery metrics in D1; original activity downloads go to a private R2 bucket. Historical pagination is bounded (four activities per pass). Activity changes outside the most recent window are not automatically rescanned.

  • refresh_latest_run is an explicit, rate-limited Garmin read: it checks up to five recent activity summaries, updates the latest cached run and returns it. It is not a full history sync. Full details, charts and originals arrive on the scheduled sync; a newly refreshed run reports details_pending. The tool has a ten-minute cooldown and shares a single-flight lease with cron. Garmin may take time to process a new activity.

  • All other MCP tools are cache-only: latest_run, list_runs, run_details, run_chart, compare_runs, running_trends, recovery, training_context, sync_status. No agent can modify the Garmin account.

  • Garmin login is interactive on your machine. Password and MFA are not stored. Renewable OAuth tokens are AES-256-GCM encrypted in D1, with the key stored separately as a Worker secret. A revoked session requires repeating the interactive login.

Privacy: The D1 cache includes original Garmin JSON, and private R2 files can contain precise GPS tracks. Protect Cloudflare account access and the encryption key. The MCP tools do not expose the private R2 files or raw chart responses; run_chart exposes only a bounded allowlist of non-coordinate metrics. Activity names and timestamps may still be identifying.

Related MCP server: hybridlog

Requirements

  • Node 22+, pnpm 10.17.0, a Cloudflare account with Workers, D1, R2 and Zero Trust Access. R2 may require a billing method. A ChatGPT plan/UI that supports private custom MCP apps and OAuth (or another compatible MCP client).

  • An Alchemy default Cloudflare OAuth profile with access to your account (pnpm exec alchemy profile show). For one-time Garmin-session upload, Wrangler must separately have D1 access (pnpm exec wrangler login). Use the same Cloudflare account in both.

  • Your exact Cloudflare Access login email. If you don't have a Zero Trust organization, choose an available *.cloudflareaccess.com team domain; otherwise Alchemy discovers the existing domain without changing the organization.

Deploy

pnpm install
cp .env.example .env.prod
chmod 600 .env.prod
# Edit .env.prod with your CLOUDFLARE_ACCOUNT_ID, ACCESS_EMAIL and
# GARMIN_TOKEN_KEY (generate it with: openssl rand -base64 32).
# Set NEW_ACCESS_TEAM_DOMAIN only if there is no existing Zero Trust organization.
pnpm check && pnpm test
pnpm run plan
pnpm run deploy

Review the plan before deployment: the Access application must have Managed OAuth enabled, one exact-email allow rule, and your Worker as its destination. Do not connect an unauthenticated Worker to ChatGPT. Deployment prints the /mcp URL. Alchemy creates the Access app/policy, Worker, D1 schema, private R2 bucket and schedule. Keep the encryption key backed up securely; losing it prevents decrypting the Garmin session.

On a machine with an interactive terminal and Wrangler authenticated to the same account:

pnpm garmin:connect

Enter your Garmin email, hidden password and MFA code if requested. The bootstrap forces an OAuth1→OAuth2 refresh through the Worker-side client and reads one activity before uploading only encrypted tokens and IV to remote D1. Do not paste credentials, tokens or FIT/GPX data into a GitHub issue or commit them. .env.prod, Alchemy state, Wrangler state and .private/ are gitignored.

Optional unattended Cloudflare API token

Local deployments can use Alchemy's OAuth profile; no Cloudflare API token is needed. For unattended CI, provision an account-scoped token (or use an Alchemy admin profile to mint one) with only the permissions needed for Zero Trust Access organization/IdP and app/policy management, Workers Scripts (including Alchemy state-store bootstrap), D1, R2 and Secrets Store. Minting API tokens requires an additional API Tokens — Write bootstrap credential; do not grant it to routine deployments. Follow the Cloudflare permissions reference and Alchemy CI guide.

Connect and verify

  1. In ChatGPT, enable developer mode and create a private custom MCP app using the deployed https://…workers.dev/mcp URL with OAuth authentication. Approve the Access email PIN for your allowlisted email. If the client uses a different OAuth callback, add only its exact callback URL to dynamicClientRegistration.allowedUris in alchemy.run.ts and redeploy; do not remove authentication.

  2. Ask for sync_status (status: ok after cron), latest_run and run_details. Compare distance, time, laps and HR against Garmin Connect. Test list_runs, running_trends and recovery where available. After a new run appears on Garmin, explicitly call refresh_latest_run; updated, no_new_run, cooldown and busy describe what happened. Try again after its retry interval if Garmin is still processing the activity.

  3. An unauthenticated request to /mcp must receive 401. No tool should offer Garmin modification. run_chart is opt-in and paginates up to 100 samples from the first 500 cached chart points; it never returns GPS coordinates or the original ZIP/FIT.

The first scheduled sync runs at minute 17 every six hours (UTC). sync_status.backfill_offset advances only after a successful page; complete means the current archive was paginated. If you later import older runs into Garmin, verify their activity type is Running (GPX uploads can default to Other), verify they appear in Garmin's activity list, then intentionally reset backfill_offset to 0 in D1 so the next scheduled sync rescans history. Do not reset blindly or repeatedly; use a temporary bounded schedule only when supervised, then restore the six-hour cron. Garmin's API and manual imports can lag.

Tool semantics and limits

  • Dates on runs are Garmin-local dates. Recovery queries currently use UTC calendar dates and device-dependent metrics may be missing (null, not zero).

  • Pace = moving seconds × 1000 / distance in meters; elapsed time is preserved separately. Cadence uses Garmin's steps-per-minute summary or a moving-time-weighted lap average. compare_runs returns newest-minus-older numeric deltas (negative pace is faster). Different distances/intensities may limit comparisons.

  • At most 100 runs per range, 10 comparison IDs, 365 trend days, 100 samples per run_chart call. run_details excludes the chart; details_pending means a manual refresh has cached the summary but cron has not enriched it yet.

  • refresh_latest_run is not marked MCP read-only: it reads Garmin and mutates the local cache, never the Garmin account. It won't perform history pagination, recovery calls or original activity downloads. If cron is already running, it returns busy instead of racing token updates. sync_status separately tracks cron status and manual refresh status; after a manual failure, refresh_latest_error_stage contains only a fixed checkpoint and HTTP status (never an exception body), and is cleared on success.

  • training_context gives a starting goal of sub-30-minute 5K (6:00/km) and eventual 10K. Suggestions are for the runner to review, not medical advice or automatic workout writes.

Failure modes

needs_initial_login / needs_reauthentication: run the interactive bootstrap again and investigate revoked or expired Garmin tokens. rate_limited: wait rather than retrying rapidly. upstream_error: Garmin might be unavailable or its undocumented API may have changed. Do not log third-party exception bodies: they can contain credentials or OAuth tokens. The adapter is isolated in src/garmin.ts and the bootstrap in scripts/connect-garmin.ts for repair or replacement. Back up D1 and R2; keep the key separately.

Local checks: pnpm check && pnpm test; Worker bundle: pnpm exec wrangler deploy src/worker.ts --dry-run --outdir /tmp/garmin-build --compatibility-date 2026-09-20 --compatibility-flags nodejs_compat. Tests and dry-runs cannot prove production Access OAuth or Garmin token renewal; validate against your own account.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    B
    maintenance
    A stateless MCP server for Cloudflare Workers that exposes read-only Garmin Connect data (activities, health, sleep, training status, HRV, VO2 max) for a single securely linked personal account.
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    MCP server that mirrors your Garmin data into a personal database and exposes tools for health summaries, training load, muscle readiness, and race analysis, with optional chat-driven insights via stdio or HTTP.
    1
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Remote MCP server for Garmin Connect on Cloudflare Workers (free tier), providing 139 tools and 5 resources covering activities, health, training, workouts, nutrition, challenges, devices, gear, weight, women's health, courses, FIT-file analysis, and user profile, with OAuth 2.1 login flow.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables reading and analyzing Garmin Connect data—activities, health metrics, FIT files, and challenges—through natural language, for AI assistants like Claude and ChatGPT.
    MIT