5.2 KiB
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):
{
"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
garminconnectusesgarthunder the hood, which caches OAuth tokens in~/.garthafter first login.- Startup: The server attempts a silent login using cached
~/.garthtokens. 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 callscomplete_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 frameworkgarminconnect— Garmin Connect API wrapper (usesgarthfor 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)