Skip to main content
Glama

MCP Server for garth-ng

CI M8ven Score

A thin MCP server layer over the garth-ng library, exposing your own Garmin Connect data to an LLM via the Model Context Protocol — plus a small local store for body measurements Garmin cannot hold (see Body Measurements).

Disclaimer: This is an unofficial, personal project, not affiliated with or endorsed by Garmin. It uses garth-ng, which talks to undocumented, reverse-engineered Garmin Connect endpoints. Use at your own risk, with your own account and credentials.

Project Structure

mcp-garmin/
├── src/
│   ├── config.py            # Central config: env vars, credentials checked on demand
│   ├── server_instance.py  # Creates the shared MCPServer instance
│   ├── mcp_server.py       # Composition root: wires up the server + tools
│   ├── tools/               # One module per MCP tool
│   │   ├── _shared.py        # local_iso: normalizes garth's inconsistent timestamp types
│   │   ├── ping.py
│   │   ├── sleep.py
│   │   ├── hrv.py
│   │   ├── activity.py
│   │   ├── strength.py
│   │   ├── steps.py
│   │   ├── calories.py
│   │   ├── weight.py
│   │   ├── training_readiness.py
│   │   ├── metrics.py         # The five measurement tools over metrics_store.py
│   │   └── daily_review.py   # Morning check-in in one call: sleep, HRV, readiness, training
│   ├── metrics_store.py    # Local SQLite store for self-defined measurements (no Garmin access)
│   ├── garmin_client.py    # Garth login/session handling + data access
│   ├── main.py              # Entry point (starts the server over stdio)
│   └── explore_sleep.py     # Standalone script for manually testing the Garth integration
├── tests/                    # Mirrors src/, mocks the garth boundary
│   ├── test_garmin_client.py
│   ├── test_config.py
│   ├── test_main.py
│   └── tools/
│       ├── test_ping.py
│       ├── test_sleep.py
│       ├── test_hrv.py
│       ├── test_activity.py
│       ├── test_strength.py
│       ├── test_steps.py
│       ├── test_calories.py
│       ├── test_weight.py
│       ├── test_training_readiness.py
│       ├── test_shared.py
│       └── test_daily_review.py
├── .github/
│   └── workflows/
│       └── ci.yml            # CI: pytest + ruff on 3.11 and 3.14
├── .env.template            # Template for credentials and settings
├── pyproject.toml           # ruff configuration: line length and rule set
├── pytest.ini
├── requirements.txt
├── requirements-dev.txt     # Adds pytest/pytest-cov/ruff on top of requirements.txt
├── LICENSE
└── README.md

Related MCP server: garmin-mcp-triathlon

Installation

Python 3.11 or newer is required (tested with 3.11 and 3.14; the CI checks both).

# Create and activate a virtual environment
python3 -m venv venv
source venv/bin/activate

# Install dependencies
pip install -r requirements.txt

Setting Up Credentials

cp .env.template .env

Fill in your Garmin Connect credentials in .env (GARMIN_USERNAME, GARMIN_PASSWORD, both required). On first run, this logs in once and saves the session under ~/.garth — after that, the saved session is reused without logging in again.

Two optional settings, with sensible defaults if omitted:

  • LOG_LEVEL (default INFO) — one of DEBUG, INFO, WARNING, ERROR, CRITICAL

  • GARTH_SESSION_DIR (default ~/.garth) — where the cached session is stored

Usage

Start the MCP server (waits over stdio for a client, e.g. Claude Desktop):

python src/main.py

For manually testing the Garth integration (without the MCP protocol):

python src/explore_sleep.py

For interactively testing the MCP server with the MCP Inspector:

npx -y @modelcontextprotocol/inspector ./venv/bin/mcp run src/mcp_server.py:server

Opens a browser tab with the Inspector UI, where the available tools can be called directly.

