garmin-mcp
This is a local Garmin MCP server for reading training/recovery data, managing structured workouts, and supporting local training plans and data operations.
Verify saved Garmin tokens authenticate.
Preview structured workouts without changing Garmin Connect; create and optionally schedule them after explicit confirmation.
List workout templates and scheduled calendar workouts; delete workouts or unschedule entries with confirmation.
List completed activities and fetch compact summaries plus lap/interval splits.
Read available sleep, HRV, Body Battery, Training Readiness, and Training Status for a date.
Use the local training planner to create/edit/apply cycles and drafts, record execution/feedback/matches/reviews, and compare history.
Request Garmin refreshes, compute versioned features, browse saved data via lookup UI/SQLite/Swagger, and create/list/restore verified backups.
All Garmin writes require confirm=true; local plan edits save directly.
Provides tools for creating and scheduling structured workouts in Garmin Connect, managing the workout library and calendar, and reading activities, sleep, HRV, Body Battery, Training Readiness, and Training Status.
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., "@garmin-mcp preview a 6x800m running workout with pace targets"
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.
Personal Garmin MCP
A local Garmin training-data app with SQLite, immutable raw archives, request-driven features, a read-only lookup UI, Swagger, and MCP. It also creates and schedules structured workouts through unofficial Garmin endpoints.
This is an experimental personal project. Garmin may change the endpoints, rate-limit requests, or revoke a session without notice. Do not expose the server to the internet without adding separate authentication.
Important URLs and endpoints
Bookmark Garmin home for one starting page linking to the planner, saved data, database browser, and API documentation.
These links work after starting Docker Compose or the server in streamable-http
mode. They use the default local port, 8765; if you change GARMIN_MCP_PORT, use
that port in the URLs below.
Page or endpoint | Clickable URL | Purpose |
Home | All application pages and service links in one place. | |
Training planner | Create cycles, edit weekly plans, record execution, and review history. | |
Lookup UI | Browse saved activities and daily data. | |
SQLite browser | Browse all tables, filter rows, run read-only SQL, and export CSV/JSON with Datasette. | |
Swagger API docs | Inspect and try HTTP API requests. | |
MCP endpoint | Connection URL for a Streamable HTTP MCP client; not a web page. | |
OpenAPI schema | Download the HTTP API schema as JSON. | |
Local status | Read coverage, freshness, and errors. | |
Saved activities | Read cached activity records as JSON. | |
Saved days | Read cached daily records as JSON. | |
Feature catalog | List available feature definitions. | |
Backups | List complete local backups. |
The JSON links above use GET. For POST operations such as refresh,
recalculation, backup creation, restore, and Garmin actions, use
Swagger to supply the required parameters and body.
Related MCP server: Garmin Connect MCP
Features
stores separate activity/day records, typed features, provenance, and freshness;
refreshes only on request, with full pagination, recent overlap, and explicit failures;
computes requested versioned Python features from archived objects;
provides an editable training planner plus the read-only lookup UI and Swagger alongside MCP;
makes daily local online SQLite backups and verifies requested restores;
verifies a saved Garmin session;
previews Garmin JSON without making changes;
creates workouts and optionally adds them to the calendar;
lists the workout library and calendar;
reads completed activities, compact summaries, and lap/split metrics;
reads available sleep, HRV, Body Battery, Training Readiness, and Training Status;
deletes workouts and removes workouts from the calendar;
supports running, cycling, walking, and hiking;
supports time, distance, and
lap buttonsteps;supports interval repeats;
supports pace, heart-rate, power, and cadence targets;
requires
confirm=truefor every Garmin write; local plan edits save directly.
Training plans
The daily-use first release of the planning specification is implemented at Training planner. Create a cycle with a main event, goal, timezone, and availability. Weeks begin as outlines; add sessions, explicit rest, structured workout steps, or alternatives as needed. The planner supports additional events, training phases, travel constraints, manual execution, feedback, and links to imported Garmin activities.
Saving a plan creates an immutable full snapshot. Drafts remain proposals until applied. The history view compares before/after values and records the reason. Moving a session preserves its ID; cancellation preserves the assignment. Restoring an earlier version makes a new draft and retains later sessions as cancelled, extending the date envelope if necessary. Recorded execution keeps its original assignment even when the current plan changes.
Only one cycle can be active per configured account. Completed and cancelled cycles remain readable; create a new cycle for the next goal. Dates are local calendar dates with stored IANA timezones, not device-timezone conversions.
Weekly totals count each imported activity once. Only confirmed matches contribute to session comparisons. If one activity is shared, explicitly allocate both seconds and metres to every linked session; unresolved allocations produce unknown session metrics. Linked manual records are not added again to weekly totals. Choose manual or Garmin metrics in Record result; both edit history and comments are retained. A day without an activity remains unknown, not skipped. An outline week without sessions has an unknown scheduled total, not zero.
Coverage, source timestamps, missing health sources, and partial totals are visible. Activity corrections invalidate affected matches and reviews for explicit review; confirmed source metrics and capture references are retained in match history. Reading any planner view never imports data or calculates features. Refresh Garmin data is an explicit request to the existing activity-import service.
The first release also includes local candidate suggestions and saved review notes. Managed publication of plan sessions to Garmin is the specification's later stage, and is not implemented here. Existing workout preview/create/schedule tools remain available with their original explicit Garmin-write confirmation. Saving, restoring, or applying a plan never uses those tools.
Shared HTTP and MCP operations
Every operation below uses the same training.TrainingService. HTTP uses
/api/training; request models and error responses appear in
Swagger. MCP uses the listed names and the same parameters;
mutation payloads are passed as body.
HTTP (after | MCP |
GET / POST |
|
GET |
|
POST |
|
POST |
|
PUT |
|
POST |
|
POST |
|
GET |
|
GET |
|
GET |
|
PUT |
|
PUT |
|
PUT |
|
PUT |
|
GET |
|
Read the current snapshot before editing. Supply expected_revision_id for every
mutation of an existing cycle; draft editing/applying also needs
expected_draft_version. Operational records use expected_version=0 to create,
then the returned version to edit. Always supply author and reason. Create,
apply, discard, and lifecycle commands require an idempotency_key: replaying the
same request returns its previous result; changing content under that key returns
conflict. Reload after a 409 instead of overwriting another editor's work.
Snapshot payloads contain cycle, events, constraints, phases, weeks, and
sessions. Each collection item has a stable caller-chosen id. Target ranges
use distance_min_m / distance_max_m, duration_min_s / duration_max_s,
pace in seconds/km, elevation in metres, and heart rate in bpm. Unknowns are null.
A session structure has schema_version: 1, optional free-text instructions, and
an optional existing WorkoutSpec under workout. For alternatives, provide
variants and an explicit selected_variant_id. Do not sum alternatives.
Constraint weekday indices are Monday=0 through Sunday=6; null means unspecified,
while an empty list means no available days.
Constraints and weekly targets optionally specify sport; null keeps the existing
all-sports scope. New cycles in the UI default to running availability and targets.
Weekly reports include by_sport and all_sports_totals, with running and cycling
subtypes grouped together. The main totals follow the week's target sport; manual
facts retain the sport of their assessed assignment. Cycling distance never counts
toward a running target. The Replace action cancels an assignment and creates a
linked replacement with its own sport and targets; it does not change Garmin.
Warnings do not silently override availability or volume targets: applying a
snapshot with warnings requires override_explanation. Other invalid references,
dates, units, or transitions return validation_error (422), missing records return
not_found (404), concurrent edits return conflict (409), and unavailable evidence
returns missing_inputs (422). Incomplete context has status: partial with coverage
details; known_sum is explicitly not a complete weekly total.
The numbered migration adds planning tables to the existing SQLite database. Existing online backups include all plans, versions, execution, feedback, matches, reviews, idempotency receipts, and audit history. Restore never calls Garmin.
Run the complete fixture-only suite using the patched SQLite Docker runtime:
docker build --target test -t garmin-mcp-training-test .
docker run --rm --network none garmin-mcp-training-testInstallation
Docker Compose — recommended
Local use requires Docker with Compose support. Build the image from the repository root:
docker compose buildThe build installs dependencies strictly from uv.lock and compiles checksum-verified
SQLite 3.51.3, fixing the WAL-reset bug.
Run the interactive login once. The password and MFA code are entered directly in the
container and are not stored. OAuth tokens are saved in the private
garmin-mcp_garmin_tokens named volume:
docker compose --profile login run --rm garmin-loginStart the MCP server:
docker compose up -d garmin-mcp
docker compose up -d --build datasette
docker compose psOpen Garmin home or Swagger API docs. See Important URLs and endpoints for all quick links. To use a different local port:
GARMIN_MCP_PORT=8767 docker compose up -d garmin-mcpSQLite web browser
Datasette runs locally at http://127.0.0.1:8766/garmin. Open activities or days, use column filters, or choose View and edit SQL to run SELECT queries and joins. Results can be exported as CSV or JSON. This reads saved data and never requests Garmin downloads.
Start or rebuild it with docker compose up -d --build datasette; stop only the
browser with docker compose stop datasette. It restarts automatically with Docker.
Set GARMIN_DB_UI_PORT to change its port (choose a different port from the app).
Recreate the app service too after changing this port so the home page link updates:
docker compose up -d garmin-mcp datasette.
The browser shares GARMIN_DATA_DIR with the app through a read-only directory
mount, including SQLite's WAL/SHM files. It uses the same patched SQLite build
and reads the live database, without immutable mode or a copied snapshot.
The app keeps an idle SQLite connection open so WAL/SHM remain available to the
read-only browser between requests; it does not keep a read transaction open.
Reload a page after a refresh or recalculation to see newly committed records.
The app must initialize the database first; Compose waits for its health check.
The browser binds to 127.0.0.1 and cannot edit the database. It displays raw
stored columns: consult feature_results.status and source timestamps for
validity; the app's lookup UI additionally masks invalidated feature values.
Datasette dependencies are pinned with hashes in
deploy/datasette/requirements.txt. To update them after editing requirements.in:
uv pip compile deploy/datasette/requirements.in --python-version 3.12 \
--generate-hashes -o deploy/datasette/requirements.txt
docker compose up -d --build datasetteService operations
View logs or stop the server:
docker compose logs -f garmin-mcp
docker compose downdocker compose down preserves the Garmin tokens. Running docker compose down -v
deletes the volume and its tokens.
Data persists in the host directory ./.garmin-data (override with GARMIN_DATA_DIR).
Keep it on a local disk and run one app process per data directory. Startup initializes
the local schema; it does not log in to Garmin, import data, or calculate features.
Daily backups run at 03:00 Europe/Berlin, or on the next scheduler tick after that
time if the app was stopped. Configure GARMIN_BACKUP_TIME, GARMIN_TIMEZONE, or
GARMIN_BACKUP_ENABLED=false. Automatic backups exclude tokens. Local backups
protect against accidental changes; a second location is needed for host loss.
Requested data and features
The first refresh requires a start date:
curl -X POST http://127.0.0.1:8765/api/refresh/activities \
-H 'Content-Type: application/json' -d '{"start_date":"2026-10-01"}'
curl -X POST http://127.0.0.1:8765/api/refresh/days/2026-10-06 \
-H 'Content-Type: application/json' -d '{"sources":["sleep","hrv"]}'
curl -X POST http://127.0.0.1:8765/api/features/recalculate \
-H 'Content-Type: application/json' \
-d '{"names":["sleep_hours"],"entity":"days","ids":["2026-10-06"]}'Subsequent activity refreshes can use {} to continue from the successful cursor,
rechecking GARMIN_OVERLAP_DAYS (default 7). Pagination is bounded by 1,000 pages and
a 120-second budget checked between requests; in-flight calls have transport timeouts
and one bounded transient retry. Failed refreshes preserve completed pages without
advancing coverage. Retrying replays and deduplicates pages to tolerate moving offsets.
Explicit date ranges request older history. Detail/split and daily sources are fetched
only when named. dry_run=true previews work without downloads or calculations.
Feature inputs are never downloaded implicitly. Lookups report stale sources (24 hours
by default) and mask invalidated values. Corrections conservatively invalidate all
features for the account; recomputation is explicit. Built-in examples: pace and
sleep_hours. Feature and migration directories are mounted into Compose; new feature
modules load on request and new migrations apply on restart.
The agent operations skill documents paths, schema, Python features, direct SQL repairs, migrations, errors, and restore. Raw objects are immutable; repairs retain audit provenance.
Manual backup: POST /api/backups with {} or {"include_tokens":true}. List with
GET on the same route. Restore: POST /api/backups/{backup_id}/restore with
{"confirm":true}. Restore verifies checksums, integrity, references, account, and
migrations, and makes a safety backup. Saved code/configuration are recovery artifacts,
not automatically executed. Token restore is separately enabled with restore_tokens.
Local installation without Docker
Python 3.12+, uv, and patched SQLite are required (3.51.3+ or backports 3.50.7 / 3.44.6).
Older SQLite runtimes are rejected; Docker includes the fix.
cd /path/to/garmin-mcp
uv sync --extra devOne-time login
uv run garmin-mcp-loginThe script interactively requests an email address, password, and MFA code when required.
The password is not stored. OAuth tokens are saved to
.garmin-tokens/garmin_tokens.json with restricted permissions. The directory is already
included in .gitignore.
To store tokens somewhere else:
export GARMIN_TOKEN_DIR=/safe/private/path/garmin-tokens
uv run garmin-mcp-loginLocal MCP over stdio
export GARMIN_TOKEN_DIR=/safe/private/path/garmin-tokens
uv run garmin-mcpExample MCP client configuration:
{
"mcpServers": {
"garmin": {
"command": "/absolute/path/to/garmin-mcp/.venv/bin/garmin-mcp",
"env": {
"GARMIN_TOKEN_DIR": "/safe/private/path/garmin-tokens"
}
}
}
}Streamable HTTP for ChatGPT
export GARMIN_MCP_TRANSPORT=streamable-http
export GARMIN_MCP_HOST=127.0.0.1
export GARMIN_MCP_PORT=8765
export GARMIN_TOKEN_DIR=/safe/private/path/garmin-tokens
uv run garmin-mcpMCP connection URL: http://127.0.0.1:8765/mcp.
The app binds to loopback by default and validates Host/Origin. It has no public authentication layer. Remote access needs a separately designed authenticated connection; this repository does not open a public tunnel.
Example preview_workout argument
{
"workout": {
"name": "6 x 800",
"sport": "running",
"description": "Controlled intervals",
"blocks": [
{
"steps": [
{
"step_type": "warmup",
"duration_type": "time",
"duration_value": 900
}
]
},
{
"repeat": 6,
"steps": [
{
"step_type": "interval",
"duration_type": "distance",
"duration_value": 800,
"target_type": "pace_seconds_per_km",
"target_low": 250,
"target_high": 260
},
{
"step_type": "recovery",
"duration_type": "time",
"duration_value": 120
}
]
},
{
"steps": [
{
"step_type": "cooldown",
"duration_type": "time",
"duration_value": 600
}
]
}
]
}
}Pace values are specified in seconds per kilometre: 250 = 4:10/km and 260 =
4:20/km. The server converts them to the m/s values used by Garmin Connect.
After previewing, call create_workout with the same object, a date in YYYY-MM-DD
format, and confirm=true.
Read-only training context
The server exposes compact read-only tools for adapting future plans to completed work:
list_activitieslists activities in an inclusive date range, with an optional Garmin activity type filter;get_activity_summaryreturns planning metrics for one activity;get_activity_splitsreturns compact lap and interval metrics;get_recovery_statusaggregates available sleep, HRV, Body Battery, Training Readiness, and Training Status for one date.
Activity responses intentionally omit GPS coordinates and owner details. Recovery metrics vary by Garmin device, account, and date. A missing source is returned as unavailable and does not make the entire recovery request fail.
Checks
docker build --target test -t personal-garmin-mcp:test .
docker run --rm --network none personal-garmin-mcp:test
# Or, with patched host SQLite:
uv run --extra dev pytest
uv run --extra dev ruff check .Tests do not contact Garmin or require credentials. Coverage includes workout confirmation, pagination/retry, corrections, typed features, partial source failures, concurrency, HTTP schemas, and backup/restore.
Important limitations
This is not an official Garmin Training API.
Frequent logins may receive HTTP 429 responses; reuse saved tokens.
Tokens provide access to Garmin Connect and must be protected like a password.
If creation succeeds but scheduling fails, the tool returns
created_not_scheduledandworkout_id; the created workout remains in the library.Before regular use, test one simple workout in Garmin Connect and on the specific watch model.
Available Tools
11 toolsconnection_statusARead-onlyIdempotent
Verify that saved Garmin tokens can authenticate.
Inputs: no named parameters. Returns: object
create_workoutA
Create a Garmin workout and optionally schedule it on YYYY-MM-DD.
Call with confirm=false for a non-mutating confirmation response. Set confirm=true only after the user explicitly approves the preview and date.
Inputs: workout (required), confirm, schedule_date. Returns: object
delete_workoutADestructiveIdempotent
Permanently delete a workout template after explicit confirmation.
Inputs: workout_id (required), confirm. Returns: object
get_activity_splitsARead-onlyIdempotent
Get compact lap and interval metrics for one completed Garmin activity.
Inputs: activity_id (required). Returns: object
get_activity_summaryARead-onlyIdempotent
Get compact summary metrics for one completed Garmin activity.
Inputs: activity_id (required). Returns: object
get_recovery_statusARead-onlyIdempotent
Get available sleep, HRV, Body Battery, readiness, and training status for a date.
Inputs: date (required). Returns: object
list_activitiesARead-onlyIdempotent
List completed Garmin activities in an inclusive YYYY-MM-DD date range.
Returns compact planning metrics and omits GPS coordinates and owner details.
activity_type is an optional Garmin type key such as running or cycling.
Inputs: start_date (required), limit, end_date, activity_type. Returns: object
list_scheduled_workoutsBRead-onlyIdempotent
List Garmin calendar entries for a year and month.
Inputs: year (required), month (required). Returns: object
list_workoutsARead-onlyIdempotent
List workout templates from the user's Garmin Connect library.
Inputs: limit. Returns: result
preview_workoutARead-onlyIdempotent
Validate and preview Garmin JSON without changing Garmin Connect.
Inputs: workout (required). Returns: object
unschedule_workoutADestructiveIdempotent
Remove a scheduled workout from the calendar without deleting its template.
Inputs: scheduled_workout_id (required), confirm. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 11 tools
The tools are generally distinct: connection_status, preview_workout, list/workout types, get_activity_summary vs splits, recovery, and create/delete/unschedule. There is slight overlap between list_workouts and list_scheduled_workouts, but descriptions clarify that one is templates and the other is calendar entries. The naming also helps distinguish them.
All tools follow a consistent verb_noun pattern: connection_status, preview_workout, list_workouts, list_scheduled_workouts, list_activities, get_activity_summary, get_activity_splits, get_recovery_status, create_workout, delete_workout, unschedule_workout. The pattern is uniform and predictable.
With 11 tools, the server covers a reasonable scope for a Garmin integration: connection check, workout preview/CRUD, activity listing/retrieval, and recovery status. Each tool serves a distinct function, and the count is within the typical range for a domain-specific server.
The surface appears complete for common Garmin workflows: managing workouts (create, delete, unschedule), viewing activities (list, summary, splits), checking recovery, and verifying connection. It includes both list and get operations, plus scheduling. There is a minor gap for updating workouts, but that may be acceptable given the preview/confirm workflow. Overall, it covers the primary user needs.
Maintenance
Related MCP Connectors
Remote MCP server for training, nutrition, wellness, and performance data with OAuth 2.0.
List, fetch, create, edit (replace), delete and schedule structured workouts on Garmin Connect (runn
MCP server for Withings health data — sleep, activity, heart, and body metrics.
Fitness MCP for coaches and gyms: build workouts and read clients' training from live Superset data.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenancePersonal MCP server for interacting with your Garmin Connect data. Exposes 62 tools across 11 domains including activities, health, training, and workouts.1MIT
- AlicenseBqualityDmaintenanceExposes personal Garmin Connect data to MCP-capable clients like Claude and Gemini. Enables querying daily stats, heart rate, sleep, activities, and managing workouts.16MIT
- AlicenseAqualityDmaintenanceMCP server for reading and querying Garmin Connect data, including activities, strength history, recovery, trends, and optionally creating workouts.12MIT
- FlicenseAqualityDmaintenanceProvides read-only access to your Garmin Connect health data, including sleep, HRV, body battery, stress, training readiness, and activities, through an MCP server.214-