garmin_mcp for Garmin Connect with MFA
Garmin MCP Server
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
Features
-
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
Prerequisites
- Python 3.10+ (3.12 recommended)
- Garmin Connect account credentials
- For Kubernetes: kubectl access to your cluster
Deployment Options
1. Standalone (Local Development)
Installation
# Clone the repository
git clone <repository-url>
cd garmin_mcp
# Install dependencies
uv sync
Running
With 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-mcp
With 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-mcp
The server will be accessible at http://localhost:8000/mcp
First-Time Setup (MFA - Only if enabled on Garmin Connect)
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 appear
Note: 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.
Configuration with Claude Desktop
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.
2. Docker Deployment
Build the Image
docker build -t garmin-mcp:latest .
Run the Container
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:latest
Network-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:latest
Note: 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
3. Kubernetes Deployment
Prerequisites
- Kubernetes cluster with kubectl configured
- PersistentVolume support (for token storage)
Step 1: Create Secrets
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 mcpo
Note: 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.
Step 2: Create PersistentVolumeClaim
Create pvc.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: garmin-tokens
namespace: mcpo
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
Apply it:
kubectl apply -f pvc.yaml
Step 3: Deploy the Application
Create 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-tokens
Apply it:
kubectl apply -f deployment.yaml
Step 4: Create Service
Create 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: ClusterIP
Apply it:
kubectl apply -f service.yaml
Step 5: (Optional) Create Istio HTTPRoute
If 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: 80
Apply it:
kubectl apply -f httproute.yaml
Environment Variables
| 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
Token Management
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.
Troubleshooting
Login Issues
- 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
Network Issues
- 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)
Kubernetes Issues
- 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
Viewing Logs
Docker:
docker logs <container-id>
Kubernetes:
kubectl logs -n mcpo deployment/garmin-mcp -f
Available Tools
This server provides 78+ MCP tools. See TOOLS.md for a complete list organised by category.
Example Queries
Once connected to the MCP server, you can query your Garmin fitness data using natural language. Here are some example queries you can make:
Activity Queries
-
"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:
Health & Wellness Queries
-
"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:
Training & Performance Queries
-
"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:
Device & Gear Queries
-
"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:
Challenges & Goals Queries
-
"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:
User Profile Queries
-
"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:
Data Management Queries
-
"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:
Training and Diet Recommedations
-
"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:
Complex Queries
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:
One‑Pane Summaries and Insights
-
"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.
Accessing the Server
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.
Security Notes
- 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
License
See LICENSE file for details.
Contributing
Contributions are welcome! Please open an issue or submit a pull request.