Known issue: mcp dev src/mcp_server.py:server (the CLI's own dev command) currently fails — it spins up an isolated uv run --with mcp==<version> environment that's missing the cli extras mcp run itself needs. The command above works around this by pointing the Inspector directly at this project's own virtual environment instead.

Running Tests

pip install -r requirements-dev.txt
pytest -v
pytest --cov=src --cov-report=term-missing  # with coverage
ruff check src tests                        # lint gate, must be clean

requirements-dev.txt adds pytest, pytest-cov and ruff on top of requirements.txt. The ruff rule set (line length 100, E, F, I, UP, B) lives in pyproject.toml, so the same command gives the same result in the editor and in CI. Tests mock the garth boundary (no real network calls or credentials needed) and live under tests/, mirroring the src/ structure.

Deployment (Hermes)

Install it with its own venv (see Installation above).

Hermes registration (mcp_servers in the profile's config.yaml):

mcp-garmin:
  command: /path/to/mcp-garmin/venv/bin/python
  args: [/path/to/mcp-garmin/src/main.py]
  env:
    GARMIN_USERNAME: ${GARMIN_USERNAME}
    GARMIN_PASSWORD: ${GARMIN_PASSWORD}
    GARTH_SESSION_DIR: ${GARTH_SESSION_DIR}   # e.g. ~/.garth

Body Measurements

Garmin Connect accepts a fixed body-composition schema (weight and friends), so everything else — waist, hip, a pulse taken by hand — has no place there. Those values live in a local SQLite store instead, next to the Garmin data in your training log:

  • create_measurement_category(name, unit) — define a category once, e.g. waist / cm.

  • list_measurement_categories() — what already exists, with the number of values and the newest timestamp.

  • add_measurement(category, value, unit=None, measured_at=None) — record a value. The unit falls back to the category default; a different unit is stored as given. measured_at is local ISO 8601 (2026-10-18T06:35:00), a bare date means midnight, omitted means now. An unknown category is created on the fly when you pass a unit.

  • get_measurements(category, start=None, end=None) — the series, ascending.

  • get_measurement_summary(category, start=None, end=None) — per unit: count, first, last, delta, min, max and the ISO-week averages (2026-W42, Monday to Sunday).

A typical exchange:

You: My waist was 95 cm on 12 October and 96 cm on 14 October. Assistant: (calls add_measurement("waist", 95, "cm", "2026-10-12T07:00:00"), then the same for the 14th) You: How has my waist developed? Assistant: (calls get_measurement_summary("waist")) — one week so far, 2026-W42, average 95.5 cm from two measurements.

Rules worth knowing:

  • Units are never converted. cm and in stay separate blocks — you get back exactly the numbers you entered.

  • Weeks are ISO weeks (Monday–Sunday), labelled by the ISO week-year: 2025-12-29 belongs to 2026-W01.

  • Nothing leaves the machine. These five tools never call Garmin Connect; they need no credentials and no network.

  • The file is GARMIN_METRICS_DB, default ~/.garmin-metrics.db, created on first use (parent directory included).

  • Errors name the offending value: an unparsable timestamp, NaN/inf, an empty category, a duplicate category with the unit that already exists, or an unusable database path.

get_weight_summary stays the source for everything the scale measures — the store is for what Garmin cannot hold.

Available Tools

Tool

Description

ping

Health check, returns "pong"

get_sleep_summary(target_date)

Summary of sleep data for a date: total sleep time, sleep phases, scores, respiration rate, and more

get_hrv_summary(target_date)

Overnight heart rate variability: last-night/weekly averages and Garmin's status label (e.g. BALANCED)

get_activity_list(target_date, period)

Overview (id, timestamp, type) of activities in the period days up to target_date

get_activity_detail(activity_id)

Full detail for one activity: distance, duration, pace, heart rate, power, cadence, and more, depending on activity type

get_strength_exercises(activity_id, include_rest=False)

Sets of a strength training activity: exercise (category/name), repetitions, weight in kg, duration and local start time; REST sets only on request

get_calorie_summary(target_date, period=1)

Daily calorie expenditure for the period days (1-31) up to target_date: total = active + resting (BMR), plus the Garmin calorie goal; for today additionally the whole-day projection shown on the watch (resting calories scaled to 24 h + active so far)

get_steps_summary(target_date, period)

Daily step counts, distance, and step goal for each day in the period days up to target_date

get_weight_summary(target_date)

Weigh-in data for a date: weight, BMI, body composition (fat/water/bone/muscle), if measured

get_training_readiness_summary(target_date)

Garmin's most recent training readiness score and its contributing factors (sleep, HRV, recovery time, acute load, stress) for a date

create_measurement_category(name, unit)

Create a category for a self-defined measurement with its default unit (e.g. waist/cm); a duplicate fails and names the unit that already exists

list_measurement_categories()

Every category with its unit, the number of measurements and the timestamp of the newest one — look here first when the categories are unknown

add_measurement(category, value, unit=None, measured_at=None)

Store one value; the unit falls back to the category default, measured_at is local ISO 8601 (2026-10-18T06:35:00, a bare date means midnight, omitted means now); an unknown category needs a unit

get_measurements(category, start=None, end=None)

All values of a category in the range, ascending by timestamp; bounds are inclusive and a bare end date covers its whole day

get_measurement_summary(category, start=None, end=None)

Per unit found in the range: count, first, last, delta, min, max and the ISO-week averages with their Monday/Sunday and label (2026-W42)

| get_daily_review(target_date, days=6, language="en") | Morning check-in in ONE call: last night's sleep, HRV (vs. weekly average), training readiness with all factors, the latest weigh-in, the previous day's training with details and strength sets, steps, and the days before as context — pre-formatted text plus a ready-made status line for a training log. days is the training context before the reviewed day (0-13), language (en or de) sets weekday names and the status line |

All dates are ISO 8601 (YYYY-MM-DD).

Scope / Limitations

  • A single-user tool, not a service: it runs locally over stdio — no network port, no authentication.

  • It uses the unofficial Garmin Connect endpoints via garth-ng, which can break at any time (see the disclaimer above).

  • Credentials are stored in plain text in .env; the .garth session directory and .env must not be committed.

  • It handles personal health data. Keep the repository, logs and issue reports free of that data.

  • The five measurement tools are the exception to "Garmin only": they never call Garmin Connect. They keep self-defined values (waist, hip, …) in a local SQLite file (GARMIN_METRICS_DB, default ~/.garmin-metrics.db), because Garmin's only write endpoint accepts a fixed body-composition schema. Values of different units are stored as given and never converted.

License

MIT

Available Tools

40 tools
get_activitiesC

List of recent activities (limit/start pagination).

Inputs: limit, start. Returns: result

get_activity_detailC

Details for a single activity (activity_id).

Inputs: activity_id (required).

get_activity_mapC

Map data (GPS track) for an activity (activity_id).

Inputs: activity_id (required).

get_blood_pressureB

Blood pressure reading for a day (YYYY-MM-DD).

Inputs: day.

get_body_batteryB

Body Battery readings for a day (YYYY-MM-DD).

Inputs: day. Returns: result

get_body_battery_stressC

Body Battery + stress summary for a day (YYYY-MM-DD).

Inputs: day.

get_body_battery_stress_historyB

Body Battery + stress history for the last N days (up to end).

Inputs: end, days. Returns: result

get_body_weightC

Body weight for a day (YYYY-MM-DD) — grams, BMI, body fat, etc.

Inputs: day.

get_connected_devicesA

List of all connected Garmin devices.

Inputs: no named parameters. Returns: result

get_daily_heart_rateB

Daily heart rate for a day (YYYY-MM-DD): max, min, resting.

Inputs: day.

get_daily_hydrationB

Fluid intake including daily goal for a day (YYYY-MM-DD).

Inputs: day.

get_daily_stepsB

Step count for the last day (period=1, up to end, YYYY-MM-DD).

Inputs: end. Returns: result

get_daily_stressB

Stress history for the last N days (period=days, up to end, YYYY-MM-DD).

Inputs: end, days. Returns: result

get_daily_summaryB

Daily summary for a day (YYYY-MM-DD): steps, calories, heart rate.

Inputs: day.

get_daily_summary_historyA

Daily summary history for the last N days (up to end, YYYY-MM-DD).

Inputs: end, days. Returns: result

get_device_infoA

Active Garmin device: type, name, battery level.

Inputs: no named parameters.

get_fitness_activitiesC

Fitness activities (steps/calories) for the last N days (up to end).

Inputs: end, days. Returns: result

get_garmin_scoresA

Garmin fitness scores for a day (YYYY-MM-DD) — Vo2Max, Endurance, Power.

Inputs: day.

get_hrvB

HRV history for the last N days (period=days, up to end, YYYY-MM-DD).

Inputs: end, days. Returns: result

get_hydration_historyA

Fluid intake history for the last N days (period=days, up to end, YYYY-MM-DD).

Inputs: end, days. Returns: result

get_morning_readinessA

Morning Readiness for a day (YYYY-MM-DD) — alias for get_training_readiness.

Inputs: day.

get_nutrition_logB

Nutrition log for a day (YYYY-MM-DD) — calories, macros, meals.

Inputs: day.

get_nutrition_statusB

Nutrition status: current calorie goals and consumption.

Inputs: no named parameters.

get_personal_recordsC

All personal records.

Inputs: no named parameters. Returns: result

get_personal_record_typesC

Available record types.

Inputs: no named parameters. Returns: result

get_resting_heart_rateA

Resting heart rate history for the last N days (up to end, YYYY-MM-DD).

Inputs: end, days. Returns: result

get_sleepB

Sleep data for a day (YYYY-MM-DD) including sleep stage time blocks.

Inputs: day.

get_sleep_detailB

Sleep details (daily data) for a day (YYYY-MM-DD).

Inputs: day.

get_sleep_summaryB

Daily summary for a day (YYYY-MM-DD).

Returns only sleep-related fields if available, otherwise the full dict.

Inputs: day.

get_steps_goalB

Steps goal for a day (YYYY-MM-DD) — device + user + sync status.

Inputs: day.

get_training_readinessB

Morning Training Readiness for a day (YYYY-MM-DD).

Inputs: day.

get_training_status_dailyB

Training status for a day (YYYY-MM-DD).

Inputs: day. Returns: result

get_training_status_monthlyB

Training status for the last month (up to end, YYYY-MM-DD).

Inputs: end. Returns: result

get_training_status_weeklyB

Training status for the last week (up to end, YYYY-MM-DD).

Inputs: end. Returns: result

get_user_profileB

Garmin user profile: name, email, age, sex, height, location.

Inputs: no named parameters.

get_user_settingsB

User settings: VO2Max, thresholds, measurement system, sleep times.

Inputs: no named parameters.

get_weekly_stepsB

Step count for the last 7 days (period=7, up to end, YYYY-MM-DD).

Inputs: end. Returns: result

get_weekly_stressB

Stress history for the last 7 days (up to end, YYYY-MM-DD).

Inputs: end. Returns: result

get_weight_goalB

Weight goal for a day (YYYY-MM-DD) — target + target ranges.

Inputs: day.

get_weight_historyB

Weight history for the last N days (up to end, YYYY-MM-DD).

Inputs: end, days. Returns: result

Tool Schema Changelog

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

Loading tool schema changelog…

    TDQS

    C2.9/5.0

    Scored across 40 tools

    Disambiguation2/5

    Many tools have unclear or overlapping boundaries: get_sleep, get_sleep_detail, and get_sleep_summary all appear to return daily sleep data with no crisp distinction, while get_training_readiness and get_morning_readiness are explicit duplicates. The many body battery/stress and daily/weekly variants further blur selection.

    Naming Consistency4/5

    All tools follow a consistent get_<domain> snake_case pattern, which is predictable. However, suffixes like _detail, _summary, _history, and _status are used inconsistently, and duplicate alias names add minor confusion.

    Tool Count2/5

    40 tools is well beyond the typical well-scoped range and feels heavy for a read-only health data server. Many endpoints could be consolidated into parameterized fetch functions (e.g., period, date range, and detail level).

    Completeness4/5

    The server covers a broad range of Garmin health data: weight, heart, HRV, sleep, stress, training, steps, hydration, activities, records, devices, nutrition, and goals. It is mostly complete for read-only access, though a few history variants and some single-day-only metrics are missing.

    Maintenance

    ActivityActive
    ResponsivenessLimited data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      C
      maintenance
      Provides read access to Garmin Connect health and training data (sleep, heart rate, stress, HRV, activities) and supports writes like creating workouts, editing activities, and exporting files.
      854 npm
      MIT
    • A
      license
      B
      quality
      C
      maintenance
      Enables triathlon coaches and athletes to interact with Garmin Connect, including retrieving health/activity data, building and uploading structured workouts (cycling, running, swimming, brick), and accessing coaching analytics like readiness, load, and performance trends.
      172
      MIT
    • A
      license
      B
      quality
      D
      maintenance
      Connects to Garmin Connect and exposes fitness and health data via 110+ tools for activities, health metrics, workouts, training analytics, and more to MCP-compatible clients.
      138
      MIT
    • F
      license
      Not graded
      quality
      C
      maintenance
      Enables read-only access to Garmin Connect data through OAuth, including activities, sleep, HRV, body battery, training status, and scheduled TrainingPeaks workouts.
      -