30 KiB
MCP Garmin Server — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a single-file FastMCP stdio server exposing 15 Garmin Connect tools to Claude Desktop, with interactive MFA handling from within chat.
Architecture: server.py holds all tools and a module-level Garmin client shared across calls. Auth state is a global string flag. MFA is handled by running login in a background thread that blocks on a queue.Queue; complete_mfa() unblocks it. Startup attempts a silent login using cached ~/.garth tokens, falling back gracefully if none exist.
Tech Stack: Python 3.14, mcp[cli] (FastMCP), garminconnect, pytest, unittest.mock
File Map
| File | Purpose |
|---|---|
pyproject.toml |
Project metadata and pip dependencies |
.env.example |
Template for required env vars (committed) |
.gitignore |
Excludes .env, .garth/, .venv/ |
server.py |
All 15 MCP tools + auth state + startup logic |
tests/__init__.py |
Empty — makes tests/ a package |
tests/test_server.py |
Unit tests with mocked Garmin client |
Task 1: Project Scaffolding
Files:
-
Create:
pyproject.toml -
Create:
.env.example -
Create:
.gitignore -
Step 1: Create
pyproject.toml
[project]
name = "mcp-garmin"
version = "0.1.0"
description = "MCP server for Garmin Connect"
requires-python = ">=3.11"
dependencies = [
"mcp[cli]",
"garminconnect",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
- Step 2: Create
.env.example
GARMIN_EMAIL=your@email.com
GARMIN_PASSWORD=yourpassword
- Step 3: Create
.gitignore
.env
.garth/
.venv/
__pycache__/
*.pyc
.idea/
dist/
- Step 4: Install dependencies
.venv/bin/pip install "mcp[cli]" garminconnect pytest
Expected: All packages install without errors. Verify with:
.venv/bin/python -c "from garminconnect import Garmin; from mcp.server.fastmcp import FastMCP; print('OK')"
Expected output: OK
- Step 5: Initialize git and commit
git init
git add pyproject.toml .env.example .gitignore docs/
git commit -m "chore: project scaffolding and design docs"
Task 2: Server Skeleton + Auth Tools
Files:
-
Create:
server.py -
Create:
tests/__init__.py -
Create:
tests/test_server.py -
Step 1: Write failing tests for auth tools
Create tests/__init__.py as an empty file, then create tests/test_server.py:
import json
import queue
import threading
from unittest.mock import MagicMock, patch
import pytest
import server
@pytest.fixture(autouse=True)
def reset_state():
original_state = server._auth_state
original_client = server._client
for q in (server._mfa_input_queue, server._login_result_queue):
while not q.empty():
try:
q.get_nowait()
except queue.Empty:
break
yield
server._auth_state = original_state
server._client = original_client
def test_check_auth_unauthenticated():
server._auth_state = "unauthenticated"
assert server._check_auth() == "Not authenticated. Ask Claude to call authenticate() first."
def test_check_auth_authenticated():
server._auth_state = "authenticated"
assert server._check_auth() is None
def test_authenticate_success():
with patch("server.Garmin") as mock_garmin_cls:
mock_garmin_cls.return_value = MagicMock()
# mock login() returns immediately, thread puts success in queue
result = server.authenticate()
assert result == "Authenticated successfully."
assert server._auth_state == "authenticated"
def test_authenticate_mfa_required():
with patch("server.Garmin") as mock_garmin_cls:
mock_garmin_cls.return_value = MagicMock()
with patch.object(server._login_result_queue, "get", side_effect=queue.Empty):
result = server.authenticate()
assert "MFA required" in result
assert server._auth_state == "mfa_pending"
def test_complete_mfa_success():
server._auth_state = "mfa_pending"
server._login_result_queue.put(("success", None))
result = server.complete_mfa("123456")
assert result == "MFA accepted. Authenticated successfully."
assert server._auth_state == "authenticated"
assert server._mfa_input_queue.get_nowait() == "123456"
def test_complete_mfa_not_in_progress():
server._auth_state = "unauthenticated"
result = server.complete_mfa("123456")
assert result == "No MFA in progress. Call authenticate() first."
- Step 2: Run tests to verify they fail
.venv/bin/pytest tests/test_server.py -v 2>&1 | head -20
Expected: ModuleNotFoundError: No module named 'server' — server.py doesn't exist yet.
- Step 3: Create
server.py
import json
import os
import queue
import threading
from datetime import date
from garminconnect import Garmin, GarminConnectAuthenticationError
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("garmin")
_client: Garmin | None = None
_auth_state: str = "unauthenticated"
_mfa_input_queue: queue.Queue = queue.Queue()
_login_result_queue: queue.Queue = queue.Queue()
def _today() -> str:
return date.today().isoformat()
def _check_auth() -> str | None:
if _auth_state != "authenticated":
return "Not authenticated. Ask Claude to call authenticate() first."
return None
def _prompt_mfa() -> str:
return _mfa_input_queue.get(timeout=300)
def _startup_login() -> None:
global _client, _auth_state
email = os.environ.get("GARMIN_EMAIL")
password = os.environ.get("GARMIN_PASSWORD")
if not email or not password:
raise SystemExit(
"GARMIN_EMAIL and GARMIN_PASSWORD environment variables are required."
)
try:
_client = Garmin(email, password)
_client.login()
_auth_state = "authenticated"
except Exception:
_auth_state = "unauthenticated"
@mcp.tool()
def authenticate() -> str:
"""Initiate Garmin authentication using credentials from environment variables."""
global _client, _auth_state
def _do_login() -> None:
try:
_client.login()
_login_result_queue.put(("success", None))
except Exception as exc:
_login_result_queue.put(("error", str(exc)))
_client = Garmin(os.environ["GARMIN_EMAIL"], os.environ["GARMIN_PASSWORD"])
_client.prompt_mfa = _prompt_mfa
threading.Thread(target=_do_login, daemon=True).start()
try:
status, err = _login_result_queue.get(timeout=10)
if status == "success":
_auth_state = "authenticated"
return "Authenticated successfully."
return f"Authentication failed: {err}"
except queue.Empty:
_auth_state = "mfa_pending"
return (
"MFA required. Garmin has sent a verification code to your registered "
"email or phone. Call complete_mfa(code) with the code you received."
)
@mcp.tool()
def complete_mfa(code: str) -> str:
"""Provide the MFA verification code to complete Garmin authentication."""
global _auth_state
if _auth_state != "mfa_pending":
return "No MFA in progress. Call authenticate() first."
_mfa_input_queue.put(code)
try:
status, err = _login_result_queue.get(timeout=30)
if status == "success":
_auth_state = "authenticated"
return "MFA accepted. Authenticated successfully."
return f"Authentication failed after MFA: {err}"
except queue.Empty:
return "Timed out waiting for authentication to complete."
if __name__ == "__main__":
_startup_login()
mcp.run()
- Step 4: Run tests to verify they pass
.venv/bin/pytest tests/test_server.py -v
Expected: All 6 tests pass. Example output:
test_server.py::test_check_auth_unauthenticated PASSED
test_server.py::test_check_auth_authenticated PASSED
test_server.py::test_authenticate_success PASSED
test_server.py::test_authenticate_mfa_required PASSED
test_server.py::test_complete_mfa_success PASSED
test_server.py::test_complete_mfa_not_in_progress PASSED
- Step 5: Commit
git add server.py tests/__init__.py tests/test_server.py
git commit -m "feat: server skeleton with authenticate and complete_mfa tools"
Task 3: Activity Tools
Files:
-
Modify:
server.py(addget_activities,get_activity_details) -
Modify:
tests/test_server.py(add activity tests) -
Step 1: Write failing tests for activity tools
Add the following imports to the top import block of tests/test_server.py (after import pytest):
from datetime import date
from garminconnect import GarminConnectAuthenticationError
Then append to the bottom of tests/test_server.py:
def test_get_activities_unauthenticated():
server._auth_state = "unauthenticated"
assert "Not authenticated" in server.get_activities()
def test_get_activities_success():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_activities_by_date.return_value = [
{"activityId": "111", "activityType": {"typeKey": "running"}, "distance": 5000.0},
{"activityId": "222", "activityType": {"typeKey": "cycling"}, "distance": 20000.0},
]
result = server.get_activities("2026-05-01", "2026-05-17", 20)
data = json.loads(result)
assert len(data) == 2
assert data[0]["activityId"] == "111"
server._client.get_activities_by_date.assert_called_once_with("2026-05-01", "2026-05-17")
def test_get_activities_limit():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_activities_by_date.return_value = [{"activityId": str(i)} for i in range(10)]
data = json.loads(server.get_activities("2026-05-01", "2026-05-17", 3))
assert len(data) == 3
def test_get_activities_defaults_to_today():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_activities_by_date.return_value = []
server.get_activities()
today = date.today().isoformat()
server._client.get_activities_by_date.assert_called_once_with(today, today)
def test_get_activities_auth_error():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_activities_by_date.side_effect = GarminConnectAuthenticationError("auth")
assert "Authentication error" in server.get_activities()
def test_get_activity_details_success():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_activity_details.return_value = {"activityId": "111", "laps": []}
data = json.loads(server.get_activity_details("111"))
assert data["activityId"] == "111"
server._client.get_activity_details.assert_called_once_with("111")
def test_get_activity_details_error():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_activity_details.side_effect = Exception("not found")
assert "Error fetching activity details" in server.get_activity_details("999")
- Step 2: Run tests to verify new ones fail
.venv/bin/pytest tests/test_server.py::test_get_activities_success -v
Expected: AttributeError: module 'server' has no attribute 'get_activities'
- Step 3: Add activity tools to
server.py
Add the following before the if __name__ == "__main__": block:
@mcp.tool()
def get_activities(start_date: str = "", end_date: str = "", limit: int = 20) -> str:
"""List Garmin activities between two dates (YYYY-MM-DD). Defaults to today."""
if err := _check_auth():
return err
start = start_date or _today()
end = end_date or _today()
try:
result = _client.get_activities_by_date(start, end)
return json.dumps(result[:limit], indent=2)
except GarminConnectAuthenticationError:
return "Authentication error. Call authenticate() again."
except Exception as exc:
return f"Error fetching activities: {exc}"
@mcp.tool()
def get_activity_details(activity_id: str) -> str:
"""Get full details for a single activity by its ID (splits, laps, metrics)."""
if err := _check_auth():
return err
try:
result = _client.get_activity_details(activity_id)
return json.dumps(result, indent=2)
except GarminConnectAuthenticationError:
return "Authentication error. Call authenticate() again."
except Exception as exc:
return f"Error fetching activity details: {exc}"
- Step 4: Run all tests to verify they pass
.venv/bin/pytest tests/test_server.py -v
Expected: All 13 tests pass.
- Step 5: Commit
git add server.py tests/test_server.py
git commit -m "feat: add get_activities and get_activity_details tools"
Task 4: Health Stats Tools
Files:
-
Modify:
server.py(add 6 health tools) -
Modify:
tests/test_server.py(add health tests) -
Step 1: Write failing tests for health tools
Append to the bottom of tests/test_server.py:
def test_get_sleep_success():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_sleep_data.return_value = {
"dailySleepDTO": {"sleepScores": {"overall": {"value": 85}}}
}
data = json.loads(server.get_sleep("2026-05-17"))
assert data["dailySleepDTO"]["sleepScores"]["overall"]["value"] == 85
server._client.get_sleep_data.assert_called_once_with("2026-05-17")
def test_get_sleep_no_data():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_sleep_data.return_value = None
assert server.get_sleep("2026-05-17") == "No sleep data found for 2026-05-17"
def test_get_heart_rate_success():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_heart_rates.return_value = {"restingHeartRate": 52}
data = json.loads(server.get_heart_rate("2026-05-17"))
assert data["restingHeartRate"] == 52
server._client.get_heart_rates.assert_called_once_with("2026-05-17")
def test_get_stress_success():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_stress_data.return_value = {"stressValuesArray": [[1000, 35]]}
data = json.loads(server.get_stress("2026-05-17"))
assert "stressValuesArray" in data
def test_get_body_battery_success():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_body_battery.return_value = [{"bodyBatteryStatList": []}]
data = json.loads(server.get_body_battery("2026-05-17"))
assert isinstance(data, list)
server._client.get_body_battery.assert_called_once_with("2026-05-17", "2026-05-17")
def test_get_hrv_success():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_hrv_data.return_value = {"hrvSummary": {"weeklyAvg": 45}}
data = json.loads(server.get_hrv("2026-05-17"))
assert data["hrvSummary"]["weeklyAvg"] == 45
def test_get_spo2_success():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_spo2_data.return_value = {"spO2HourlyAverages": []}
data = json.loads(server.get_spo2("2026-05-17"))
assert "spO2HourlyAverages" in data
def test_health_tools_unauthenticated():
server._auth_state = "unauthenticated"
for fn in (server.get_sleep, server.get_heart_rate, server.get_stress,
server.get_body_battery, server.get_hrv, server.get_spo2):
assert "Not authenticated" in fn("2026-05-17")
- Step 2: Run tests to verify new ones fail
.venv/bin/pytest tests/test_server.py::test_get_sleep_success -v
Expected: AttributeError: module 'server' has no attribute 'get_sleep'
- Step 3: Add health tools to
server.py
Add the following before the if __name__ == "__main__": block:
@mcp.tool()
def get_sleep(date: str = "") -> str:
"""Get sleep data for a date (YYYY-MM-DD): score, stages, duration. Defaults to today."""
if err := _check_auth():
return err
cdate = date or _today()
try:
result = _client.get_sleep_data(cdate)
if not result:
return f"No sleep data found for {cdate}"
return json.dumps(result, indent=2)
except GarminConnectAuthenticationError:
return "Authentication error. Call authenticate() again."
except Exception as exc:
return f"Error fetching sleep data: {exc}"
@mcp.tool()
def get_heart_rate(date: str = "") -> str:
"""Get heart rate data for a date (YYYY-MM-DD): resting HR and daily timeline. Defaults to today."""
if err := _check_auth():
return err
cdate = date or _today()
try:
result = _client.get_heart_rates(cdate)
if not result:
return f"No heart rate data found for {cdate}"
return json.dumps(result, indent=2)
except GarminConnectAuthenticationError:
return "Authentication error. Call authenticate() again."
except Exception as exc:
return f"Error fetching heart rate data: {exc}"
@mcp.tool()
def get_stress(date: str = "") -> str:
"""Get stress level timeline for a date (YYYY-MM-DD). Defaults to today."""
if err := _check_auth():
return err
cdate = date or _today()
try:
result = _client.get_stress_data(cdate)
if not result:
return f"No stress data found for {cdate}"
return json.dumps(result, indent=2)
except GarminConnectAuthenticationError:
return "Authentication error. Call authenticate() again."
except Exception as exc:
return f"Error fetching stress data: {exc}"
@mcp.tool()
def get_body_battery(date: str = "") -> str:
"""Get body battery charge/drain values for a date (YYYY-MM-DD). Defaults to today."""
if err := _check_auth():
return err
cdate = date or _today()
try:
result = _client.get_body_battery(cdate, cdate)
if not result:
return f"No body battery data found for {cdate}"
return json.dumps(result, indent=2)
except GarminConnectAuthenticationError:
return "Authentication error. Call authenticate() again."
except Exception as exc:
return f"Error fetching body battery data: {exc}"
@mcp.tool()
def get_hrv(date: str = "") -> str:
"""Get HRV status and nightly average for a date (YYYY-MM-DD). Defaults to today."""
if err := _check_auth():
return err
cdate = date or _today()
try:
result = _client.get_hrv_data(cdate)
if not result:
return f"No HRV data found for {cdate}"
return json.dumps(result, indent=2)
except GarminConnectAuthenticationError:
return "Authentication error. Call authenticate() again."
except Exception as exc:
return f"Error fetching HRV data: {exc}"
@mcp.tool()
def get_spo2(date: str = "") -> str:
"""Get SpO2 (blood oxygen) readings for a date (YYYY-MM-DD). Defaults to today."""
if err := _check_auth():
return err
cdate = date or _today()
try:
result = _client.get_spo2_data(cdate)
if not result:
return f"No SpO2 data found for {cdate}"
return json.dumps(result, indent=2)
except GarminConnectAuthenticationError:
return "Authentication error. Call authenticate() again."
except Exception as exc:
return f"Error fetching SpO2 data: {exc}"
- Step 4: Run all tests to verify they pass
.venv/bin/pytest tests/test_server.py -v
Expected: All 21 tests pass.
- Step 5: Commit
git add server.py tests/test_server.py
git commit -m "feat: add health stats tools (sleep, HR, stress, body battery, HRV, SpO2)"
Task 5: Steps & Daily Metrics Tools
Files:
-
Modify:
server.py(addget_steps,get_daily_stats) -
Modify:
tests/test_server.py(add steps/metrics tests) -
Step 1: Write failing tests
Append to the bottom of tests/test_server.py:
def test_get_steps_success():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_steps_data.return_value = [{"steps": 8342, "primaryActivityLevel": "active"}]
data = json.loads(server.get_steps("2026-05-17"))
assert data[0]["steps"] == 8342
server._client.get_steps_data.assert_called_once_with("2026-05-17")
def test_get_steps_no_data():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_steps_data.return_value = []
assert server.get_steps("2026-05-17") == "No steps data found for 2026-05-17"
def test_get_daily_stats_success():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_stats.return_value = {
"totalSteps": 9231,
"totalKilocalories": 2100.0,
"floorsAscended": 5,
}
data = json.loads(server.get_daily_stats("2026-05-17"))
assert data["totalSteps"] == 9231
server._client.get_stats.assert_called_once_with("2026-05-17")
def test_steps_and_stats_unauthenticated():
server._auth_state = "unauthenticated"
for fn in (server.get_steps, server.get_daily_stats):
assert "Not authenticated" in fn("2026-05-17")
- Step 2: Run tests to verify new ones fail
.venv/bin/pytest tests/test_server.py::test_get_steps_success -v
Expected: AttributeError: module 'server' has no attribute 'get_steps'
- Step 3: Add steps and daily stats tools to
server.py
Add the following before the if __name__ == "__main__": block:
@mcp.tool()
def get_steps(date: str = "") -> str:
"""Get step count and daily goal for a date (YYYY-MM-DD). Defaults to today."""
if err := _check_auth():
return err
cdate = date or _today()
try:
result = _client.get_steps_data(cdate)
if not result:
return f"No steps data found for {cdate}"
return json.dumps(result, indent=2)
except GarminConnectAuthenticationError:
return "Authentication error. Call authenticate() again."
except Exception as exc:
return f"Error fetching steps data: {exc}"
@mcp.tool()
def get_daily_stats(date: str = "") -> str:
"""Get daily summary for a date (YYYY-MM-DD): calories, floors, intensity minutes. Defaults to today."""
if err := _check_auth():
return err
cdate = date or _today()
try:
result = _client.get_stats(cdate)
if not result:
return f"No daily stats found for {cdate}"
return json.dumps(result, indent=2)
except GarminConnectAuthenticationError:
return "Authentication error. Call authenticate() again."
except Exception as exc:
return f"Error fetching daily stats: {exc}"
- Step 4: Run all tests to verify they pass
.venv/bin/pytest tests/test_server.py -v
Expected: All 25 tests pass.
- Step 5: Commit
git add server.py tests/test_server.py
git commit -m "feat: add get_steps and get_daily_stats tools"
Task 6: Gear, Devices & Profile Tools
Files:
-
Modify:
server.py(addget_devices,get_gear,get_user_profile) -
Modify:
tests/test_server.py(add gear/device/profile tests) -
Step 1: Write failing tests
Append to the bottom of tests/test_server.py:
def test_get_devices_success():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_devices.return_value = [
{"deviceId": "abc123", "productDisplayName": "Forerunner 965", "softwareVersion": "21.20"}
]
data = json.loads(server.get_devices())
assert data[0]["productDisplayName"] == "Forerunner 965"
server._client.get_devices.assert_called_once()
def test_get_devices_empty():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_devices.return_value = []
assert server.get_devices() == "No devices found."
def test_get_gear_success():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.display_name = "john.doe"
server._client.get_gear.return_value = [
{"gearPk": "shoe1", "customMakeModel": "Nike Pegasus", "totalDistance": 320000.0}
]
data = json.loads(server.get_gear())
assert data[0]["customMakeModel"] == "Nike Pegasus"
server._client.get_gear.assert_called_once_with("john.doe")
def test_get_user_profile_success():
server._auth_state = "authenticated"
server._client = MagicMock()
server._client.get_user_profile.return_value = {
"displayName": "john.doe",
"age": 35,
"weight": 75000.0,
}
data = json.loads(server.get_user_profile())
assert data["displayName"] == "john.doe"
server._client.get_user_profile.assert_called_once()
def test_gear_devices_profile_unauthenticated():
server._auth_state = "unauthenticated"
for fn in (server.get_devices, server.get_gear, server.get_user_profile):
assert "Not authenticated" in fn()
- Step 2: Run tests to verify new ones fail
.venv/bin/pytest tests/test_server.py::test_get_devices_success -v
Expected: AttributeError: module 'server' has no attribute 'get_devices'
- Step 3: Add gear, devices, and profile tools to
server.py
Add the following before the if __name__ == "__main__": block:
@mcp.tool()
def get_devices() -> str:
"""List paired Garmin devices with model name and firmware version."""
if err := _check_auth():
return err
try:
result = _client.get_devices()
if not result:
return "No devices found."
return json.dumps(result, indent=2)
except GarminConnectAuthenticationError:
return "Authentication error. Call authenticate() again."
except Exception as exc:
return f"Error fetching devices: {exc}"
@mcp.tool()
def get_gear() -> str:
"""List gear items (shoes, bikes) with mileage and usage stats."""
if err := _check_auth():
return err
try:
result = _client.get_gear(_client.display_name)
if not result:
return "No gear found."
return json.dumps(result, indent=2)
except GarminConnectAuthenticationError:
return "Authentication error. Call authenticate() again."
except Exception as exc:
return f"Error fetching gear: {exc}"
@mcp.tool()
def get_user_profile() -> str:
"""Get Garmin user profile: display name, age, weight, and HR zones."""
if err := _check_auth():
return err
try:
result = _client.get_user_profile()
if not result:
return "No user profile found."
return json.dumps(result, indent=2)
except GarminConnectAuthenticationError:
return "Authentication error. Call authenticate() again."
except Exception as exc:
return f"Error fetching user profile: {exc}"
- Step 4: Run all tests to verify they pass
.venv/bin/pytest tests/test_server.py -v
Expected: All 30 tests pass.
- Step 5: Commit
git add server.py tests/test_server.py
git commit -m "feat: add get_devices, get_gear, and get_user_profile tools"
Task 7: README & Claude Desktop Config
Files:
-
Create:
README.md -
Step 1: Create
README.md
# mcp-garmin
MCP server exposing Garmin Connect data to Claude Desktop.
## Setup
### 1. Install dependencies
```bash
python -m venv .venv
.venv/bin/pip install "mcp[cli]" garminconnect
2. Configure Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"garmin": {
"command": "/absolute/path/to/mcp-garmin/.venv/bin/python",
"args": ["/absolute/path/to/mcp-garmin/server.py"],
"env": {
"GARMIN_EMAIL": "your@email.com",
"GARMIN_PASSWORD": "yourpassword"
}
}
}
}
Replace /absolute/path/to/mcp-garmin with the actual path to this directory.
3. Restart Claude Desktop
Quit and relaunch Claude Desktop to pick up the new server config.
4. First-time authentication
On first use (no cached session in ~/.garth), ask Claude:
"Call authenticate()"
If Garmin requires MFA, a prompt will appear in the chat. Check your email or phone for the code, then:
"Call complete_mfa with code 123456"
Once authenticated, the session is cached in ~/.garth. Future server restarts will log in silently without MFA.
Available Tools
| Tool | Parameters | Description |
|---|---|---|
authenticate() |
— | Initiate Garmin login |
complete_mfa(code) |
code: str |
Provide MFA code to finish login |
get_activities(start_date, end_date, limit) |
all optional | List recent activities |
get_activity_details(activity_id) |
activity_id: str |
Full detail for one activity |
get_sleep(date) |
optional | Sleep score and stages |
get_heart_rate(date) |
optional | Resting HR and daily timeline |
get_stress(date) |
optional | Stress level timeline |
get_body_battery(date) |
optional | Body battery charge/drain |
get_hrv(date) |
optional | HRV status and nightly average |
get_spo2(date) |
optional | Blood oxygen readings |
get_steps(date) |
optional | Step count and daily goal |
get_daily_stats(date) |
optional | Calories, floors, intensity minutes |
get_devices() |
— | Paired Garmin devices |
get_gear() |
— | Gear items with mileage stats |
get_user_profile() |
— | Profile, age, weight, HR zones |
All date parameters accept YYYY-MM-DD format and default to today when omitted.
Running Tests
.venv/bin/pytest tests/ -v
- [ ] **Step 2: Run the full test suite one final time**
```bash
.venv/bin/pytest tests/test_server.py -v
Expected: All 30 tests pass. No warnings or errors.
- Step 3: Commit
git add README.md
git commit -m "docs: add README with setup and Claude Desktop config instructions"