Skip to main content
Glama

Garmin Coach MCP Server (garmin-coach-mcp)

A Personal Health, Running, and Fitness Coach Model Context Protocol (MCP) server powered by Garmin Connect telemetry. Built on the official Python mcp SDK (FastMCP), garminconnect, garth, and Pydantic v2, this server transforms noisy, deeply nested Garmin Connect API responses into token-efficient, coach-ready Markdown and structured JSON for both local AI clients (stdio for Claude Desktop, Cursor, Antigravity, and Gemini CLI) and remote web/cloud clients (streamable-http over HTTPS with Bearer token authentication for gemini.google.com Connected Apps, Gemini Mobile, and Gemini Enterprise on Google Cloud Run).


Core Coaching Use Cases

  1. Daily Readiness & Morning Check-in

    • Combines Training Readiness, Body Battery (current, charged, drained), Overnight HRV (last night average, 5-min high, 7-day average vs. baseline), Sleep stages & score, Resting Heart Rate (RHR), Stress, and yesterday's training load in a single aggregated call (get_coach_daily_briefing) to answer: "Should I execute my hard interval session today, modify the pace, or swap for a recovery jog?"

  2. Deep Workout Analysis & Post-Activity Debrief

    • Inspects lap/split pacing evenness, Heart Rate Zone distribution, Power/Pace Zone compliance, elevation-adjusted effort, cadence, weather impact, and cardiac drift (%) (get_activity_details, list_recent_activities) to evaluate execution quality.

  3. Longitudinal Trend & Load Management

    • Tracks Acute-to-Chronic Workload Ratio (ACWR), aerobic/anaerobic load focus, VO2 Max, Endurance/Hill scores (get_training_status_and_load), and 7-to-90 day rolling averages & first-half vs. second-half deltas for RHR, HRV, sleep, steps, stress, and training volume (get_health_trends) to detect overreaching early.

  4. Adaptive Training Plans & Structured Workout Prescription

    • Synthesizes athlete biometrics, Lactate Threshold HR/Pace, FTP, and zone boundaries (get_body_composition_and_biometrics, garmin://athlete/profile) with current recovery state to build periodized training plans and generate Garmin-compatible structured workouts (create_structured_workout_json) with optional direct upload to Garmin Connect Calendar.


Related MCP server: claude-garmin

System Architecture

flowchart LR
    subgraph LocalClients["Local AI Clients"]
        IDEs["Claude Desktop / Cursor / Antigravity"]
        GeminiCLI["Gemini CLI"]
        Inspector["MCP Inspector"]
    end

    subgraph RemoteClients["Remote Web & Mobile Clients"]
        GeminiWeb["gemini.google.com<br/>(Connected Apps & Mobile)"]
        GeminiEnt["Gemini Enterprise"]
    end

    subgraph Server["garmin-coach-mcp (FastMCP / Cloud Run)"]
        AuthMiddleware["BearerAuthMiddleware<br/>(Authorization: Bearer + /healthz)"]
        Tools["8 Coaching Tools<br/>(briefing, health, activities, workouts)"]
        Resources["2 MCP Resources<br/>(athlete/profile, coach/context/today)"]
        Prompts["3 Coaching Prompts<br/>(morning, debrief, weekly-plan)"]
        Normalizers["Normalizers & Pydantic v2 Models<br/>(Metric/Imperial + Hybrid Formatter)"]
        Client["GarminCoachClient"]
        Cache[("SQLite TTLCache<br/>~/.cache/garmin-coach-mcp/cache.db")]
        Auth["GarminAuthManager<br/>(garth OAuth1/OAuth2)"]
    end

    subgraph Upstream["Token Stores & External Upstream"]
        Tokens[("Local Token Store<br/>~/.garminconnect (0700)")]
        TokenB64[("GARMIN_TOKEN_BASE64<br/>(Cloud Secret Manager)")]
        GarminAPI["Garmin Connect API"]
    end

    IDEs <-->|"MCP (stdio)"| Tools
    GeminiCLI <-->|"MCP (stdio)"| Tools
    Inspector <-->|"MCP (stdio / HTTP)"| AuthMiddleware
    GeminiWeb <-->|"HTTPS streamable-http (/mcp)<br/>+ Bearer Auth"| AuthMiddleware
    GeminiEnt <-->|"HTTPS streamable-http (/mcp)<br/>+ Bearer Auth"| AuthMiddleware
    AuthMiddleware --> Tools
    IDEs -.-> Resources
    IDEs -.-> Prompts

    Tools --> Normalizers
    Resources --> Normalizers
    Normalizers --> Client
    Client <-->|"Parameterized SQL"| Cache
    Client --> Auth
    Auth <-->|"Resume / Auto-Refresh"| Tokens
    TokenB64 -.->|"Stateless Hydration (0600)"| Tokens
    Auth <-->|"HTTPS"| GarminAPI

Installation & Setup

Prerequisites

  • Python 3.11+

  • uv (recommended) or standard python3 -m venv + pip

  • A Garmin Connect account

Option A: One-Click Install (./install.sh or .\install.ps1) — Recommended

Run the interactive one-click installer from the repository root:

Linux / macOS / WSL (Bash):

cd /path/to/GarminMCP
./install.sh

Windows (PowerShell 5.1+ / PowerShell 7+):

cd C:\path\to\GarminMCP
.\install.ps1

Both ./install.sh and .\install.ps1 automate the complete 5-stage setup workflow:

  1. [1/5] System Requirements Check: Verifies Python >= 3.11 (via python3/python/py -3 or uv).

  2. [2/5] Virtual Environment & Dependencies: Creates .venv and installs garmin-coach-mcp in editable mode with dev/test dependencies (uv pip install -e ".[dev]" or .venv/bin/pip / .venv\Scripts\pip.exe install -e ".[dev]"), then verifies garmin-coach-mcp --help.

  3. [3/5] Interactive Garmin Connect Login (MFA/2FA): Runs garmin-coach-mcp auth (or prompts whether to reuse existing tokens if already present in ~/.garminconnect).

  4. [4/5] OAuth Token & Permission Verification: Confirms non-empty OAuth token files exist in your token directory (with strict 0700 directory / 0600 file permissions on POSIX systems).

  5. [5/5] Optional Live Account Verification & MCP Config Output: Optionally runs garmin-coach-mcp verify to fetch your Garmin athlete profile and prints ready-to-copy MCP registration snippets for Claude Desktop / Cursor / Antigravity (~/.gemini/config/mcp_config.json) and Gemini CLI (gemini mcp add garmin-coach ...) using your machine's resolved paths.

Installer CLI Flags

Bash (./install.sh)

PowerShell (.\install.ps1)

Description

--token-dir <dir>

-TokenDir <dir>

Custom directory for Garmin OAuth tokens (defaults to GARMIN_TOKEN_DIR or ~/.garminconnect).

-y, --yes

-Yes, -y

Run non-interactively without [Y/n] confirmation prompts (keeps existing tokens and runs live verification automatically).

--skip-verify

-SkipVerify

Skip the live Garmin athlete profile read check in Step 5.

--skip-auth

-SkipAuth

Skip interactive Garmin login and token checks (Steps 3–5) — ideal for CI or automated testing.

-h, --help

-Help, -h

Show usage and available flags.


Option B: Manual Installation (uv or pip)

Linux / macOS / WSL:

cd /path/to/GarminMCP

# Using uv (recommended):
uv venv
uv pip install -e ".[dev]"

# Or using standard Python venv + pip:
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install -e ".[dev]"

Windows (PowerShell):

cd C:\path\to\GarminMCP

# Using uv (recommended):
uv venv
uv pip install -e ".[dev]"

# Or using standard Python venv + pip:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
.\.venv\Scripts\pip.exe install --upgrade pip
.\.venv\Scripts\pip.exe install -e ".[dev]"

Verify the CLI entrypoint is installed and responsive:

# Cross-platform via uv:
uv run garmin-coach-mcp --help

# Or directly via virtualenv binary (Linux / macOS / WSL):
.venv/bin/garmin-coach-mcp --help

# Or directly via virtualenv binary (Windows PowerShell):
.\.venv\Scripts\garmin-coach-mcp.exe --help

Authentication & MFA / 2FA Bootstrap Guide

Garmin Connect uses garth (OAuth1 + OAuth2) and frequently challenges logins with 2FA / MFA or Cloudflare protections. Because MCP servers run headlessly over stdio, interactive MFA prompts during an active agent tool call would hang stdio transport.

garmin-coach-mcp solves this with a two-stage authentication lifecycle:

Run the built-in interactive authentication command once in your terminal:

uv run garmin-coach-mcp auth

Or specify a custom email and token directory:

uv run garmin-coach-mcp auth --email your-email@example.com --token-dir ~/.garminconnect
  1. You will be prompted for your Garmin Connect email and password (hidden input) if not already set in the environment.

  2. If your account has 2FA / MFA enabled, you will be prompted interactively in your terminal for the one-time verification code sent via SMS or email.

  3. Upon success, garth saves oauth1_token.json and oauth2_token.json to ~/.garminconnect (with 0700 directory permissions).

  4. During all future headless MCP server runs, GarminAuthManager automatically resumes from ~/.garminconnect and refreshes short-lived OAuth2 tokens using the long-lived OAuth1 token (~1 year validity) without storing your raw password or re-prompting for MFA.

Step 2: Verify Saved OAuth Tokens & Account Access (garmin-coach-mcp verify)

After authenticating, verify your saved tokens and perform a live read check of your Garmin athlete profile:

uv run garmin-coach-mcp verify
# or with a custom token directory:
uv run garmin-coach-mcp verify --token-dir ~/.garminconnect

garmin-coach-mcp verify checks that your token files exist in ~/.garminconnect, connects to Garmin Connect, and prints a summary of your connected athlete name, configured unit system (metric / imperial), and key physiological anchors (Max HR, Resting HR, VO2 Max).

Step 3: Headless Environment Variable Fallback (Non-MFA Accounts Only)

If your account does not require interactive 2FA, you can optionally provide credentials via environment variables (see .env.example). Upon first login, tokens are still persisted to GARMIN_TOKEN_DIR for subsequent calls.

Environment Variables Reference

Variable

Default

Description

GARMIN_TOKEN_DIR

~/.garminconnect

Directory storing oauth1_token.json and oauth2_token.json (0700 permissions).

GARMIN_TOKEN_BASE64

(unset)

Base64-encoded token bundle produced by garmin-coach-mcp export-tokens for stateless Cloud Run / container deployments. Automatically hydrates GARMIN_TOKEN_DIR (0600 file permissions) on startup.

GARMIN_EMAIL

(unset)

Optional Garmin Connect email for headless fallback login.

GARMIN_PASSWORD

(unset)

Optional Garmin Connect password for headless fallback login.

GARMIN_CACHE_DIR

~/.cache/garmin-coach-mcp

Directory for the SQLite TTL cache (cache.db).

GARMIN_CACHE_TTL

900

Default cache TTL in seconds (900 = 15 minutes) for daily telemetry.

GARMIN_UNITS

metric

Unit system for distances, paces, elevations, and weights (metric or imperial).

GARMIN_MCP_TRANSPORT

stdio

MCP transport mode: stdio, streamable-http, or sse.

GARMIN_MCP_AUTH_TOKEN

(unset)

Bearer token secret (Authorization: Bearer <token>) required for remote streamable-http / sse requests when set.

GARMIN_MCP_PORT

8000 (or $PORT)

HTTP port when running in streamable-http or sse transport mode.


Ready-to-Copy MCP Client Configurations

Pre-built configuration files are available in examples/:

1. Claude Desktop / Cursor / Antigravity (uv Configuration)

Add the garmin-coach entry to your MCP client configuration (e.g., ~/.gemini/config/mcp_config.json or claude_desktop_config.json; see examples/uv_mcp_config.json):

{
  "mcpServers": {
    "garmin-coach": {
      "command": "uv",
      "args": [
        "--directory",
        "/path/to/GarminMCP",
        "run",
        "garmin-coach-mcp"
      ],
      "env": {
        "GARMIN_TOKEN_DIR": "~/.garminconnect",
        "GARMIN_CACHE_DIR": "~/.cache/garmin-coach-mcp",
        "GARMIN_CACHE_TTL": "900",
        "GARMIN_UNITS": "metric"
      }
    }
  }
}

2. Gemini CLI (~/.gemini/settings.json)

Merge the following block into your ~/.gemini/settings.json (see examples/gemini_cli_settings.json):

{
  "mcpServers": {
    "garmin-coach": {
      "command": "uv",
      "args": [
        "--directory",
        "/path/to/GarminMCP",
        "run",
        "garmin-coach-mcp"
      ],
      "env": {
        "GARMIN_TOKEN_DIR": "~/.garminconnect",
        "GARMIN_CACHE_DIR": "~/.cache/garmin-coach-mcp",
        "GARMIN_CACHE_TTL": "900",
        "GARMIN_UNITS": "metric"
      },
      "timeout": 30000,
      "trust": false
    }
  }
}

3. Direct Virtualenv Binary Configuration (examples/mcp_config.json)

If you prefer invoking the virtualenv entrypoint binary directly without uv run:

{
  "mcpServers": {
    "garmin-coach": {
      "command": "/path/to/GarminMCP/.venv/bin/garmin-coach-mcp",
      "args": [],
      "env": {
        "GARMIN_TOKEN_DIR": "~/.garminconnect",
        "GARMIN_CACHE_DIR": "~/.cache/garmin-coach-mcp",
        "GARMIN_CACHE_TTL": "900",
        "GARMIN_UNITS": "metric"
      }
    }
  }
}

Using with gemini.google.com (Web Client) & Google Cloud Run

1. How It Works

Unlike local desktop agents (Claude Desktop, Cursor, Antigravity, or Gemini CLI) that spawn an MCP subprocess on your workstation over stdio, gemini.google.com (Connected Apps) and Gemini Enterprise execute in Google's cloud and cannot spawn local processes on your laptop. Instead, they connect to remote MCP servers over HTTPS using the MCP streamable-http transport (/mcp endpoint) protected by authentication headers.

garmin-coach-mcp includes first-class support for stateless cloud deployments:

  • garmin-coach-mcp export-tokens & GARMIN_TOKEN_BASE64: Packages your locally bootstrapped oauth1_token.json and oauth2_token.json into a single Base64 secret string so stateless containers (such as Google Cloud Run) can hydrate your Garmin session in memory/tmpfs without interactive MFA prompts or persistent disks.

  • --transport streamable-http: Serves the MCP Streamable HTTP protocol at /mcp alongside an unauthenticated container liveness endpoint at GET /healthz.

  • BearerAuthMiddleware (--auth-token / GARMIN_MCP_AUTH_TOKEN): Protects your telemetry endpoint with constant-time (hmac.compare_digest) Bearer token verification (Authorization: Bearer <token>).


2. Step 1 — Bootstrap MFA Locally & Export Portable Token Bundle

First, complete interactive Garmin login (including any SMS/email MFA challenge) on your local machine and export the resulting garth OAuth1/OAuth2 tokens into a Base64 bundle:

# 1. Authenticate interactively once on your workstation
garmin-coach-mcp auth

# 2. Export the OAuth1 + OAuth2 token bundle as a Base64 string
garmin-coach-mcp export-tokens > garmin_token_b64.txt

Security Note: garmin_token_b64.txt grants access to your Garmin Connect account for up to ~1 year (the lifetime of the OAuth1 refresh token). Store it in Secret Manager and delete the local text file once uploaded.


Deploying to Google Cloud Run gives you a serverless, auto-scaling HTTPS endpoint with zero idle cost (scales to zero when not in use) backed by the included Dockerfile.

1. Store Secrets in Google Cloud Secret Manager

export PROJECT_ID="your-gcp-project-id"
export REGION="us-central1"

# Generate a strong random Bearer token for MCP client authentication
openssl rand -hex 32 > mcp_auth_token.txt

# Create secrets in Google Cloud Secret Manager
gcloud secrets create garmin-token-b64 \
  --project="$PROJECT_ID" \
  --replication-policy="automatic" \
  --data-file=garmin_token_b64.txt

gcloud secrets create garmin-mcp-auth-token \
  --project="$PROJECT_ID" \
  --replication-policy="automatic" \
  --data-file=mcp_auth_token.txt

# Clean up temporary local secret files
rm -f garmin_token_b64.txt

2. Build & Deploy to Cloud Run

gcloud run deploy garmin-coach-mcp \
  --project="$PROJECT_ID" \
  --region="$REGION" \
  --source . \
  --port 8080 \
  --allow-unauthenticated \
  --min-instances=0 \
  --max-instances=2 \
  --set-env-vars="GARMIN_MCP_TRANSPORT=streamable-http,GARMIN_UNITS=metric,GARMIN_TOKEN_DIR=/tmp/.garminconnect,GARMIN_CACHE_DIR=/tmp/.cache/garmin-coach-mcp" \
  --set-secrets="GARMIN_TOKEN_BASE64=garmin-token-b64:latest,GARMIN_MCP_AUTH_TOKEN=garmin-mcp-auth-token:latest"

(Note: --allow-unauthenticated at the Cloud Run ingress layer allows gemini.google.com to reach the HTTPS URL directly, while BearerAuthMiddleware inside garmin-coach-mcp enforces Authorization: Bearer <token> on every MCP request.)

3. Verify Your Cloud Run Deployment

export SERVICE_URL="https://garmin-coach-mcp-xxxxxxxxxx-uc.a.run.app"
export GARMIN_MCP_AUTH_TOKEN="$(cat mcp_auth_token.txt)"

# 1. Verify container health check (returns 200 OK JSON without auth)
curl -sS "$SERVICE_URL/healthz"
# -> {"status":"ok","service":"garmin-coach-mcp","version":"0.1.0"}

# 2. Verify unauthenticated requests to /mcp are rejected (returns 401 Unauthorized)
curl -i -sS "$SERVICE_URL/mcp"

# 3. Verify authenticated MCP request passes Bearer auth
curl -sS -X POST "$SERVICE_URL/mcp" \
  -H "Authorization: Bearer $GARMIN_MCP_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl-check","version":"1.0"}}}'

