This Model Context Protocol (MCP) server connects to Garmin Connect and exposes your fitness and health data to OpenWebUI, Claude or any other MCP-compatible clients via Streamable HTTP transport.
Credits: https://github.com/Taxuspt/garmin_mcp
-
78+ MCP Tools covering:
- Activity management (list, get details, splits, weather, gear)
- Health & wellness metrics (steps, heart rate, sleep, stress, body battery)
- Training & performance data (VO2 Max, HRV, training effect, fitness age)
- Device management
- Gear tracking
- Weight management
- Challenges & badges
- Workouts
- Women's health data
- Data management (add body composition, blood pressure, hydration)
- Personalised training and diet recommedations
-
Streamable HTTP Transport - Network-accessible MCP server for Kubernetes/container deployments
-
Token Persistence - OAuth tokens cached to avoid repeated MFA prompts
-
Non-interactive MFA - Supports containerised deployments with environment-based MFA codes
-
API Key Authentication - Secure HTTP/SSE endpoints with Bearer token or header verification (while keeping
/healthzendpoints public) -
Hybrid Memory & Disk Caching - 5-minute memory TTL for recent/dynamic queries, and permanent disk caching for historical/static data (queries >7 days old)
-
High Concurrency & Parallelism - Automatically offloads blocking third-party Garmin API calls to background worker threads to keep the event loop highly responsive
- Python 3.10+ (3.12 recommended)
- Garmin Connect account credentials
- For Kubernetes: kubectl access to your cluster
# Clone the repository
git clone <repository-url>
cd garmin_mcp
# Install dependencies
uv syncWith stdio transport (for MCP clients like Claude Desktop):
export GARMIN_EMAIL="your-email@example.com"
export GARMIN_PASSWORD="your-password"
export GARMIN_MCP_TRANSPORT="stdio"
uv run garmin-mcpWith Streamable HTTP transport (for network access):
export GARMIN_EMAIL="your-email@example.com"
export GARMIN_PASSWORD="your-password"
export GARMIN_MCP_TRANSPORT="streamable-http"
export GARMIN_MCP_HOST="0.0.0.0"
export GARMIN_MCP_PORT="8000"
uv run garmin-mcpThe server will be accessible at http://localhost:8000/mcp
If you have MFA enabled on your Garmin Connect account, you'll need to provide the 2FA code on first run. You have two options:
Option A: Interactive (for local development)
- The server will prompt for the MFA code in the terminal
Option B: Non-interactive (for automation)
export GARMIN_MFA_CODE="123456" # Code from email/SMS
export GARMIN_MFA_WAIT_SECONDS="180" # Optional: wait up to 180s for code to appearNote: If MFA is not enabled on your Garmin Connect account, you can skip these environment variables.
After successful login, OAuth tokens are saved to ~/.garminconnect and future runs won't require MFA until tokens expire.
Edit your Claude Desktop configuration:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"garmin": {
"command": "uv",
"args": ["run", "garmin-mcp"],
"env": {
"GARMIN_EMAIL": "your-email@example.com",
"GARMIN_PASSWORD": "your-password",
"GARMIN_MCP_TRANSPORT": "stdio"
}
}
}
}Note: If you have MFA enabled on your Garmin Connect account, add "GARMIN_MFA_CODE": "123456" to the env section for the first run.
Restart Claude Desktop after making changes.
The stdio config above launches the server locally. To connect to a server that
is already running over HTTP (GARMIN_MCP_TRANSPORT=streamable-http, see the
Docker/Kubernetes sections), point your MCP client at the /mcp endpoint and
pass the API key. When GARMIN_MCP_API_KEY is set, send it as a Bearer token
(or X-API-Key header, or ?api_key= query param):
{
"mcpServers": {
"garmin": {
"type": "streamable-http",
"url": "https://your-domain/mcp",
"headers": { "Authorization": "Bearer your-api-key" }
}
}
}If your client cannot send custom headers, append the key to the URL instead:
"url": "https://your-domain/mcp?api_key=your-api-key".
docker build -t garmin-mcp:latest .Basic run (stdio transport):
docker run --rm -it \
-e GARMIN_EMAIL="your-email@example.com" \
-e GARMIN_PASSWORD="your-password" \
-e GARMIN_MCP_TRANSPORT="stdio" \
-v garmin_tokens:/root/.garminconnect \
garmin-mcp:latestNetwork-accessible (Streamable HTTP):
docker run --rm -it \
-e GARMIN_EMAIL="your-email@example.com" \
-e GARMIN_PASSWORD="your-password" \
-e GARMIN_MCP_TRANSPORT="streamable-http" \
-e GARMIN_MCP_HOST="0.0.0.0" \
-e GARMIN_MCP_PORT="8000" \
-e GARMIN_MFA_CODE="123456" \
-e GARMIN_MFA_WAIT_SECONDS="180" \
-p 8000:8000 \
-v garmin_tokens:/root/.garminconnect \
garmin-mcp:latestNote: Only include GARMIN_MFA_CODE and GARMIN_MFA_WAIT_SECONDS if you have MFA enabled on your Garmin Connect account.
The server will be accessible at http://localhost:8000/mcp
Using Docker Compose:
Create docker-compose.yml:
version: '3.8'
services:
garmin-mcp:
build: .
image: garmin-mcp:latest
container_name: garmin-mcp
restart: unless-stopped
ports:
- "8000:8000"
environment:
- GARMIN_EMAIL=${GARMIN_EMAIL}
- GARMIN_PASSWORD=${GARMIN_PASSWORD}
- GARMIN_MCP_TRANSPORT=streamable-http
- GARMIN_MCP_HOST=0.0.0.0
- GARMIN_MCP_PORT=8000
# Only include MFA variables if MFA is enabled on your Garmin Connect account
- GARMIN_MFA_CODE=${GARMIN_MFA_CODE}
- GARMIN_MFA_WAIT_SECONDS=180
volumes:
- garmin_tokens:/root/.garminconnect
volumes:
garmin_tokens:Run with:
docker-compose up -d- Kubernetes cluster with kubectl configured
- PersistentVolume support (for token storage)
Create a Kubernetes Secret with your Garmin credentials:
kubectl create namespace mcpo # or your preferred namespace
kubectl create secret generic garmin-secrets \
--from-literal=email='your-email@example.com' \
--from-literal=password='your-password' \
--from-literal=mfa='123456' \
-n mcpoNote: The mfa key is only needed if you have MFA enabled on your Garmin Connect account. If MFA is not enabled, omit the --from-literal=mfa line. The mfa key is only needed for the first run or when tokens expire. You can remove it after tokens are established.
Create pvc.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: garmin-tokens
namespace: mcpo
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1GiApply it:
kubectl apply -f pvc.yamlCreate deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: garmin-mcp
namespace: mcpo
spec:
replicas: 1
selector:
matchLabels:
app: garmin-mcp
template:
metadata:
labels:
app: garmin-mcp
spec:
containers:
- name: garmin-mcp
image: garmin-mcp:latest # Replace with your image registry
imagePullPolicy: Always
ports:
- containerPort: 8000
name: http
env:
- name: GARMIN_EMAIL
valueFrom:
secretKeyRef:
name: garmin-secrets
key: email
- name: GARMIN_PASSWORD
valueFrom:
secretKeyRef:
name: garmin-secrets
key: password
- name: GARMIN_MCP_TRANSPORT
value: "streamable-http"
- name: GARMIN_MCP_HOST
value: "0.0.0.0"
- name: GARMIN_MCP_PORT
value: "8000"
# Only include MFA variables if MFA is enabled on your Garmin Connect account
- name: GARMIN_MFA_CODE
valueFrom:
secretKeyRef:
name: garmin-secrets
key: mfa
- name: GARMIN_MFA_WAIT_SECONDS
value: "180"
volumeMounts:
- name: tokens
mountPath: /root/.garminconnect
readinessProbe:
tcpSocket:
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
tcpSocket:
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
volumes:
- name: tokens
persistentVolumeClaim:
claimName: garmin-tokensApply it:
kubectl apply -f deployment.yamlCreate service.yaml:
apiVersion: v1
kind: Service
metadata:
name: garmin-mcp
namespace: mcpo
spec:
selector:
app: garmin-mcp
ports:
- name: http
port: 80
targetPort: 8000
type: ClusterIPApply it:
kubectl apply -f service.yamlIf using Istio, create httproute.yaml:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: garmin-mcp
namespace: mcpo
spec:
parentRefs:
- name: your-gateway
namespace: istio-system
hostnames:
- garmin-mcp.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /mcp
backendRefs:
- name: garmin-mcp
port: 80Apply it:
kubectl apply -f httproute.yaml| Variable | Description | Default | Required |
|---|---|---|---|
GARMIN_EMAIL |
Garmin Connect email address | - | Yes |
GARMIN_PASSWORD |
Garmin Connect password | - | Yes |
GARMIN_MFA_CODE |
2FA code (only if MFA is enabled) | - | No* |
GARMIN_MFA_WAIT_SECONDS |
Seconds to wait for MFA code | 0 |
No |
GARMINTOKENS |
Path to token storage directory | ~/.garminconnect |
No |
GARMIN_MCP_TRANSPORT |
Transport type: stdio or streamable-http |
http |
No |
GARMIN_MCP_HOST |
Bind host for HTTP transport | 0.0.0.0 |
No |
GARMIN_MCP_PORT |
Port for HTTP transport | 8000 |
No |
GARMIN_MCP_API_KEY |
Optional API key to secure SSE/HTTP server | - | No |
GARMIN_PREFETCH_DAYS |
Number of days to prefetch historical data | 180 |
No |
GARMIN_HEALTH_EXPORT_UTC_OFFSET |
UTC offset for /api/health-export timestamps (e.g. +02:00, -0500, or minutes like 120). Overrides the timezone read from your Garmin profile |
from profile | No |
*Required only if MFA is enabled on your Garmin Connect account, and only on first run or when tokens expire
OAuth tokens are automatically saved to ~/.garminconnect (or path specified by GARMINTOKENS) after successful login. These tokens persist across restarts, eliminating the need for MFA on subsequent runs until they expire.
For Kubernetes: Tokens are stored in the PersistentVolumeClaim, so they persist across pod restarts and deployments.
When using the HTTP or SSE transports, the server can be secured by setting the GARMIN_MCP_API_KEY environment variable. If set:
- Clients must authenticate by sending the key in either:
- An
Authorizationheader:Authorization: Bearer <your-api-key> - An
X-API-Keyheader:X-API-Key: <your-api-key> - A query parameter:
?api_key=<your-api-key>
- An
- Kubernetes health probes (
/healthzand/readyz) and the root path (/) remain public and return200 OK(returning{"status": "ok", "service": "garmin-mcp"}) without authentication.
In addition to the MCP transport, the HTTP server exposes a single read-only REST endpoint that returns the Health-Connect-supported metrics Garmin does not push natively, as one normalized JSON bundle:
GET /api/health-export?start=YYYY-MM-DD[&end=YYYY-MM-DD][&types=hrv,spo2,...]
- Auth: protected by the same
GARMIN_MCP_API_KEYguard described above (Bearer header,X-API-Key, or?api_key=). Same Cloudflare Tunnel — no new tunnel/DNS. - Params:
startis required;enddefaults to today;typesis an optional CSV subset ofhrv,spo2,respiration,resting_hr,vo2max,blood_pressure,hydration(omit for all). The range is capped at 370 days. - Response: one array per metric plus an
errors[]array. Every timestamp is timezone-aware ISO-8601 with an explicit offset (e.g.2026-06-07T03:14:00+02:00). The offset comes fromGARMIN_HEALTH_EXPORT_UTC_OFFSETif set, otherwise from your Garmin profile timezone. - Resilience: a failure on a single metric/day is recorded in
errors[]and the request still returns200with everything else.401on a bad key,400on invalid params,502if the Garmin session is unavailable. - Caching: per-day results are cached on disk (isolated from the MCP tool cache) so overlapping/repeated exports don't re-hit Garmin's rate limiter.
types value |
Source | Records |
|---|---|---|
hrv |
get_hrv_data |
granular hrvReadings, else nightly average (rmssd_ms) |
spo2 |
get_spo2_data |
{time, percent} |
respiration |
get_respiration_data |
{time, breaths_per_min} |
resting_hr |
get_rhr_day |
one daily {date, time, bpm} |
vo2max |
get_max_metrics |
one daily {date, time, value, sport} per sport |
blood_pressure |
get_blood_pressure |
{time, systolic, diastolic, pulse} |
hydration |
get_hydration_data |
one daily {date, start, end, volume_ml} |
Pass the API key exactly like the MCP transport — any one of:
# Bearer header
curl -s -H "Authorization: Bearer your-api-key" \
"https://your-domain/api/health-export?start=2026-06-06&end=2026-06-07"
# X-API-Key header
curl -s -H "X-API-Key: your-api-key" \
"https://your-domain/api/health-export?start=2026-06-06"
# query parameter (handy for quick checks)
curl -s "http://localhost:8000/api/health-export?start=2026-06-06&end=2026-06-07&api_key=your-api-key"
# limit to specific metrics
curl -s -H "Authorization: Bearer your-api-key" \
"https://your-domain/api/health-export?start=2026-06-06&types=hrv,spo2,resting_hr"{
"start": "2026-06-06",
"end": "2026-06-07",
"generated_at": "2026-06-08T10:00:00+02:00",
"hrv": [
{ "time": "2026-06-07T03:14:00+02:00", "rmssd_ms": 42.0, "granularity": "reading" }
],
"spo2": [ { "time": "2026-06-07T03:14:00+02:00", "percent": 96 } ],
"respiration": [ { "time": "2026-06-07T03:14:00+02:00", "breaths_per_min": 14.0 } ],
"resting_hr": [ { "date": "2026-06-07", "time": "2026-06-07T00:00:00+02:00", "bpm": 52 } ],
"vo2max": [ { "date": "2026-06-07", "time": "2026-06-07T00:00:00+02:00", "value": 48.2, "sport": "running" } ],
"blood_pressure": [],
"hydration": [ { "date": "2026-06-07", "start": "2026-06-07T00:00:00+02:00", "end": "2026-06-07T23:59:59+02:00", "volume_ml": 1500 } ],
"errors": []
}To optimize responsiveness and prevent hitting Garmin Connect API rate limits, the server utilizes a hybrid caching system:
- In-Memory Cache: Active queries and recent data (less than 7 days old) are cached in memory with a 5-minute Time-To-Live (TTL).
- Disk Cache: Historical queries (with dates older than 7 days) and completed activities (using
activity_idoractivityId) are permanently cached on disk (at~/.garminconnect/cache/perm/). - Mutation Bypass: Write and modification operations (e.g. adding weigh-ins, setting blood pressure, uploading workouts) automatically bypass the cache.
Additionally, all Garmin Connect API requests are offloaded to background worker threads. This guarantees that multiple parallel requests from the MCP client are processed efficiently without blocking the main async event loop.
- Invalid credentials: Verify your email and password are correct
- MFA required: If you have MFA enabled, ensure
GARMIN_MFA_CODEis set for first run - Token expired: Delete the token directory and re-authenticate
- Can't connect to server: Verify the server is binding to
0.0.0.0(not127.0.0.1) - Connection refused: Check firewall rules and port exposure
- 404 errors: Ensure you're using the correct transport type (Streamable HTTP for network access)
- Pod crash loops: Check logs with
kubectl logs -n mcpo deployment/garmin-mcp - Service not accessible: Verify Service selector matches Deployment labels
- Token persistence: Ensure PVC is properly mounted and has storage available
Docker:
docker logs <container-id>Kubernetes:
kubectl logs -n mcpo deployment/garmin-mcp -fThis server provides 78+ MCP tools. See TOOLS.md for a complete list organised by category.
Once connected to the MCP server, you can query your Garmin fitness data using natural language. Here are some example queries you can make:
-
"Show me my recent activities"
- Uses:
list_activities
- Uses:
-
"What running activities did I do between September 1st and November 6th?"
- Uses:
get_activities_by_datewith activity_type="running"
- Uses:
-
"Get details for activity ID 204592654"
- Uses:
get_activity
- Uses:
-
"Show me the splits for my last run"
- Uses:
get_activity_splits
- Uses:
-
"What was the weather during my activity 204592654?"
- Uses:
get_activity_weather
- Uses:
-
"How many steps did I take on November 6th?"
- Uses:
get_steps_data
- Uses:
-
"Show me my sleep data for November 5th"
- Uses:
get_sleep_data
- Uses:
-
"What was my heart rate on November 6th?"
- Uses:
get_heart_rates
- Uses:
-
"Get my body battery data from November 1st to November 6th"
- Uses:
get_body_battery
- Uses:
-
"What was my stress level on November 5th?"
- Uses:
get_stress_dataorget_all_day_stress
- Uses:
-
"Show me my resting heart rate for November 6th"
- Uses:
get_rhr_day
- Uses:
-
"Get my body composition data for November 6th"
- Uses:
get_body_composition
- Uses:
-
"What was my training readiness on November 6th?"
- Uses:
get_training_readiness
- Uses:
-
"Show me my hydration data for November 6th"
- Uses:
get_hydration_data
- Uses:
-
"Get my SpO2 (blood oxygen) data for November 6th"
- Uses:
get_spo2_data
- Uses:
-
"What was my respiration rate on November 6th?"
- Uses:
get_respiration_data
- Uses:
-
"What's my VO2 Max and fitness age for November 6th?"
- Uses:
get_max_metrics
- Uses:
-
"Get my HRV (Heart Rate Variability) data for November 6th"
- Uses:
get_hrv_data
- Uses:
-
"Show me my fitness age data for November 6th"
- Uses:
get_fitnessage_data
- Uses:
-
"What was my training effect for activity 204592654?"
- Uses:
get_training_effect
- Uses:
-
"Get my hill score from September 1st to November 6th"
- Uses:
get_hill_score
- Uses:
-
"Show me my endurance score between September 1st and November 6th"
- Uses:
get_endurance_score
- Uses:
-
"List all my Garmin devices"
- Uses:
get_devices
- Uses:
-
"What's my primary training device?"
- Uses:
get_primary_training_device
- Uses:
-
"Show me the gear I used for activity 204592654"
- Uses:
get_activity_gear
- Uses:
-
"What are my active goals?"
- Uses:
get_goalswith goal_type="active"
- Uses:
-
"Show me my personal records"
- Uses:
get_personal_record
- Uses:
-
"What badges have I earned?"
- Uses:
get_earned_badges
- Uses:
-
"Get my race predictions"
- Uses:
get_race_predictions
- Uses:
-
"What's my full name?"
- Uses:
get_full_name
- Uses:
-
"What unit system do I use?"
- Uses:
get_unit_system
- Uses:
-
"Show me my user profile"
- Uses:
get_user_profile
- Uses:
-
"Add a weight measurement: 75.5 kg"
- Uses:
add_weigh_in
- Uses:
-
"Get my weight measurements from November 1st to November 6th"
- Uses:
get_weigh_ins
- Uses:
-
"Add body composition data for November 6th"
- Uses:
add_body_composition
- Uses:
-
"Prepare me for a marathon with training and diet recommendations"
- Uses:
get_training_and_diet_recommendations
- Uses:
-
"I want to improve my running performance, give me training and diet recommendations"
- Uses:
get_training_and_diet_recommendations
- Uses:
-
"I want to lose weight - what should I do?"
- Uses:
get_training_and_diet_recommendations
- Uses:
The MCP server can handle complex, multi-step queries:
-
"Analyze my training week: show me my activities, sleep quality, and body battery from November 1st to November 6th"
- Combines:
get_activities_by_date,get_sleep_data,get_body_battery
- Combines:
-
"Compare my running performance: get my activities, training effect, and heart rate zones for my last 5 runs"
- Combines:
list_activities,get_training_effect,get_activity_hr_in_timezones
- Combines:
-
"Give me a complete health summary for November 6th: steps, sleep, stress, heart rate, and body battery"
- Combines:
get_steps_data,get_sleep_data,get_stress_data,get_heart_rates,get_body_battery
- Combines:
-
"Show my weekly single-pane summary for last week"
- Uses:
get_period_summarywith period="weekly", anchor_date="last week"
- Uses:
-
"Fetch a monthly dashboard summary including activities and readiness"
- Uses:
get_period_summarywith period="monthly"
- Uses:
-
"What are my trends over the last 4 weeks?"
- Uses:
get_trendswith start_date, end_date, include=["rhr","hrv","sleep","steps","body_battery"]
- Uses:
-
"Detect any recovery red flags this week"
- Uses:
detect_anomalieswith heuristic thresholds (defaults sensible)
- Uses:
-
"Give me a readiness breakdown for today"
- Uses:
get_readiness_breakdown
- Uses:
-
"How complete is my data this month?"
- Uses:
get_data_completeness
- Uses:
-
"Hydration target for a 60‑minute run at 28°C, weight 75 kg"
- Uses:
get_hydration_guidancewith weight_kg=75, training_minutes=60, temperature_c=28
- Uses:
-
"Coach cues for this week"
- Uses:
get_coach_cueswith period="weekly"
- Uses:
Tip: These tools accept natural timeframe phrases like
today,yesterday,last week,this week,last month,last 28 days. Ranges automatically clamp to today so mid-week requests never reach into the future.
When deployed with Streamable HTTP transport, the MCP server is accessible at:
- Local:
http://localhost:8000/mcp - Kubernetes with Istio:
https://garmin-mcp.example.com/mcp - Docker:
http://<container-ip>:8000/mcp
Configure your MCP client (OpenWebUI, Claude, etc.) to connect to the /mcp endpoint for Streamable HTTP transport.
- Never commit credentials: Use environment variables or Kubernetes Secrets
- Token storage: Tokens are stored locally/on PVC - ensure proper access controls
- Network security: For production, use TLS/HTTPS (terminate at Ingress/Gateway)
- Secret rotation: Rotate Garmin password regularly and update secrets accordingly
See LICENSE file for details.
Contributions are welcome! Please open an issue or submit a pull request.