diff --git a/server.py b/server.py index 44f39cc..8ad8adf 100644 --- a/server.py +++ b/server.py @@ -266,6 +266,54 @@ def get_daily_stats(date: str = "") -> str: return f"Error fetching daily stats: {exc}" +@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}" + + if __name__ == "__main__": _startup_login() mcp.run() diff --git a/tests/test_server.py b/tests/test_server.py index e8204d8..fb2b71a 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -247,3 +247,52 @@ 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") + + +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()