Files
mcp-garmin/server.py
2026-05-17 19:46:45 +02:00

136 lines
4.1 KiB
Python

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
email = os.environ.get("GARMIN_EMAIL", "")
password = os.environ.get("GARMIN_PASSWORD", "")
if not email or not password:
return "GARMIN_EMAIL and GARMIN_PASSWORD environment variables are required."
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(email, 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."
@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}"
if __name__ == "__main__":
_startup_login()
mcp.run()