# Garmin Grafana Mcp Server

> Bring your Garmin data to AI. An MCP server that bridges the garmin-grafana InfluxDB ecosystem with AI agents.

- **Type:** MCP server
- **Install:** `agentstack add mcp-ghighi3f-garmin-grafana-mcp-server`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ghighi3f](https://agentstack.voostack.com/s/ghighi3f)
- **Installs:** 0
- **Category:** [Integrations](https://agentstack.voostack.com/c/integrations)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ghighi3f](https://github.com/ghighi3f)
- **Source:** https://github.com/ghighi3f/garmin-grafana-mcp-server

## Install

```sh
agentstack add mcp-ghighi3f-garmin-grafana-mcp-server
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# garmin-grafana-mcp-server

An optional, self-hosted [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that exposes your Garmin training data to AI assistants. It is designed as a companion add-on to the [arpanghosh8453/garmin-grafana](https://github.com/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](https://github.com/arpanghosh8453/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.

```yaml
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:

```bash
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

```bash
git clone https://github.com/ghighi3f/garmin-grafana-mcp-server.git
cd garmin-grafana-mcp-server
```

### 2. Configure environment variables

```bash
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:

```bash
docker network ls | grep garmin
```

If the name differs from `garmin-grafana_default`, edit the `networks` section at the bottom of `docker-compose.yml`:

```yaml
networks:
  default:
    external: true
    name: your-actual-network-name   # ← change this
```

### 4. Start the server

```bash
docker compose up -d
```

Verify it is running:

```bash
curl http://localhost:8765/health
```

Expected response:

```json
{
  "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:

```yaml
# 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:

```bash
docker compose down && docker compose up -d
```

**Step 2: Set up the Python environment**

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

For InfluxDB v2, also install:

```bash
pip install influxdb-client
```

**Step 3: Configure for local access**

```bash
cp .env.example .env
```

Edit `.env` and set:

```env
INFLUXDB_HOST=localhost   # reach InfluxDB via the exposed host port
INFLUXDB_PORT=8086
```

**Step 4: Run the server**

```bash
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.

```json
{
  "mcpServers": {
    "garmin": {
      "type": "sse",
      "url": "http://:8765/sse"
    }
  }
}
```

---

### ChatGPT / VS Code / Modern clients (Streamable HTTP)

```json
{
  "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):

```json
{
  "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`):

```json
{
  "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:

```bash
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):**
```json
{"measurements": ["ActivitySummary", "DailyStats", "SleepSummary", "..."]}
```

**Returns (measurement specified):**
```json
{
  "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 **activity_id, date, and activity_name** 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](https://github.com/ghighi3f)
- **Source:** [ghighi3f/garmin-grafana-mcp-server](https://github.com/ghighi3f/garmin-grafana-mcp-server)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** yes
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-ghighi3f-garmin-grafana-mcp-server
- Seller: https://agentstack.voostack.com/s/ghighi3f
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
