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
- 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.
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 |
*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.
- 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.