Skip to main content
Glama
README.md
# Personal Garmin MCP

A local MCP server for creating and scheduling structured workouts in Garmin Connect
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.

## Features

- 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 button` steps;
- supports interval repeats;
- supports pace, heart-rate, power, and cadence targets;
- requires `confirm=true` for every change.

## Installation

### Docker Compose — recommended

Local use requires Docker with Compose support. Build the image from the repository root:

```bash
docker compose build
```

The build installs Python dependencies with `uv` strictly from `uv.lock`.

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:

```bash
docker compose --profile login run --rm garmin-login
```

Start the MCP server:

```bash
docker compose up -d garmin-mcp
docker compose ps
```

The endpoint is available at `http://127.0.0.1:8000/mcp`. To use a different local port:

```bash
GARMIN_MCP_PORT=8765 docker compose up -d garmin-mcp
```

View logs or stop the server:

```bash
docker compose logs -f garmin-mcp
docker compose down
```

`docker compose down` preserves the Garmin tokens. Running `docker compose down -v`
deletes the volume and its tokens.

### Local installation without Docker

Python 3.12+ and `uv` are required.

```bash
cd /path/to/garmin-mcp
uv sync --extra dev
```

## One-time login

```bash
uv run garmin-mcp-login
```

The 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:

```bash
export GARMIN_TOKEN_DIR=/safe/private/path/garmin-tokens
uv run garmin-mcp-login
```

## Local MCP over stdio

```bash
export GARMIN_TOKEN_DIR=/safe/private/path/garmin-tokens
uv run garmin-mcp
```

Example MCP client configuration:

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

```bash
export GARMIN_MCP_TRANSPORT=streamable-http
export GARMIN_MCP_HOST=127.0.0.1
export GARMIN_MCP_PORT=8000
export GARMIN_TOKEN_DIR=/safe/private/path/garmin-tokens
uv run garmin-mcp
```

Endpoint: `http://127.0.0.1:8000/mcp`.

ChatGPT must be able to reach the endpoint over HTTPS. A protected tunnel can be used for
personal testing. Do not expose this MVP directly to the internet: it intentionally does
not include a separate OAuth resource server for access to the MCP server itself.

## Example `preview_workout` argument

```json
{
  "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_activities` lists activities in an inclusive date range, with an optional Garmin
  activity type filter;
- `get_activity_summary` returns planning metrics for one activity;
- `get_activity_splits` returns compact lap and interval metrics;
- `get_recovery_status` aggregates 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

```bash
uv run --extra dev pytest
uv run --extra dev ruff check .
```

Unit tests do not contact Garmin and do not require credentials.

## 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_scheduled` and
  `workout_id`; the created workout remains in the library.
- Before regular use, test one simple workout in Garmin Connect and on the specific watch
  model.

TDQS

A4/5.0

Scored across 11 tools

Disambiguation4/5

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.

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness5/5

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

ActivitySlowing
ResponsivenessNo issues