# 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 found for "` | | 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)