feat: add get_activities and get_activity_details tools

This commit is contained in:
Christophe Vila
2026-05-17 19:46:45 +02:00
parent 6b38f17e9b
commit 3630aa2c42
2 changed files with 91 additions and 0 deletions

View File

@@ -100,6 +100,36 @@ def complete_mfa(code: str) -> str:
return "Timed out waiting for authentication to complete." 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__": if __name__ == "__main__":
_startup_login() _startup_login()
mcp.run() mcp.run()

View File

@@ -2,9 +2,11 @@ import json
import os import os
import queue import queue
import threading import threading
from datetime import date
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
from garminconnect import GarminConnectAuthenticationError
import server import server
@@ -84,3 +86,62 @@ def test_authenticate_missing_credentials():
os.environ.pop("GARMIN_PASSWORD", None) os.environ.pop("GARMIN_PASSWORD", None)
result = server.authenticate() result = server.authenticate()
assert result == "GARMIN_EMAIL and GARMIN_PASSWORD environment variables are required." 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")