feat: add get_activities and get_activity_details tools
This commit is contained in:
30
server.py
30
server.py
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user