mcp-garmin
An MCP server for read-only querying of your personal Garmin Connect health, fitness, activity, device, and profile data.
Retrieve daily or historical health metrics such as sleep, HRV, heart rate, stress, Body Battery, training readiness/status, steps, hydration, weight/body composition, blood pressure, and nutrition.
List and inspect activities, including activity details, GPS map data, fitness activities, and personal records.
Check goals and settings: steps goal, weight goal, nutrition status, Garmin fitness scores, user profile/settings, active device, and connected devices.
Query many metrics over configurable date ranges or with pagination, with structured outputs from many tools.
Provides tools for accessing Garmin Connect fitness data, including body metrics, heart rate, sleep, stress, activities, steps, hydration, devices, nutrition, goals, and user profile information.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-garmin Show my daily steps and sleep summary for this week."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
MCP Server for garth-ng
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.mdRelated 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.txtSetting Up Credentials
cp .env.template .envFill 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(defaultINFO) — one ofDEBUG,INFO,WARNING,ERROR,CRITICALGARTH_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.pyFor manually testing the Garth integration (without the MCP protocol):
python src/explore_sleep.pyFor interactively testing the MCP server with the MCP Inspector:
npx -y @modelcontextprotocol/inspector ./venv/bin/mcp run src/mcp_server.py:serverOpens 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 isolateduv run --with mcp==<version>environment that's missing thecliextrasmcp runitself 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 cleanrequirements-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. ~/.garthBody 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_atis 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: (callsget_measurement_summary("waist")) — one week so far,2026-W42, average 95.5 cm from two measurements.
Rules worth knowing:
Units are never converted.
cmandinstay 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 |
| Health check, returns |
| Summary of sleep data for a date: total sleep time, sleep phases, scores, respiration rate, and more |
| Overnight heart rate variability: last-night/weekly averages and Garmin's status label (e.g. |
| Overview (id, timestamp, type) of activities in the |
| Full detail for one activity: distance, duration, pace, heart rate, power, cadence, and more, depending on activity type |
| Sets of a strength training activity: exercise (category/name), repetitions, weight in kg, duration and local start time; REST sets only on request |
| Daily calorie expenditure for the |
| Daily step counts, distance, and step goal for each day in the |
| Weigh-in data for a date: weight, BMI, body composition (fat/water/bone/muscle), if measured |
| Garmin's most recent training readiness score and its contributing factors (sleep, HRV, recovery time, acute load, stress) for a date |
| Create a category for a self-defined measurement with its default unit (e.g. |
| Every category with its unit, the number of measurements and the timestamp of the newest one — look here first when the categories are unknown |
| Store one value; the unit falls back to the category default, |
| All values of a category in the range, ascending by timestamp; bounds are inclusive and a bare end date covers its whole day |
| Per unit found in the range: count, first, last, delta, min, max and the ISO-week averages with their Monday/Sunday and label ( |
| 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.garthsession directory and.envmust 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
Available Tools
40 toolsget_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
Scored across 40 tools
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.
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.
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).
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
Related MCP Connectors
Unofficial Garmin Connect access: sleep, HRV, training, activities, workouts and weight.
Garmin watch data for AI assistants: activities, sleep, HRV, recovery. Never writes to Garmin.
Access COROS activity, training and health data, and manage training plans and workouts.
Garmin data in Claude & ChatGPT via the Garmin Health API. OAuth sign-in, no password sharing.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceProvides 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 npmMIT
- AlicenseBqualityCmaintenanceEnables 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.172MIT
- AlicenseBqualityDmaintenanceConnects 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.138MIT
- FlicenseNot gradedqualityCmaintenanceEnables read-only access to Garmin Connect data through OAuth, including activities, sleep, HRV, body battery, training status, and scheduled TrainingPeaks workouts.-