A webhook receiver and MCP (Model Context Protocol) server that collects Garmin health data and makes it available to ChatGPT through OpenAI's Remote MCP integration.
- Garmin Webhook Receiver: Accepts health data from Garmin devices
- SQLite Persistence: Stores data in SQLite database on EFS for reliability
- MCP Integration: Provides health data to ChatGPT via OpenAI's Remote MCP
- AWS Deployment: Runs on ECS Fargate with Terraform infrastructure
- Rate Limiting: Protects webhook endpoints from abuse
Garmin Device → Webhook → SQLite DB → MCP Server → ChatGPT
- Daily step count
- Resting heart rate
- Active calories burned
- Sleep duration
- Body battery levels
- Clone the repository:
git clone https://github.com/yourusername/garmin-mcp.git
cd garmin-mcp- Install dependencies:
npm install- Set up environment variables:
cp env.example .env
# Edit .env with your configuration- Build and run:
npm run build
npm startdocker-compose up --buildYou can expose your local server with a temporary public HTTPS URL (no DNS or account needed):
brew install cloudflared
./scripts/cloudflared-quick.shThis prints a https://<random>.trycloudflare.com URL. Use it for testing endpoints:
- Webhook:
https://<random>.trycloudflare.com/garmin/webhook - Health:
https://<random>.trycloudflare.com/healthz
| Variable | Description | Required |
|---|---|---|
PORT |
Server port (default: 8080) | No |
MCP_API_TOKEN |
Bearer token for MCP authentication | Yes |
GARMIN_WEBHOOK_SECRET |
Secret for webhook signature verification | No |
GARMIN_API_KEY |
Garmin Health API key | No |
GARMIN_API_SECRET |
Garmin Health API secret | No |
POST /garmin/webhook- Receives Garmin health data
GET /mcp/tools- Lists available toolsPOST /mcp/tools/call- Executes a toolGET /mcp/sse- Server-sent events endpoint
GET /healthz- Health check endpoint
Get daily health summary for a user and date.
Parameters:
user_id(string, required): User identifierdate(string, optional): Date in YYYY-MM-DD format (defaults to today)
Get health data for the last N days.
Parameters:
user_id(string, required): User identifierdays(number, optional): Number of days to retrieve (default: 7)
We use git-based versioning for Docker images:
- Git Tags: Create semantic version tags (e.g.,
v1.0.0,v1.1.0) - Docker Tags: Images are tagged with both the git version and
latest - Rollback: Can easily rollback by updating ECS task definition to use a previous version
# 1. Create a git tag for the release
git tag v1.0.0
# 2. Build and push with versioning
cd terraform
./build-and-push.sh
# 3. Deploy to ECS
aws ecs update-service --cluster garmin-mcp-cluster --service garmin-mcp-service --force-new-deployment- v0.1.0: Initial release with SQLite persistence and integration tests
- Next:
v1.0.0- Production ready with Cloudflare Tunnel deployment
# List available versions in ECR
aws ecr describe-images --repository-name garmin-mcp --query 'imageDetails[*].imageTags' --output table
# Update task definition to use specific version
aws ecs update-service --cluster garmin-mcp-cluster --service garmin-mcp-service --task-definition garmin-mcp-task:REVISION_NUMBEROur infrastructure uses a cost-optimized architecture with Cloudflare Tunnel for secure access:
- ECS Fargate: Single task with 0.25 vCPU, 0.5GB RAM
- No NAT Gateway: Saves ~$33/month by using public IP for outbound only
- Cloudflare Tunnel: Provides secure HTTPS access without public inbound traffic
- EFS Storage: SQLite database persistence (~$0.30/month)
- Cloudflare Account: Create a tunnel and get the tunnel token
- AWS Credentials: Configure AWS CLI access
- Domain: Optional - for custom hostname instead of trycloudflare.com
-
Create Cloudflare Tunnel:
# Install cloudflared brew install cloudflared # Login to Cloudflare cloudflared tunnel login # Create tunnel cloudflared tunnel create garmin-mcp # Get tunnel token (save this) cloudflared tunnel token garmin-mcp
-
Configure Terraform:
cd terraform # Create terraform.tfvars with your tunnel token echo 'cloudflare_tunnel_token = "your-tunnel-token-here"' > terraform.tfvars
-
Deploy:
./deploy.sh
-
Configure Tunnel Route (optional):
# For custom domain cloudflared tunnel route dns garmin-mcp webhook.yourdomain.com
- No Public Inbound: Security group blocks all incoming traffic
- Outbound Only: Task can make outbound connections (Docker images, Cloudflare)
- Encrypted Tunnel: All traffic encrypted through Cloudflare's edge
- Cost Effective: No NAT Gateway or ALB required
- Build the Docker image
- Push to ECR
- Deploy to ECS Fargate
This is a personal, non-commercial project. See PRIVACY.md for details on data collection and usage.
Personal use only. This project is not intended for commercial use.
This is a personal project, but suggestions and improvements are welcome through issues and discussions.