4. Step 2 — Option B: Quick Personal Testing via HTTPS Tunnel (Tailscale Funnel / Cloudflare Tunnel)

If you want to test gemini.google.com against your local workstation before deploying to Cloud Run, run garmin-coach-mcp locally in streamable-http mode bound to 127.0.0.1 and expose it via an HTTPS tunnel:

# 1. Start the MCP server locally on 127.0.0.1:8000 with Bearer auth enabled
export GARMIN_MCP_AUTH_TOKEN="$(openssl rand -hex 32)"
echo "Bearer Token: $GARMIN_MCP_AUTH_TOKEN"

garmin-coach-mcp \
  --transport streamable-http \
  --host 127.0.0.1 \
  --port 8000 \
  --auth-token "$GARMIN_MCP_AUTH_TOKEN"

In a second terminal, expose 127.0.0.1:8000 over HTTPS using Tailscale Funnel or Cloudflare Tunnel:

# Option 1: Tailscale Funnel
tailscale funnel 8000
# -> Exposes https://<your-machine>.<your-tailnet>.ts.net/mcp

# Option 2: Cloudflare Quick Tunnel
cloudflared tunnel --url http://127.0.0.1:8000
# -> Exposes https://<random-subdomain>.trycloudflare.com/mcp

5. Step 3 — Connect in gemini.google.com

