garmin-mcp
Provides read-only access to a user's Garmin Connect data through Garmin's official Personal API, including daily summaries, sleep and HRV, activities and decoded FIT files, weight and body composition, nutrition, menstrual cycle data, and profile settings.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
garmin-mcp
A remote MCP server that gives Claude (web, desktop, mobile and Claude Code), or any MCP client, read-only access to your Garmin Connect data. It uses Garmin's official Personal API: no Garmin password, no scraping. It runs on Cloudflare Workers with the Agents SDK and is protected by OAuth 2.1.
Everyone deploys it to their own Cloudflare account with their own Garmin key: it is a single-user server (see Deployment).
Contents
Related MCP server: garmin-mcp
How it works
MCP client ──OAuth 2.1──▶ garmin-mcp Worker ─┬─ /authorize consent page, behind Cloudflare Access
(Claude, Cursor…) ├─ /token, /register, /.well-known/* OAuth server (OAuthProvider)
└─ /mcp MCP server (Streamable HTTP, stateless)
└─▶ personal.connectapi.garmin.comThe client connects to
/mcp. Without a token it gets a401pointing to the OAuth metadata (/.well-known/oauth-protected-resource/mcp).The client registers itself, through dynamic client registration (
/register) or a Client ID Metadata Document, and opens/authorizein the browser.You sign in and approve. Cloudflare Access protects
/authorize: it asks for a one-time code sent to your email before the page loads, and only your address is allowed. The consent page then shows which application is asking and where the token will be sent, and you click Allow. The server has exactly one user: you.The client gets its tokens: a 1-hour access token and a refresh token. The refresh token lasts as long as you use the client at least once a month, for up to a year.
The server calls Garmin. On each tool call it exchanges your Personal API key for a 24-hour Garmin token, caches it in memory and in KV, and calls the API.
The Garmin key never leaves the Worker: MCP clients only receive OAuth tokens issued by this server.
On top of forwarding API calls, the server shapes the responses so a model reads them well:
Long ranges: date ranges are split into the windows each endpoint allows (28 days for steps and stress, 31 for sleep or Body Battery, 1 year for daily summaries) and the results are merged.
Compact views: activity search and the daily and sleep histories return one row of key metrics by default;
detail=fullreturns Garmin's complete response.Aggregation:
Daily, sleep and weigh-in histories average by week or month (
period).Activity search totals by period and sport (
groupBy), merging subtypes: street, trail and treadmill running all count as running.
Clean output:
Nulls and social noise are dropped.
Epoch timestamps become ISO 8601 and decimals are rounded.
Sample arrays are left out unless requested (
includeSamples), and each result is capped at about 100,000 characters.
Fixed units: Garmin sends weight and body composition in grams although its reference says kilograms; the server returns kilograms.
Decoded FIT files:
garmin_get_activity_fitdownloads the original ZIP and decodes it with Garmin's official FIT SDK. It returns sessions, laps, per-kilometre splits computed from every record, Garmin's own splits, time in zones and, on request, the timeline as a table.Actionable errors: Garmin failures (expired key, invalid range, 429…) reach the model as tool errors that say how to fix them.
"Today" in your time zone: set by
TIMEZONE. The server instructions include today's date and weekday.
Tools and prompts
There is one tool per Personal API endpoint (26), with every documented parameter. All of them are read-only (readOnlyHint).
Tool | Returns | Garmin endpoint |
Daily | ||
| One whole day: steps, calories, floors, intensity minutes, heart rate, stress, Body Battery, SpO2, respiration, hydration |
|
| A summary per day for a range, or weekly or monthly averages |
|
| Steps, distance and goal per day |
|
| Daily stress (0-100) |
|
| Logged water, goal and estimated sweat loss |
|
| One day's heart rate: resting, min/max, 7-day average and, optionally, the full timeline |
|
| One day's pulse oximetry and hourly averages |
|
| Body Battery per day, with |
|
Sleep and HRV | ||
| One night: stages, score, sleep need, heart rate, stress, SpO2 |
|
| The nights in a range, or weekly or monthly averages |
|
| Overnight HRV: average, baseline and status |
|
| HRV per day for a range |
|
Activities | ||
| Search with every Garmin filter; with |
|
| Full summary of one activity and its splits |
|
| Structured workout performed during the activity |
|
| The decoded FIT file: sessions, laps, per-km splits, zones and timeline |
|
| Wellness activities in a range |
|
Body | ||
| Weight summary per day, range average and neighbouring weigh-ins |
|
| Every weigh-in with body composition, or weekly or monthly averages with min and max |
|
| Weight goals |
|
| Fitness age and its components |
|
| Heart rate zones configured per sport |
|
Other | ||
| Logged meals, goals and totals |
|
| Menstrual cycle summary |
|
| Menstrual cycle detail for one day |
|
| Units, time zone and profile preferences |
|
Prompts (shortcuts that clients show as commands):
readiness_check: how ready am I to train today? Combines sleep, HRV, Body Battery, resting heart rate, stress and recent load.training_review: a review of recent weeks of training (volume, intensity, recovery).
Repository layout
src/
index.ts Entry point: OAuthProvider protects /mcp and serves /authorize
env.d.ts Types for the bindings OAuthProvider adds at runtime
auth/authorize.ts Consent page, served only to requests that passed Cloudflare Access
garmin/
client.ts Personal API client: key exchange, token cache, errors
dates.ts Dates: "today" per time zone, range windows
fit.ts FIT unzipping and decoding, distance splits
mcp/
server.ts MCP server: instructions, tool and prompt registration
context.ts Shared schemas (dates, ranges) and range splitting
result.ts Response formatting: cleanup, timestamps, size cap, errors
summaries.ts Compact views and aggregations (period, groupBy)
tools/ Tools by area: daily, sleep, activities, body, profile
test/ Vitest suite running inside workerd (Garmin mocked)
evals/ Synthetic prompts and a runner to test with real Claude
.github/workflows/ CI: typecheck and tests on every push and PR
wrangler.jsonc Worker configuration: bindings, variables, required secrets
worker-configuration.d.ts Types generated with `pnpm cf-typegen`Configuration
Secrets
They live in .env locally and as Worker secrets in production. They never go to git.
Name | What it is | Where to get it |
| The Personal API key of your Garmin Connect account. The server exchanges it for 24-hour tokens. | In Garmin Connect → Personal API, signed in on the web: Create Key, accept the terms and pick an expiry (30, 90, 180 days or 1 year). It is shown only once. Only one key can be active: creating another requires deleting the current one. |
| Audience (AUD) tag of the Cloudflare Access application that protects | Zero Trust → Access → Applications → your application → Overview (see Cloudflare Access). Locally, keep |
Variables
Set in wrangler.jsonc, under vars. They are not secret.
Name | Value | Purpose |
|
|
|
|
| IANA time zone used to resolve "today" when a tool gets no date. |
Bindings
Binding | Type | Purpose |
| KV namespace ( | OAuth state: registered clients, granted consents and tokens (stored only as hashes, with their data encrypted). It also caches the Garmin token so every Worker instance shares it. |
Cloudflare Access
/authorize is protected by a self-hosted Access application, so only you can approve clients. It has no password to leak or brute-force: Access signs you in with a one-time code sent to your email.
In the Cloudflare dashboard, open Zero Trust. The first time, it asks you to pick a team name.
Go to Access → Applications → Add an application → Self-hosted:
Domain:
garmin-mcp.<your-subdomain>.workers.dev, pathauthorize. Only that path is protected;/mcp,/token,/registerand the metadata must stay reachable for MCP clients.Login method: One-time PIN, the only one enabled, with instant auth so you skip the provider screen.
Policy: Allow, include Emails → your email address.
Session duration: 24 hours is a good default.
Copy the application's Application Audience (AUD) tag and store it as
ACCESS_AUD.
wrangler dev simulates Access with the access.dev block in wrangler.jsonc, so locally ACCESS_AUD=local-dev and no sign-in is needed.
Environment files
File | In git | Contents |
| yes | Template with the keys documented and empty |
| no | Your local secrets. |
Local development
You need Node 26 (fnm use reads .node-version) and pnpm, at the version pinned by packageManager.
pnpm installcp .env.example .envFill in GARMIN_API_KEY in .env (ACCESS_AUD stays local-dev) and start the server. It runs at http://localhost:8787/mcp:
pnpm devTo try it by hand you have two options:
MCP Inspector: start it and connect to
http://localhost:8787/mcp. It takes you to the consent page.pnpm dlx @modelcontextprotocol/inspectorClaude Code, on the same machine:
claude mcp add --transport http garmin-local http://localhost:8787/mcp
Locally, KV is simulated and stored in .wrangler/state, and Access is simulated by access.dev in wrangler.jsonc.
Scripts:
Script | What it does |
| Local server with hot reload |
| Typecheck and tests |
| TypeScript for |
| Vitest suite inside workerd |
| Deploy to Cloudflare (plain |
| Regenerate |
| Run the synthetic prompts against a running server |
Tests, CI and evals
Tests (
test/) run inside the Workers runtime with@cloudflare/vitest-plugin, mocking Garmin's responses. They cover:the Garmin client: key exchange, token cache, retry on 401 and errors;
the full OAuth flow, and that the consent page refuses requests without this Access application's identity;
range splitting and search filters;
compact views, aggregations and the kilogram conversion;
a real FIT file generated with the SDK's encoder.
CI:
.github/workflows/ci.ymlrunspnpm typecheckandpnpm teston every push tomainand every pull request. It does not deploy.Evals:
evals/prompts.jsonholds 25 realistic questions: sleep, recovery, trends, training, totals, and cases with no data or out of scope. They are in Spanish on purpose, plus one in English, to test a non-English user.evals/run.mjsruns them through Claude Code in headless mode against a running server, with no tools other than this MCP. It records which tools the model chose, errors, sizes and the answer. It needs the Claude CLI signed in. Results contain your data and go toevals/results/, which is git-ignored.pnpm evals --base http://localhost:8787 --model sonnet
Deployment
What gets deployed
Worker |
|
MCP URL |
|
Root ( | JSON with the name, version and MCP URL, handy as a health check |
KV |
|
Secrets |
|
Access | A self-hosted Access application on |
Observability | Workers logs on (dashboard → Workers & Pages → garmin-mcp → Logs) |
Preview URLs | Off: the production origin is the only OAuth issuer |
How it's deployed | By hand from your machine with |
First deploy
Sign in to wrangler (if it hangs, see Troubleshooting):
pnpm wrangler loginCreate the Cloudflare Access application for
/authorizeand put its AUD tag in.envasACCESS_AUD, next toGARMIN_API_KEY. If you don't know yourworkers.devsubdomain yet, deploy first: untilACCESS_AUDmatches,/authorizeanswers 403.Deploy, uploading both secrets at the same time:
pnpm wrangler deploy --secrets-file .envWrangler prints the Worker URL: add
/mcpand connect a client.
Deploying changes
Check and deploy. Secrets and KV are kept between deploys:
pnpm check && pnpm run deployConnecting a client
Claude (web, desktop, mobile): on claude.ai, Settings → Connectors → Add custom connector, with the MCP URL. A connector added there is also available in the desktop and mobile apps.
Claude Code:
claude mcp add --transport http garmin https://garmin-mcp.<your-subdomain>.workers.dev/mcpThen authenticate it from
/mcp.Other clients (Cursor, VS Code…): any MCP client with OAuth and Streamable HTTP transport. Add it with the same URL.
The client opens /authorize in your browser: sign in with the code Access emails you, check the application name and destination on the consent page, and click Allow. This flow has been tested end to end with Claude on the web.
Operations
The Garmin key expires: Garmin emails you a week before. Create a new key (deleting the old one) and update the secret. The cached Garmin token stops being used on its own, because the cache is tied to the key.
pnpm wrangler secret put GARMIN_API_KEYChange who can approve clients: edit the policy of the Access application (Zero Trust → Access → Applications). Clients already connected keep working until their tokens expire.
Live logs:
pnpm wrangler tail garmin-mcpDisconnect every client: clear the
garmin-mcp-oauth-kvnamespace from the dashboard. Clients will have to authorize again.
Security
Access to
/mcp: only with an OAuth token issued by this server for its own URL, with thegarmin:readscope.Consent page:
It shows the application's name, whether it is verified and the host the token will be sent to, and warns when the destination is a local app.
The form carries a single-use handle bound to the browser and cannot be embedded in another site.
Only you reach it: Cloudflare Access signs you in with a one-time code to your email, and the Worker fails closed (403) unless the request carries this Access application's identity (
ctx.access), so it can't be bypassed through another route.
Secrets: the Garmin key only lives in the Worker's secrets. MCP clients never see it, and it never appears in logs or responses.
Read-only: the Personal API cannot write, and the server only sends
GETrequests.Personal use: the Personal API is for the key's owner. This server is meant for a single user and should not be shared with others.
Limits
What the Personal API doesn't offer: Training Readiness, VO2 max, race predictions, devices, gear, courses, or uploading workouts. It is read-only and on demand, with no webhooks.
Rate limits: Garmin publishes none. The server makes the requests for a range one after another, and
groupByscans at most 1,000 activities (10 requests).CPU: decoding a long FIT file takes more than the 10 ms of CPU per request of the Workers Free plan. The heaviest FIT file tested in production used 157 ms and worked.
Cost: a single user's KV and Workers usage fits comfortably in Cloudflare's free tier.
Troubleshooting
wrangler loginwaits and times out: use the device code flow. It doesn't need the browser to call back tolocalhost, and here it only asks for the permissions this project needs:pnpm wrangler login --device --scopes account:read user:read workers:write workers_kv:write workers_scripts:write workers_tail:read/authorizeanswers 403 "Access required": the request didn't come through the Access application, orACCESS_AUDdoesn't match its AUD tag. Check the application's domain and path and the secret.Tools answer "Garmin rejected the Personal API key": the key expired or was deleted. Create another and update
GARMIN_API_KEY.A client can't connect or asks to authorize again: its refresh token expired (over a month without use) or the KV was cleared. Authorize it again from the client.
Two
wrangler devat once fail withSQLITE_BUSY: they share.wrangler/state. Start the second one with its own state:pnpm dev --port 8788 --persist-to /tmp/garmin-mcp-state
License
MIT © Iván Barcia
This server cannot be deployed
Maintenance
Related MCP Connectors
Remote MCP server for training, nutrition, wellness, and performance data with OAuth 2.0.
Unofficial Garmin Connect access: sleep, HRV, training, activities, workouts and weight.
Garmin data in Claude & ChatGPT via the Garmin Health API. OAuth sign-in, no password sharing.
Multi-tenant hosted MCP server for Oura Ring — 21 read-only tools, OAuth per user.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceExposes personal Garmin wellness data through MCP tools for accessing summary, sleep, HRV, heart rate, stress, body battery, and historical data.MIT
- FlicenseNot gradedqualityDmaintenanceExposes Garmin Connect health and activity data (steps, sleep, heart rate, etc.) via MCP tools, with built-in login and MFA support.-
- AlicenseNot gradedqualityDmaintenanceExposes Garmin Connect data through MCP, enabling retrieval of activities, health metrics, training status, and workouts, with OAuth authentication and confirmed write operations.MIT
- AlicenseNot gradedqualityCmaintenanceEnables MCP clients to read and update Garmin Connect data, including activities, health metrics, training status, and more, with per-account multi-tenant authentication.3MIT