garmin-mcp
Provides tools for interacting with Garmin Connect, enabling triathlon training management: daily health metrics, activity analysis, training load, sport-specific analysis (running, cycling, swimming, strength), and structured workout creation.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@garmin-mcpHow's my training load looking this week?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
garmlink
A privacy-focused Garmin Connect MCP server for triathlon training. ~47 tools covering daily health metrics, activity analysis, training load, running, cycling, swimming, strength training, and workout creation — plus triathlon-specific analysis (brick workouts, sport volume balance, cross-sport fitness snapshots).
Deployed as a remote MCP server over HTTPS. Connects to Claude Desktop or Claude Code via the streamable-HTTP transport.
One-Time Auth Setup
Run this locally once to generate tokens:
Install locally:
pip install -e .Authenticate with Garmin:
garmlink-authSaves tokens to
~/.garminconnect/garmin_tokens.jsonand prints the base64 export command.Copy the printed
GARMIN_TOKENS_JSON=...value — you'll need it for the secrets step below.
Related MCP server: Strava MCP
Deploy to Google Cloud Run
Runs on Cloud Run's perpetual free tier. The service scales to zero when idle, so
the first request after a quiet period takes ~1-3s to wake — no dashboard step,
it just waits. --min-instances=0 is deliberate: one always-warm instance would
far exceed the free vCPU-second allowance.
Prerequisites: gcloud and gh installed.
Log in as yourself and create (or pick) a project:
gcloud auth login gcloud projects create garmlink # skip if you already have oneCloud Run's free tier requires billing to be enabled on the project. You are not charged inside the free limits, but a card must be on file.
Run the one-time setup — enables APIs, stores your Garmin secrets in Secret Manager, creates a deploy service account, and wires up keyless GitHub Actions auth via Workload Identity Federation:
./scripts/setup-cloudrun.shIt prompts for
GARMIN_EMAILandGARMIN_TOKENS_JSON. The OAuth variables in the table below —GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET,GITHUB_ALLOWED_USERS,PUBLIC_BASE_URL,READYZ_TOKEN— aren't managed by this script yet, so set them by hand before deploying, withgcloud run services update garmlink --set-env-vars/--set-secrets.Edit the variables at the top of the script first if you want a different project id, region, or service name.
Deploy — push to
main, or trigger the workflow by hand:gh workflow run "Deploy to Cloud Run"Verify:
URL=$(gcloud run services describe garmlink --region us-central1 --format='value(status.url)') curl "$URL/health" # {"status":"ok"} curl -o /dev/null -w '%{http_code}\n' "$URL/mcp" # 401 - auth is workingCheck the Garmin session. The server no longer logs in to Garmin at startup — it authenticates on the first tool call and re-authenticates itself if the session dies. That means expired tokens show up as failing tool calls rather than a failed deploy, so check readiness explicitly:
curl -H "Authorization: Bearer $READYZ_TOKEN" "$URL/readyz"Reports
neveruntil the first tool call, thenauthenticated. A503with"garmin": "error"means the tokens are bad — re-rungarmlink-authand update theGARMIN_TOKENS_JSONsecret.Garmin rotates the DI refresh token on every refresh, which invalidates the value that was presented. Those rotations are persisted to Firestore (collection
garmin-tokens), because/tmpis wiped on every cold start of a scale-to-zero service — without that, the seed goes stale the first time a token rotates and every subsequent cold start fails withFailed to retrieve social profile. Thestartuplog line reports"garmin_tokens":"firestore"when this is wired up;"ephemeral"means it is not, and the deploy will work until the first rotation and then break.
Auto-Deploy via GitHub Actions
Every push to main deploys via .github/workflows/deploy.yml. Authentication
is keyless — GitHub mints a short-lived OIDC token that Google exchanges for
credentials, so there is no long-lived service-account key in your repo secrets.
The setup script sets the three repo variables the workflow reads
(GCP_PROJECT_ID, GCP_WIF_PROVIDER, GCP_DEPLOY_SA).
Connecting a Client
Auth is GitHub OAuth now — there is no bearer token to paste into a client.
claude.ai (web and mobile): Settings → Connectors → Add custom connector,
then paste https://<your-cloud-run-url>/mcp. claude.ai drives the GitHub
OAuth flow itself; sign in with a GitHub account listed in
GITHUB_ALLOWED_USERS.
Claude Desktop / Claude Code: add the server with no headers field —
the client opens a browser for the same OAuth flow on first use. Add to
~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"garmlink": {
"url": "https://<your-cloud-run-url>/mcp"
}
}
}Replace <your-cloud-run-url> with the URL printed at the end of the deploy
workflow (or from step 4 above).
Coaching Workflows
Eight guided workflows ship with the server as MCP prompts, so they work in any MCP client rather than only in this project directory.
Prompt | Purpose |
| Daily readiness briefing (HRV, sleep, body battery) |
| Weekly training load and sport balance review |
| Acute:chronic ratio, ramp rate and injury risk |
| Split-by-split review of one session — pacing, decoupling, execution |
| Pre-race fitness assessment across all disciplines |
| Reconcile today's planned session against actual readiness |
| Design a multi-week base block → schedules it on Garmin |
| Guided structured workout builder → pushes to Garmin |
The cross-sport ones (analyze_week, load_check, race_readiness,
build_training_block) always report swim, bike and run together, because
training load does not partition by sport. The per-session ones
(session_debrief, create_workout_guide) branch on sport, because the tools
do — a swim needs stroke and SWOLF data, a run needs running dynamics.
adapt_plan and build_training_block can write to the Garmin calendar. Both
show the proposed change and wait for an explicit yes first.
Indoor bike sessions are not built as Garmin workouts — they are ridden in Zwift
under ERG, so the prompts recommend a workout from Zwift's own library instead.
They must confirm by web search that the workout is in a current collection
first: whatsonzwift.com marks collections Zwift deleted in its October 2023
library reorg as (legacy), and the same workout name can appear in both a
current and a legacy collection. Outdoor rides still go through create_workout.
How they surface depends on the client: Claude Desktop lists them in its prompt
menu, and Claude Code exposes them as /mcp__garmlink__morning_check and so on.
Environment Variables
Variable | Description |
| Your Garmin Connect email |
| Base64-encoded token file (from |
| Optional. Only used to re-authenticate if the stored tokens expire. |
| Required (unless |
| Required. That app's client secret. |
| Required. Comma-separated GitHub logins allowed to use the server — the only access control once OAuth is on, so it fails closed: a blank value or a list naming nobody (e.g. |
| Required. The service's externally reachable URL, e.g. |
| Required. Bearer token guarding |
| Set to |
| Server port (default: 8000; Cloud Run injects 8080) |
|
|
|
|
Local Development
Use a Python 3.12 virtualenv. This is not optional: garminconnect 0.3.3+
requires 3.12, so a 3.11 interpreter silently resolves to 0.3.11's predecessor
0.3.2 — a different library from the one CI and production run, with different
return types. Tests then pass against an API that production never executes.
python3.12 -m venv .venv
.venv/bin/pip install -e .Run the suite (the same files CI runs):
.venv/bin/python tests/test_garmin_contract.py
.venv/bin/python tests/test_critical_fixes.py
.venv/bin/python tests/test_auth_lifecycle.py
.venv/bin/python tests/test_token_persistence.py
.venv/bin/python tests/test_auth_provider.py
GARMIN_EMAIL=x@y.z ALLOW_UNAUTHENTICATED=1 .venv/bin/python tests/test_workout_builder.py
GARMIN_EMAIL=x@y.z ALLOW_UNAUTHENTICATED=1 .venv/bin/python tests/test_tool_dispatch.py
GARMIN_EMAIL=x@y.z ALLOW_UNAUTHENTICATED=1 .venv/bin/python tests/test_prompts.py
GARMIN_EMAIL=x@y.z ALLOW_UNAUTHENTICATED=1 .venv/bin/python tests/test_logging.pyNote that real Garmin tokens in ~/.garminconnect mean a carelessly constructed
test client will reach the live Garmin API. Tests patch the GarminClient
constructor to prevent this; follow that pattern.
Logs
The server emits one structured line per notable event. On Cloud Run these are
JSON, and the platform lifts severity into the log viewer, so filtering by
error works:
{"severity":"INFO","message":"startup","tools":48,"prompts":8,"token_source":"secret","auth":"github_oauth","storage":"firestore","garmin_tokens":"firestore"}
{"severity":"INFO","message":"garmin.tokens.load","outcome":"ok","source":"store"}
{"severity":"INFO","message":"garmin.tokens.save","outcome":"ok"}
{"severity":"INFO","message":"tool.call","name":"get_daily_summary","args":{"date":"2026-08-20"},"outcome":"ok","dur_ms":214.0,"cache":"0h/1m"}
{"severity":"WARNING","message":"auth.reject","path":"/mcp","reason":"bad_token"}
{"severity":"WARNING","message":"garmin.retry","method":"get_stats","attempt":1,"outcome":"rate_limited"}tool.call is the important one: to Cloud Run's own request log every MCP call
is an indistinguishable POST /mcp, so this is the only place you can see
which of the 45 tools ran, how long it took, and whether it was served from
cache (cache counts hits/misses, since range tools make one call per day).
Two things are deliberately never logged: tool results, which are the health data this server exists to protect, and presented credentials on a rejected request. Arguments and error messages are passed through a redactor that strips token-shaped strings.
Reading them:
gcloud run services logs read garmlink --region us-central1 --project garmlink --limit 50This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceMCP server that connects Garmin Connect data to Claude, enabling training analysis, recovery checks, and personalized plans based on real metrics like HRV, training load, and activities.14MIT
- AlicenseBqualityAmaintenancePrivacy-first MCP server for Strava activities, streams, routes and training data.293382MIT
- AlicenseAqualityAmaintenanceLocal-first Garmin data warehouse with an analysis-grade MCP server. Sync once, analyze forever, even when the API is down.123MIT
- AlicenseAqualityBmaintenanceUnofficial MCP server bridging Garmin Connect to MCP clients and ChatGPT, providing tools to access health data, activities, and trends via a self-hosted API.11MIT
Related MCP Connectors
Garmin data in Claude & ChatGPT via the Garmin Health API. OAuth sign-in, no password sharing.
Garmin data in Claude: 135 tools — activities, sleep, HRV, training, workouts. Free, open source.
List, fetch, create, edit (replace), delete and schedule structured workouts on Garmin Connect (runn
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/knahsirV/garmlink'
If you have feedback or need assistance with the MCP directory API, please join our Discord server