Once your HTTPS endpoint (https://<your-service-url>/mcp) is live:

  1. Open gemini.google.com in your desktop browser.

  2. Navigate to Settings → Connected Apps (or Extensions / MCP Servers in Gemini Advanced / Gemini Enterprise).

  3. Click Add Custom MCP Server (or Connect App) and enter:

    • Name: Garmin Coach

    • Server URL / Endpoint: https://<your-service-url>/mcp

    • Transport: Streamable HTTP

    • Authentication: Bearer Token (paste your GARMIN_MCP_AUTH_TOKEN secret)

  4. Save and enable the connection.

  5. Automatic Mobile Propagation: Once registered under your Google account on gemini.google.com, your connected Garmin Coach MCP server automatically propagates to the Gemini mobile app (Android & iOS), allowing you to ask "How was my sleep and HRV last night, and what workout should I run today?" or "Create a 4x1km threshold workout and upload it to my Garmin watch" directly from your phone.


MCP Reference: Tools, Resources & Prompts

Every tool supports response_format: "hybrid" | "markdown" | "json" (default "hybrid"), returning both a concise human/coach-readable Markdown executive summary and a normalized Pydantic v2 JSON block.

8 MCP Tools

Tool Name

Parameters

Coaching Purpose & Returned Telemetry

get_coach_daily_briefing

target_date: str | None = Noneresponse_format = "hybrid"

Single-call morning check-in aggregator. Fetches Training Readiness score & factor breakdown, Body Battery (current, charged, drained), overnight HRV & 7-day baseline, Sleep score & stages, RHR, Stress, steps, yesterday's activities, and synthesizes a recommended_intensity (high_intensity, moderate_steady, easy_recovery, rest).

get_sleep_and_recovery

start_date: str | None = Noneend_date: str | None = Noneresponse_format = "hybrid"

Multi-day sleep architecture & autonomic recovery. Returns deep/light/REM/awake durations & percentages, sleep score, overnight HRV average vs. baseline, RHR, Body Battery recharge, SpO2, respiration rate, and sleep need over a date range (defaults to last 7 days).

get_training_status_and_load

target_date: str | None = Noneresponse_format = "hybrid"

Physiological load & fitness status. Returns Training Status (Productive, Peaking, Overreaching, etc.), VO2 Max (running & cycling), Acute Load, Chronic Load, Load Ratio (ACWR), low/high aerobic & anaerobic load balance, Recovery Time remaining, Fitness Age, Endurance Score, and Hill Score.

list_recent_activities

limit: int = 10activity_type: str | None = Nonestart_date: str | None = Noneend_date: str | None = Noneresponse_format = "hybrid"

Workout log browser. Lists recent workouts filtered by sport (running, cycling, strength, etc.) and date range with converted distance (km/mi), formatted pace (/km or /mi), duration, avg/max HR, power, cadence, elevation gain, calories, and Aerobic/Anaerobic Training Effect.

get_activity_details

activity_id: int | strresponse_format = "hybrid"

Deep-dive workout execution analysis. Breaks down lap/split pacing, time & percentage in Heart Rate Zones and Power/Pace Zones, elevation gain/loss, cadence, weather conditions, cardiac drift (%), and pacing evenness assessment.

get_health_trends

days: int = 28end_date: str | None = Noneresponse_format = "hybrid"

Longitudinal baseline & overtraining detector. Aggregates days (e.g., 7, 14, 28, 90) of RHR, HRV, sleep hours, steps, and stress, computing first-half vs. second-half deltas, percentage changes, trend directions (improving, declining, stable), total training volume, sport breakdown, and automated coach_flags.

get_body_composition_and_biometrics

start_date: str | None = Noneend_date: str | None = Noneresponse_format = "hybrid"

Body composition & threshold zone settings. Tracks weight trends (kg/lbs), BMI, body fat %, skeletal muscle mass, bone mass, body water %, daily hydration, and athlete threshold biometrics (Max HR, RHR, Lactate Threshold HR & Pace, Cycling FTP, VO2 Max).

create_structured_workout_json

workout_name: strsport_type = "running"steps: list[dict] | None = Nonedescription: str | None = Noneupload_to_garmin: bool = Falseresponse_format = "hybrid"

Workout prescription & Garmin Calendar uploader. Validates warmup, interval, recovery, rest, cooldown, and nested repeat blocks (time, distance, lap.button, iterations) with HR, pace, power, or cadence targets, compiles Garmin Connect workout JSON, and optionally uploads it directly to the athlete's watch/calendar when upload_to_garmin=True.

Example Tool Call: create_structured_workout_json

{
  "workout_name": "Threshold Cruise Intervals 4x1600m",
  "sport_type": "running",
  "description": "Warm up thoroughly, run 4x1600m at Lactate Threshold HR (Zone 4) with 90s jog recovery, then cool down.",
  "upload_to_garmin": false,
  "steps": [
    {
      "step_type": "warmup",
      "duration_type": "time",
      "duration_value": 900,
      "target_type": "heart.rate.zone",
      "target_zone_number": 2
    },
    {
      "step_type": "repeat",
      "duration_type": "iterations",
      "duration_value": 4,
      "repeat_steps": [
        {
          "step_type": "interval",
          "duration_type": "distance",
          "duration_value": 1600,
          "target_type": "heart.rate.zone",
          "target_zone_number": 4,
          "description": "Threshold pace effort"
        },
        {
          "step_type": "recovery",
          "duration_type": "time",
          "duration_value": 90,
          "target_type": "no.target",
          "description": "Easy recovery jog"
        }
      ]
    },
    {
      "step_type": "cooldown",
      "duration_type": "time",
      "duration_value": 600,
      "target_type": "heart.rate.zone",
      "target_zone_number": 1
    }
  ]
}

2 MCP Resources

Resource URI

Type

Description

garmin://athlete/profile

Static / Cached

Complete athlete profile including display name, unit preferences, Max HR, Resting HR, Lactate Threshold HR & Pace, FTP, VO2 Max, configured Heart Rate / Pace / Power Zones, and Personal Records (PRs).

garmin://coach/context/today

Dynamic

Real-time Markdown coaching snapshot for today combining readiness, overnight recovery, body battery, and yesterday's training load for instant context injection.


3 MCP Prompts

Prompt Name

Arguments

Coaching Workflow

morning-readiness-check

target_date: str | None = None

Calls get_coach_daily_briefing and produces a structured morning readiness assessment, identifies recovery bottlenecks (sleep architecture, HRV suppression, elevated RHR), and recommends today's training intensity adjustment.

post-workout-debrief

activity_id: str | None = None

Inspects the specified activity (or latest activity via list_recent_activities(limit=1)), executes get_activity_details and get_training_status_and_load, and evaluates pacing splits, cardiac drift, HR/Power zone compliance, and recovery time impact.

weekly-training-plan

goal: str = "Build aerobic threshold and 10K/Half-Marathon readiness"target_race_date: str | None = None

Analyzes 28-day trends (get_health_trends(days=28)), recent workouts (list_recent_activities), and ACWR load status (get_training_status_and_load) to prescribe a periodized 7-day schedule with polarized intensity distribution and optional structured workouts via create_structured_workout_json.


Testing & Verification

The test suite uses 100% offline, deterministic pytest fixtures in tests/—zero live Garmin credentials or network calls are required to run unit or integration tests.

Run the Full Test Suite

uv run pytest -v

Run with Coverage Report

uv run pytest --cov=garmin_coach_mcp --cov-report=term-missing

Verify Live Garmin Connect Session (garmin-coach-mcp verify)

To confirm that your local OAuth tokens in ~/.garminconnect are valid and can read your Garmin Connect athlete profile:

uv run garmin-coach-mcp verify --token-dir ~/.garminconnect

Interactive Smoke Testing with MCP Inspector

You can inspect tools, resources, and prompts interactively using the official MCP CLI / Inspector:

npx @modelcontextprotocol/inspector uv --directory /path/to/GarminMCP run garmin-coach-mcp

Or run the server in local streamable-http or sse mode (bound strictly to 127.0.0.1 by default):

uv run garmin-coach-mcp --transport streamable-http --host 127.0.0.1 --port 8000 --auth-token "local-dev-secret"

Security & Privacy Architecture

Health and biometric telemetry is sensitive personal data. garmin-coach-mcp enforces strict security boundaries by design:

  1. Restricted Token & Cache Permissions (0700 / 0600):

    • GARMIN_TOKEN_DIR (~/.garminconnect) and GARMIN_CACHE_DIR (~/.cache/garmin-coach-mcp) are created with 0o700 directory permissions, and OAuth token files (including those hydrated from GARMIN_TOKEN_BASE64) / SQLite cache files are restricted to owner-only access (0o600).

  2. Constant-Time Bearer Authentication for Remote HTTP (BearerAuthMiddleware):

    • When running with --transport streamable-http or --transport sse and --auth-token / GARMIN_MCP_AUTH_TOKEN, all MCP requests require Authorization: Bearer <token> verified via hmac.compare_digest (returning 401 Unauthorized with WWW-Authenticate: Bearer on missing/invalid tokens), while GET /healthz remains available for Cloud Run liveness probes.

  3. Zero Credential & Token Logging:

    • GarminAuthManager and GarminCoachClient never log passwords, MFA verification codes, GARMIN_TOKEN_BASE64 payloads, or raw OAuth1/OAuth2 session tokens in stack traces, stdout, stderr, or MCP error payloads.

  4. Parameterized SQLite Queries:

    • TTLCache in src/garmin_coach_mcp/cache.py exclusively uses parameterized SQL queries (? placeholders) to prevent SQL injection across cache lookups, insertions, and prefix invalidations.

  5. Local-Only Network Binding (127.0.0.1) & Non-Root Container:

    • Default transport is stdio. When running HTTP transports locally, --host binds exclusively to 127.0.0.1 by default (never 0.0.0.0), preventing unintended exposure on local networks. The Cloud Run Dockerfile runs as an unprivileged user (garmincoach, uid 10001).

  6. Opt-In Upstream Writes:

    • All tools are strictly read-only except create_structured_workout_json, which defaults to upload_to_garmin=False so workouts are only pushed to Garmin Connect when explicitly requested by the user.

Available Tools

8 tools
create_structured_workout_jsonA

Validate, build, and optionally upload a structured Garmin Connect workout.

Coaching Purpose: Translates a coach's workout prescription (warmups, intervals, recovery jogs, nested repeat blocks, cooldowns with time/distance/lap-button durations and HR/pace/power/cadence targets) into a schema-validated Garmin Connect ExecutableStepDTO / RepeatGroupDTO JSON payload. Optionally uploads the workout directly to the athlete's Garmin Connect calendar/device queue when upload_to_garmin=True.

Args: workout_name: Descriptive title for the workout (e.g., "Threshold 4x1km Cruise Intervals"). sport_type: Target sport ("running", "cycling", "swimming", "strength", "cardio"). steps: List of step dictionaries matching WorkoutStepInput (step_type, duration_type, duration_value, target_type, target_zone_number, target_low, target_high, description, repeat_steps). If omitted, generates a standard 3-step template. description: Optional coaching notes or focus cues attached to the workout. upload_to_garmin: If True, uploads the generated JSON to Garmin Connect via client.upload_structured_workout(...). Defaults to False. response_format: Output representation: "markdown", "json", or "hybrid" (default).

Returns: Formatted workout summary and validated Garmin Connect workout JSON payload (including uploaded status and uploaded_workout_id when uploaded).

Inputs: workout_name (required), steps, sport_type, description, +2 more. Returns: result

get_activity_detailsA

Perform a granular post-workout debrief for a specific Garmin activity ID.

Coaching Purpose: Deep-dives into a single completed workout to evaluate interval execution, lap-by-lap pacing stability, time spent in Heart Rate and Power/Pace zones, elevation gain/loss, running/cycling cadence, first-half vs. second-half cardiac drift percentage, and environmental weather stress.

Args: activity_id: Numeric or string Garmin Connect activity ID (obtainable via list_recent_activities). response_format: Output representation: "markdown", "json", or "hybrid" (default).

Returns: Formatted deep-dive workout analysis including lap/split breakdowns, HR zone distribution, power/pace zone distribution, cardiac drift %, pacing assessment, and weather conditions.

Inputs: activity_id (required), response_format. Returns: result

get_body_composition_and_biometricsA

Fetch weight trends, body fat %, muscle mass, hydration, and physiological thresholds (LTHR, Max HR, FTP).

Coaching Purpose: Tracks body composition shifts (weight, body fat percentage, skeletal muscle mass, body water, BMI), daily hydration intake, and physiological threshold anchors (Lactate Threshold Heart Rate & Pace, Max HR, Resting HR, Cycling FTP, VO2 Max) used to calibrate training zones and race power/pacing targets.

Args: start_date: Start ISO date (YYYY-MM-DD). Defaults to a 30-day window ending on end_date. end_date: End ISO date (YYYY-MM-DD). Defaults to today. response_format: Output representation: "markdown", "json", or "hybrid" (default).

Returns: Formatted body composition and biometrics report with weight delta, weigh-in history, hydration averages, and Lactate Threshold / Max HR / FTP configurations.

Inputs: end_date, start_date, response_format. Returns: result

get_coach_daily_briefingA

Fetch a complete morning coaching briefing for a single date (defaults to today).

Coaching Purpose: High-level morning readiness aggregator designed to be the first tool called in a daily check-in. Synthesizes Training Readiness score & contributors, Body Battery (current, charged, drained), overnight Sleep quality & stages, Heart Rate Variability (HRV) status vs. personal baseline, Resting Heart Rate (RHR vs. 7-day baseline), daily Stress, and yesterday's completed workouts into an actionable training intensity prescription (high_intensity, moderate_steady, easy_recovery, or rest).

Args: target_date: Target ISO calendar date (YYYY-MM-DD). Defaults to today if omitted. response_format: Output representation: "markdown" for human-readable summary, "json" for compact normalized JSON, or "hybrid" (default) for Markdown + JSON.

Returns: Formatted coaching briefing containing readiness score, autonomic recovery metrics, sleep architecture, yesterday's training load, and today's recommended workout intensity.

Inputs: target_date, response_format. Returns: result

get_sleep_and_recoveryA

Analyze overnight sleep architecture, HRV vs. baseline, SpO2, respiration, and Body Battery recharge.

Coaching Purpose: Evaluates multi-day or single-night sleep quality and autonomic nervous system recovery. Useful for diagnosing cumulative sleep debt, suppressed nocturnal HRV, elevated sleeping heart rate/respiration (early illness or overreaching markers), and poor Body Battery recharge.

Args: start_date: Start ISO date (YYYY-MM-DD). If omitted, defaults to a 7-day window ending on end_date (or today). If provided and end_date is omitted, fetches that single day. end_date: End ISO date (YYYY-MM-DD). Defaults to start_date when start_date is explicitly provided, or today when start_date is omitted. response_format: Output representation: "markdown", "json", or "hybrid" (default).

Returns: Formatted sleep & recovery analysis including deep/light/REM/awake sleep stages, sleep score, SpO2, respiration rate, overnight HRV + baseline, and Body Battery recharge.

Inputs: end_date, start_date, response_format. Returns: result

get_training_status_and_loadA

Retrieve VO2 Max, Acute/Chronic Load (ACWR), Training Status, Recovery Time, Fitness Age, and Endurance/Hill scores.

Coaching Purpose: Quantifies macro- and meso-cycle training load progression and aerobic fitness adaptations. Assesses whether the athlete is Productive, Peaking, Maintaining, Strained, or Overreaching, inspects Acute-to-Chronic Workload Ratio (ACWR) for injury-risk management, and checks low/high aerobic vs. anaerobic load balance.

Args: target_date: Target ISO date (YYYY-MM-DD). Defaults to today if omitted. response_format: Output representation: "markdown", "json", or "hybrid" (default).

Returns: Formatted report of VO2 Max (running/cycling), Acute Load, Chronic Load, ACWR load ratio, Training Status, Recovery Time remaining, Fitness Age, Endurance Score, and Hill Score.

Inputs: target_date, response_format. Returns: result

list_recent_activitiesA

List recent workouts with distance, duration, HR, pace/power, elevation, calories, and Training Effect.

Coaching Purpose: Provides a scannable training log across running, cycling, swimming, strength, and cardio sessions. Supports filtering by workout type and date range to inspect training consistency, session load distribution, and recent activity IDs for deep-dives.

Args: limit: Maximum number of activities to return (default 10). activity_type: Optional sport filter (e.g., "running", "cycling", "strength"). start_date: Optional start ISO date (YYYY-MM-DD) filter. end_date: Optional end ISO date (YYYY-MM-DD) filter. response_format: Output representation: "markdown", "json", or "hybrid" (default).

Returns: Formatted activity log containing activity IDs, distances, durations, average/max HR, pace/power, elevation gain/loss, calories, and aerobic/anaerobic Training Effect.

Inputs: limit, end_date, start_date, activity_type, +1 more. Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.1/5.0

    Scored across 8 tools

    Disambiguation4/5

    Each tool targets a distinct analysis slice (daily readiness, sleep/recovery, training load, activity list, activity deep-dive, longitudinal trends, body composition, workout creation), so misselection risk is low. The only real overlap is get_coach_daily_briefing, which re-synthesizes HRV, sleep, Body Battery, and yesterday's workouts that dedicated tools also return, but it is explicitly framed as the entry-point aggregator, which mitigates confusion.

    Naming Consistency5/5

    All eight tools follow a clean verb_noun snake_case convention with consistent prefixes (get_*, list_*, create_*) and domain-descriptive nouns. Requests are parameterized uniformly with target_date/start_date/end_date and response_format across the read tools.

    Tool Count5/5

    Eight tools is well-scoped for a Garmin coaching/analytics server: one aggregator, five focused health/training readers, one activity deep-dive, and one workout builder. Each tool occupies a distinct analytical role without redundant entries.

    Completeness4/5

    The read surface is comprehensive for a coaching domain (readiness, sleep, load, activity list+details, trends, biometrics), and workout creation is covered. The notable gap is workout lifecycle management: there is no way to list scheduled/uploaded workouts, update or delete one after create_structured_workout_json, nor any calendar/planned-workout visibility, which could force workarounds.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      B
      quality
      D
      maintenance
      A Model Context Protocol server that integrates Garmin Connect data with LLMs to provide personalized running analysis and training plans. It enables users to monitor performance metrics, manage training loads, and receive data-driven workout suggestions based on health indicators like VO2 Max and recovery status.
      43
      5
      -
    • A
      license
      Not graded
      quality
      C
      maintenance
      MCP server that connects Garmin Connect data to Claude, enabling training analysis, recovery checks, and personalized plans based on real metrics like HRV, training load, and activities.
      24
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      A lightweight Garmin Model Context Protocol server with 12 curated endpoints for endurance athletes, enabling natural-language queries of activity, training, health, device, and goal data from Garmin Connect.
      28
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      An AI training coach MCP server that integrates with Garmin Connect to provide science-based load management, code-enforced injury gates, and persistent coaching memory, keeping data local.
      325 PyPI
      3
      MIT