127 lines
5.2 KiB
Markdown
127 lines
5.2 KiB
Markdown
# MCP Garmin Server — Design Spec
|
|
|
|
**Date:** 2026-05-17
|
|
**Status:** Approved
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
A local MCP server that exposes Garmin Connect data to Claude Desktop via the `garminconnect` Python package. The server runs as a single-file FastMCP stdio server, authenticated via environment variables, targeting personal use with Claude Desktop as the sole client.
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
**Approach:** Single-file FastMCP (Option A)
|
|
|
|
```
|
|
mcp-garmin/
|
|
├── server.py # FastMCP server — all tools and startup logic
|
|
├── pyproject.toml # Dependencies: mcp[cli], garminconnect
|
|
└── .env # GARMIN_EMAIL, GARMIN_PASSWORD (not committed)
|
|
```
|
|
|
|
`server.py` initializes a `FastMCP("garmin")` instance, registers all tools, and serves via stdio. A module-level `Garmin` client instance is shared across all tool calls — no re-authentication per call.
|
|
|
|
**Claude Desktop config** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"garmin": {
|
|
"command": "python",
|
|
"args": ["/path/to/mcp-garmin/server.py"],
|
|
"env": {
|
|
"GARMIN_EMAIL": "your@email.com",
|
|
"GARMIN_PASSWORD": "yourpassword"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Tools
|
|
|
|
All tools return JSON-serializable dicts. Dates are accepted as `YYYY-MM-DD` strings and default to today when omitted.
|
|
|
|
### Authentication
|
|
| Tool | Parameters | Description |
|
|
|------|-----------|-------------|
|
|
| `authenticate()` | — | Initiates login from env vars. Returns `"Authenticated"` on success, or the MFA prompt (URL or code instruction) if Garmin requires MFA. |
|
|
| `complete_mfa(code)` | `code: str` | Accepts the MFA code and completes login, caching the session in `~/.garth`. |
|
|
|
|
### Activities
|
|
| Tool | Parameters | Description |
|
|
|------|-----------|-------------|
|
|
| `get_activities(start_date, end_date, limit)` | `start_date: str`, `end_date: str`, `limit: int = 20` | List of activities with type, distance, duration, average HR. |
|
|
| `get_activity_details(activity_id)` | `activity_id: str` | Full detail for a single activity: splits, laps, all metrics. |
|
|
|
|
### Health Stats
|
|
| Tool | Parameters | Description |
|
|
|------|-----------|-------------|
|
|
| `get_sleep(date)` | `date: str = today` | Sleep score, stages (deep/light/REM/awake), total duration. |
|
|
| `get_heart_rate(date)` | `date: str = today` | Resting HR and daily HR timeline. |
|
|
| `get_stress(date)` | `date: str = today` | Stress level timeline throughout the day. |
|
|
| `get_body_battery(date)` | `date: str = today` | Body battery charge/drain values throughout the day. |
|
|
| `get_hrv(date)` | `date: str = today` | HRV status and nightly average. |
|
|
| `get_spo2(date)` | `date: str = today` | Blood oxygen readings. |
|
|
|
|
### Steps & Daily Metrics
|
|
| Tool | Parameters | Description |
|
|
|------|-----------|-------------|
|
|
| `get_steps(date)` | `date: str = today` | Step count, daily goal, and distance. |
|
|
| `get_daily_stats(date)` | `date: str = today` | Full daily summary: calories, floors, intensity minutes, active time. |
|
|
|
|
### Gear & Devices
|
|
| Tool | Parameters | Description |
|
|
|------|-----------|-------------|
|
|
| `get_devices()` | — | List of paired Garmin devices with model and firmware version. |
|
|
| `get_gear()` | — | Gear items (shoes, bikes) with mileage/usage stats. |
|
|
|
|
### Profile
|
|
| Tool | Parameters | Description |
|
|
|------|-----------|-------------|
|
|
| `get_user_profile()` | — | Display name, age, weight, HR zones. |
|
|
|
|
---
|
|
|
|
## Authentication & Session Management
|
|
|
|
- `garminconnect` uses `garth` under the hood, which caches OAuth tokens in `~/.garth` after first login.
|
|
- **Startup:** The server attempts a silent login using cached `~/.garth` tokens. If valid, all tools are ready immediately. If no cache exists, tools return `"Not authenticated. Ask Claude to call authenticate() first."` until auth is completed.
|
|
- **First-time setup:** The user calls `authenticate()` from Claude Desktop chat. If Garmin requires MFA, the prompt (URL or email code instruction) is returned as a string visible in the chat. The user then calls `complete_mfa(code)` with the received code. On success, garth caches the session — subsequent server restarts skip MFA entirely.
|
|
- **MFA URL handling:** If garth surfaces a URL as part of the MFA flow, it is returned verbatim in the `authenticate()` response so Claude Desktop renders it as a clickable link.
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
| Scenario | Behavior |
|
|
|----------|----------|
|
|
| Missing `GARMIN_EMAIL` or `GARMIN_PASSWORD` env vars | Fail at startup with a clear message; do not start server |
|
|
| No cached session and no auth attempted | Tools return `"Not authenticated. Call authenticate() first."` |
|
|
| `GarminConnectAuthenticationError` | Return readable error string to the LLM |
|
|
| No data for requested date | Return `"No <data type> found for <date>"` |
|
|
| Network error | Catch and return readable error string |
|
|
|
|
No custom retry logic — garth handles token refresh automatically.
|
|
|
|
---
|
|
|
|
## Dependencies
|
|
|
|
- `mcp[cli]` — FastMCP framework
|
|
- `garminconnect` — Garmin Connect API wrapper (uses `garth` for auth)
|
|
- Python 3.14 (existing venv)
|
|
|
|
---
|
|
|
|
## Out of Scope
|
|
|
|
- Write operations (creating workouts, updating data)
|
|
- Multi-user support
|
|
- Hosting / remote deployment
|
|
- GPS track/polyline data (raw GPX export)
|