Garmin Open MCP
# Garmin Open MCP
[](https://github.com/limited/garmin-connect-mcp/actions/workflows/ci.yml)
An unofficial, free, self-hosted bridge from Garmin Connect to MCP clients and ChatGPT mobile.
> **Not affiliated with or endorsed by Garmin.** This project uses undocumented Garmin Connect consumer endpoints. They can change or stop working without notice.
## Features
- Interactive Garmin login with MFA through `python-garminconnect`
- Garmin credentials are discarded after login; session tokens are stored with private filesystem permissions
- Local SQLite cache for health summaries and activities
- 11 focused MCP tools over Streamable HTTP or stdio
- REST/OpenAPI adapter for ChatGPT GPT Actions on iOS and Android
- Static single-user API key protecting remote endpoints
- Docker, systemd, and native Python deployment options
## Quick start
Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/).
```bash
git clone <repository-url> garmin-open-mcp
cd garmin-open-mcp
uv sync
uv run garmin-open-mcp init
uv run garmin-open-mcp login
uv run garmin-open-mcp sync --days 7
uv run garmin-open-mcp serve
```
Open `http://localhost:8000`.
Private data defaults to:
```text
~/.local/share/garmin-open-mcp/
├── api-key
├── garmin.sqlite3
└── tokens/
```
Override it with `GARMIN_MCP_DATA_DIR`.
## CLI
```text
garmin-open-mcp init
garmin-open-mcp login
garmin-open-mcp logout
garmin-open-mcp status --verify
garmin-open-mcp doctor
garmin-open-mcp sync --days 7
garmin-open-mcp show-api-key
garmin-open-mcp serve
garmin-open-mcp stdio
```
The login command accepts the Garmin password only in the local terminal. The password is not written to config, logs, or SQLite. Garmin session token files are set to mode `0600` where the platform supports it.
## MCP
### Local stdio
```json
{
"mcpServers": {
"garmin": {
"command": "uv",
"args": [
"--directory",
"/absolute/path/to/garmin-open-mcp",
"run",
"garmin-open-mcp",
"stdio"
]
}
}
}
```
### Remote Streamable HTTP
Endpoint:
```text
https://your-server.example/mcp
```
Send the key printed by `garmin-open-mcp show-api-key` as either:
```text
Authorization: Bearer YOUR_KEY
```
or:
```text
X-API-Key: YOUR_KEY
```
The initial release uses a static single-user bearer key, not a full OAuth authorization server. MCP clients that cannot supply custom authorization headers should use stdio or the REST/GPT Actions adapter.
## ChatGPT mobile setup
Custom MCP servers are not currently available directly in the ChatGPT mobile app. The included OpenAPI adapter exposes the same service to a private custom GPT:
1. Deploy this server at a stable HTTPS URL.
2. On ChatGPT web, create a GPT.
3. Add an Action and import `https://your-server.example/openapi.json`.
4. Select **API key** authentication.
5. Configure a custom header named `X-API-Key`.
6. Enter the value from `garmin-open-mcp show-api-key`.
7. Use `https://your-server.example/privacy` as the privacy-policy URL.
8. Keep the GPT private, save it, then open it from ChatGPT on iOS or Android.
Suggested GPT instructions are in [`GPT_INSTRUCTIONS.md`](GPT_INSTRUCTIONS.md).
## REST API
Interactive docs are at `/docs`. Useful endpoints include:
```text
GET /api/v1/status
POST /api/v1/sync
GET /api/v1/day/{day}
GET /api/v1/sleep/{day}
GET /api/v1/activities
GET /api/v1/activities/{activity_id}
GET /api/v1/training/{day}
GET /api/v1/body
GET /api/v1/trends
GET /api/v1/compare
GET /api/v1/recent
```
All `/api/v1` endpoints require `X-API-Key`.
## MCP tools
- `garmin_connection_status`
- `garmin_sync`
- `garmin_get_day`
- `garmin_get_sleep`
- `garmin_list_activities`
- `garmin_get_activity`
- `garmin_get_training_status`
- `garmin_get_body_metrics`
- `garmin_get_trends`
- `garmin_compare_periods`
- `garmin_recent_overview`
The MVP is read-only with respect to Garmin. `garmin_sync` writes only to the local cache.
## Configuration
| Environment variable | Default | Purpose |
| --- | --- | --- |
| `GARMIN_MCP_DATA_DIR` | `~/.local/share/garmin-open-mcp` | Private data and token directory |
| `GARMIN_MCP_HOST` | `0.0.0.0` | Bind address |
| `GARMIN_MCP_PORT` | `8000` | HTTP port |
| `GARMIN_MCP_PUBLIC_URL` | unset | Public HTTPS URL and MCP Host allowlist |
| `GARMIN_MCP_API_KEY` | generated file | Override the single-user API key |
| `GARMIN_MCP_MAX_SYNC_DAYS` | `31` | Maximum days per sync |
| `GARMIN_MCP_LOG_LEVEL` | `INFO` | Log level |
Set `GARMIN_MCP_PUBLIC_URL` when deploying remotely. MCP DNS-rebinding protection allows the hostname from this URL plus local development hosts.
## Docker
Login once using a mounted data directory, then start the service:
```bash
docker compose run --rm garmin-open-mcp login
docker compose run --rm garmin-open-mcp sync --days 7
docker compose up -d
```
## Development
```bash
uv sync --dev
uv run ruff check .
uv run ruff format --check .
uv run pytest
```
Tests use fake Garmin responses and never contact Garmin.
## Troubleshooting
### Login prints `mobile+cffi returned 429`
`python-garminconnect` tries several login strategies. Garmin may rate-limit one or more strategies by IP address while a later widget or portal strategy still succeeds. If the command ends with `Connected:` and `garmin-open-mcp status --verify` reports `connected: true`, the saved session is valid.
Do not repeatedly run `login` after success. Normal server requests load and refresh the saved token session instead of performing another password login. If every strategy is rate-limited and no token is saved, stop retrying and wait for Garmin's cooldown.
## Security model
- This is designed as a single-user service.
- Do not publish your API key or commit the data directory.
- Do not make a GPT containing your API key public.
- Put the service behind HTTPS.
- Back up the SQLite database only if you are comfortable storing a copy of health data.
- Logout deletes Garmin session tokens but leaves cached health data. Delete the data directory to erase everything.
See [`SECURITY.md`](SECURITY.md) for reporting vulnerabilities.
## License
MIT. See [`LICENSE`](LICENSE).
TDQS
Scored across 11 tools
Each tool targets a distinct resource or operation: connection status, sync, day summary, sleep, activities list/get, training status, body metrics, trends, period comparison, and recent overview. Even the cached-data tools are clearly separated by their analytical purpose.
All tools follow a consistent garmin_ prefix with a verb_noun structure (check, sync, get, list). The naming pattern is uniform and predictable, making it easy to infer what each tool does.
At 11 tools, the server is well-scoped for a health-data integration. It covers core data access, activity management, analytics, and connection handling without unnecessary bloat or gaps.
The surface covers major Garmin data domains: daily summary, sleep, activities, body metrics, training status, and cached data analysis. Minor gaps exist (e.g., no explicit hydration or intraday detail endpoints), but the core workflows are complete.