From 3630aa2c42e2a41035379c5d0b4cbd51c4efd3fe Mon Sep 17 00:00:00 2001 From: Christophe Vila Date: Sun, 17 May 2026 19:46:45 +0200 Subject: [PATCH] feat: add get_activities and get_activity_details tools --- server.py | 30 ++++++++++++++++++++++ tests/test_server.py | 61 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/server.py b/server.py index c7ac702..bab7a09 100644 --- a/server.py +++ b/server.py @@ -100,6 +100,36 @@ def complete_mfa(code: str) -> str: 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() diff --git a/tests/test_server.py b/tests/test_server.py index caff357..51777fd 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -2,9 +2,11 @@ import json import os import queue import threading +from datetime import date from unittest.mock import MagicMock, patch import pytest +from garminconnect import GarminConnectAuthenticationError import server @@ -84,3 +86,62 @@ def test_authenticate_missing_credentials(): os.environ.pop("GARMIN_PASSWORD", None) result = server.authenticate() assert result == "GARMIN_EMAIL and GARMIN_PASSWORD environment variables are required." + + +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")