Install
$ agentstack add mcp-ghighi3f-garmin-grafana-mcp-server ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ● Shell / process execution Used
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
garmin-grafana-mcp-server
An optional, self-hosted Model Context Protocol (MCP) server that exposes your Garmin training data to AI assistants. It is designed as a companion add-on to the arpanghosh8453/garmin-grafana project and reads directly from the InfluxDB instance that garmin-grafana already manages.
This server is a pure data access layer. It returns raw numbers from InfluxDB. All interpretation, analysis, and planning is delegated to the AI model consuming the tools — this server makes no assumptions about your fitness goals.
Table of Contents
- [How it fits into the ecosystem](#how-it-fits-into-the-ecosystem)
- [🪄 See it in action: The AI Coach](#-see-it-in-action-the-ai-coach)
- [Prerequisites](#prerequisites)
- [Quick Start (Docker Compose)](#quick-start-docker-compose)
- [Deployment (Docker — recommended)](#deployment-docker--recommended)
- [Local Development](#local-development)
- [Connecting to an MCP Client](#connecting-to-an-mcp-client)
- [MCP Tools](#mcp-tools)
- [Example prompts](#example-prompts)
- [Configuration reference](#configuration-reference)
- [Project structure](#project-structure)
- [Schema compatibility](#schema-compatibility)
- [Troubleshooting](#troubleshooting)
- [License](#license)
How it fits into the ecosystem
Garmin Device
│
▼
garmin-grafana ← you already have this running
├─ Garmin API sync
├─ InfluxDB (stores your data)
└─ Grafana (dashboards)
garmin-grafana-mcp-server ← this repo adds this
└─ Reads from the same InfluxDB
└─ Exposes MCP tools to AI clients (Claude, Copilot, etc.)
🪄 See it in action: The AI Coach
Because this MCP server provides structured, context-aware data to LLMs like Claude, it acts as a world-class personal coach.
Here is an actual, unedited response from Claude (via Copilot) summarizing a ride by cross-referencing six different MCP tools simultaneously (Activity Details, Training Status, Sleep/Recovery, Training Zones, and Stress):
> Prompt: "I just came back from my long Z2 ride. Please assess it in detail against our training plan!"
View Claude's Analysis (Click to expand)
1. SCORECARD vs PLAN
| Metric | Target | Actual | Grade | |---|---|---|---| | Duration | 120–150 min | 137.5 min | ✅ Perfect | | Time in Z1+Z2 | — | 99.4% (130.1 min) | ✅ Excellent | | Max HR | ≤130 (ceiling) | 135 bpm | ⚠️ Brief breach | | HR drift |
Prerequisites
| Requirement | Notes | |---|---| | garmin-grafana | Must already be running and populated | | Docker & Docker Compose | For the recommended deployment path | | Python 3.11+ | For local development only |
Quick Start (Docker Compose)
If you just want to run the pre-built image alongside your existing garmin-grafana stack, create a docker-compose.yml with the snippet below. No need to clone this repository.
services:
garmin-grafana-mcp-server:
image: ghcr.io/ghighi3f/garmin-grafana-mcp-server:latest
container_name: garmin-grafana-mcp-server
restart: unless-stopped
ports:
- "8765:8765"
environment:
# ── Required ─────────────────────────────────────────────
# Connection to the InfluxDB instance managed by garmin-grafana.
INFLUXDB_HOST: influxdb # container name on the shared Docker network
INFLUXDB_PORT: 8086
INFLUXDB_DATABASE: GarminStats # database name from your garmin-grafana .env
INFLUXDB_USERNAME: admin # InfluxDB v1 credentials
INFLUXDB_PASSWORD: adminpassword # ← change to your actual password
# ── InfluxDB v2 only (uncomment if you run v2) ──────────
# INFLUXDB_VERSION: 2
# INFLUXDB_TOKEN: "your-influxdb-token"
# INFLUXDB_ORG: "your-org"
# ── Optional: override schema names ─────────────────────
# Only needed if your garmin-grafana uses non-default measurement
# or field names. The defaults match the standard garmin-grafana schema.
#
# Measurement names:
# MEASUREMENT_ACTIVITIES: ActivitySummary
# MEASUREMENT_DAILY_STATS: DailyStats
# MEASUREMENT_SLEEP_SUMMARY: SleepSummary
# MEASUREMENT_ACTIVITY_SESSION: ActivitySession
# MEASUREMENT_ACTIVITY_LAP: ActivityLap
# MEASUREMENT_RESTING_HR: DailyStats
# MEASUREMENT_HRV: HRV_Intraday
# MEASUREMENT_VO2_MAX: VO2_Max
# MEASUREMENT_RACE_PREDICTIONS: RacePredictions
# MEASUREMENT_BODY_COMPOSITION: BodyComposition
#
# Field names:
# FIELD_RESTING_HR: restingHeartRate
# FIELD_HRV: hrvValue
# FIELD_VO2_MAX_RUNNING: VO2_max_value
# FIELD_VO2_MAX_CYCLING: VO2_max_value_cycling
# FIELD_RACE_5K: time5K
# FIELD_RACE_10K: time10K
# FIELD_RACE_HALF: timeHalfMarathon
# FIELD_RACE_MARATHON: timeMarathon
# FIELD_WEIGHT: weight
# FIELD_HR_ZONE_1: hrTimeInZone_1
# FIELD_HR_ZONE_2: hrTimeInZone_2
# FIELD_HR_ZONE_3: hrTimeInZone_3
# FIELD_HR_ZONE_4: hrTimeInZone_4
# FIELD_HR_ZONE_5: hrTimeInZone_5
#
# Training Status & Readiness (garmin-grafana v0.4.0+):
# MEASUREMENT_TRAINING_STATUS: TrainingStatus
# MEASUREMENT_TRAINING_READINESS: TrainingReadiness
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8765/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
default:
external: true
name: garmin-grafana_default # ← must match your garmin-grafana network
Then start it:
docker compose up -d
curl http://localhost:8765/health
> Tip: The only variables most users need to change are INFLUXDB_PASSWORD and possibly INFLUXDB_DATABASE. All schema variables have sensible defaults that match the standard garmin-grafana InfluxDB schema out of the box.
Deployment (Docker — recommended)
This is the recommended way to run the server alongside your existing garmin-grafana stack.
1. Clone this repository
git clone https://github.com/ghighi3f/garmin-grafana-mcp-server.git
cd garmin-grafana-mcp-server
2. Configure environment variables
cp .env.example .env
Edit .env to match your garmin-grafana setup. The most important values:
| Variable | Default | Description | |---|---|---| | INFLUXDB_HOST | influxdb | Container name of the InfluxDB service inside the garmin-grafana network. Override in .env only if your service is named differently. | | INFLUXDB_PORT | 8086 | InfluxDB port (usually unchanged) | | INFLUXDB_DATABASE | GarminStats | Database name set in your garmin-grafana config | | INFLUXDB_USERNAME | admin | InfluxDB username from your garmin-grafana .env | | INFLUXDB_PASSWORD | (see .env.example) | InfluxDB password from your garmin-grafana .env | | INFLUXDB_VERSION | 1 | 1 for InfluxDB v1; 2 for InfluxDB v2 | | MCP_PORT | 8765 | Port the MCP server listens on | | QUERY_TIMEZONE | UTC | IANA timezone — must match USER_TIMEZONE in garmin-grafana (see [Timezone](#timezone)) |
> Note: INFLUXDB_HOST is overridden to influxdb directly in docker-compose.yml so the container resolves the InfluxDB service by its Docker network name. You do not need to set it in .env for the Docker deployment.
3. Identify your garmin-grafana Docker network
The docker-compose.yml in this repo connects to the same Docker network that garmin-grafana creates. By default that network is named garmin-grafana_default.
Verify the network name:
docker network ls | grep garmin
If the name differs from garmin-grafana_default, edit the networks section at the bottom of docker-compose.yml:
networks:
default:
external: true
name: your-actual-network-name # ← change this
4. Start the server
docker compose up -d
Verify it is running:
curl http://localhost:8765/health
Expected response:
{
"influxdb": "connected",
"last_activity_timestamp": "2026-03-17T07:30:00+00:00",
"measurements_found": ["ActivitySummary", "DailyStats", "SleepSummary", "..."],
"mcp_endpoint": "http://localhost:8765/mcp",
"sse_endpoint": "http://localhost:8765/sse"
}
Local Development
If you want to run the MCP server directly on your host machine (outside Docker) during development, the server process needs to reach InfluxDB on port 8086. Since InfluxDB is inside a Docker network, you must expose its port to the host.
Step 1: Expose InfluxDB port in your garmin-grafana docker-compose.yml
Open the garmin-grafana docker-compose.yml and add a ports mapping to the influxdb service:
# In your garmin-grafana docker-compose.yml:
services:
influxdb:
image: influxdb:1.8
ports:
- "8086:8086" # ← add this line
# ... rest of your config
Then restart the garmin-grafana stack:
docker compose down && docker compose up -d
Step 2: Set up the Python environment
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
For InfluxDB v2, also install:
pip install influxdb-client
Step 3: Configure for local access
cp .env.example .env
Edit .env and set:
INFLUXDB_HOST=localhost # reach InfluxDB via the exposed host port
INFLUXDB_PORT=8086
Step 4: Run the server
python server.py
You should see:
============================================================
Garmin MCP Server
============================================================
InfluxDB : localhost:8086/GarminStats
Measurements: ['ActivitySummary', 'DailyStats', ...]
Transports : HTTP + SSE (always active)
/mcp (Streamable HTTP) : http://localhost:8765/mcp
/sse (SSE, deprecated) : http://localhost:8765/sse
/health : http://localhost:8765/health
============================================================
Connecting to an MCP Client
All HTTP transports are always active simultaneously — no MCP_TRANSPORT configuration needed for HTTP deployments. Point your client at the right URL and go.
Perplexity Mac / Legacy SSE clients
> Note: SSE transport is deprecated in the MCP specification. Kept for backward compatibility — prefer Streamable HTTP for new integrations.
{
"mcpServers": {
"garmin": {
"type": "sse",
"url": "http://:8765/sse"
}
}
}
ChatGPT / VS Code / Modern clients (Streamable HTTP)
{
"mcpServers": {
"garmin": {
"type": "http",
"url": "http://:8765/mcp"
}
}
}
Replace ` with the IP or hostname of the machine running the server (e.g. 192.168.1.100, pi5.local, or localhost`).
Claude Desktop / Cursor / Windsurf / Claude Code (Local stdio)
With Docker (server reads InfluxDB via the shared garmin-grafana network):
{
"mcpServers": {
"garmin": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"--network", "garmin-grafana_default",
"--env-file", "/path/to/.env",
"ghcr.io/ghighi3f/garmin-grafana-mcp-server:latest",
"python", "server.py"
],
"env": { "MCP_TRANSPORT": "stdio" }
}
}
}
Without Docker (local Python, INFLUXDB_HOST=localhost in .env):
{
"mcpServers": {
"garmin": {
"command": "python",
"args": ["/path/to/garmin-grafana-mcp-server/server.py"],
"env": {
"MCP_TRANSPORT": "stdio",
"INFLUXDB_HOST": "localhost",
"INFLUXDB_PORT": "8086",
"INFLUXDB_DATABASE": "GarminStats",
"INFLUXDB_USERNAME": "admin",
"INFLUXDB_PASSWORD": "your-password"
}
}
}
}
Or from the command line:
MCP_TRANSPORT=stdio python server.py
> Tip: The startup banner is written to stderr in stdio mode so it never interferes with the MCP protocol on stdout.
No configuration needed
All HTTP transports (SSE + Streamable HTTP) are always active on the same port. MCP_TRANSPORT only needs to be set for stdio (subprocess) clients.
MCP Tools
get_last_activity
Returns the single most recent Garmin activity with all available fields. No input parameters.
get_recent_activities
Returns activities from the last N days with a summary.
| Parameter | Type | Default | Range / Values | |---|---|---|---| | days | int | 7 | 1–90 | | sport_type | str | "all" | Any Garmin sport type (e.g. "running", "cycling", "hiking", "trail_running", "strength_training"). Supports partial matching — "cycling" also matches "indoor_cycling". "all" = no filter. | | limit | int | 20 | 1–100 |
get_weekly_load_summary
Groups activities into ISO calendar weeks with resting HR and HRV.
| Parameter | Type | Default | Range | |---|---|---|---| | weeks | int | 4 | 1–16 |
get_daily_recovery
Merges SleepSummary + DailyStats per date for a holistic recovery view.
| Parameter | Type | Default | Range | |---|---|---|---| | days | int | 7 | 1–14 |
get_activity_details
Detailed breakdown of a single activity: HR zones, training effect, per-lap splits, and analysis hints (fastest/slowest lap, HR drift). For activities with a power meter, also returns max_power, avg_power_pedaling_only, and standing_duration_seconds per lap.
| Parameter | Type | Description | |---|---|---| | activity_id | str | Activity ID from get_recent_activities |
get_fitness_trend
Long-term fitness trajectory: VO2max, race predictions, weight, resting HR — sampled weekly.
| Parameter | Type | Default | Range | |---|---|---|---| | weeks | int | 12 | 4–52 |
get_training_zones
HR zone distribution and polarization analysis (low/moderate/high intensity breakdown).
| Parameter | Type | Default | Range / Values | |---|---|---|---| | days | int | 30 | 7–180 | | sport_type | str | "all" | Any Garmin sport type (e.g. "running", "cycling", "swimming", "hiking"). Supports partial matching. "all" = no filter. |
explore_schema
Discover available InfluxDB measurements, fields, and tags at runtime. Useful for verifying field names before querying or debugging measurement mismatches.
| Parameter | Type | Optional | Description | |---|---|---|---| | measurement_name | str | Yes | Inspect a specific measurement (e.g. "ActivitySummary"). Omit to list all measurements. |
Returns (no measurement specified):
{"measurements": ["ActivitySummary", "DailyStats", "SleepSummary", "..."]}
Returns (measurement specified):
{
"measurement": "ActivitySummary",
"fields": [
{"field": "distance", "type": "float"},
{"field": "elapsedDuration", "type": "integer"},
"..."
],
"tags": ["userId", "deviceId", "..."]
}
get_stress_body_battery
Daily stress breakdown and body battery trend over 7–30 days. Surfaces systemic fatigue patterns that get_daily_recovery buries inside a per-day blob.
| Parameter | Type | Default | Range | |---|---|---|---| | days | int | 7 | 7–30 |
Returns:
- Per-day: stress minutes (high/medium/low/rest) + body battery (at wake, high, low, drained, charged)
- Summary: period averages + trend direction (
"improving"/"declining"/"stable"for body battery;"improving"/"worsening"/"stable"for stress)
get_personal_records
All-time personal records grouped by sport type. For each record, returns the value, unit, and the activityid, date, and activityname of the record-setting activity.
| Parameter | Type | Default | Range / Values | |---|---|---|---| | sport_type | str | "all" | Any Garmin sport type (e.g. "running", "cycling"). Supports partial matching. `"all
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: ghighi3f
- Source: ghighi3f/garmin-grafana-mcp-server
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.