diff --git a/server.py b/server.py index bab7a09..7a93cc3 100644 --- a/server.py +++ b/server.py @@ -130,6 +130,108 @@ def get_activity_details(activity_id: str) -> str: return f"Error fetching activity details: {exc}" +@mcp.tool() +def get_sleep(date: str = "") -> str: + """Get sleep data for a date (YYYY-MM-DD): score, stages, duration. Defaults to today.""" + if err := _check_auth(): + return err + cdate = date or _today() + try: + result = _client.get_sleep_data(cdate) + if not result: + return f"No sleep data found for {cdate}" + return json.dumps(result, indent=2) + except GarminConnectAuthenticationError: + return "Authentication error. Call authenticate() again." + except Exception as exc: + return f"Error fetching sleep data: {exc}" + + +@mcp.tool() +def get_heart_rate(date: str = "") -> str: + """Get heart rate data for a date (YYYY-MM-DD): resting HR and daily timeline. Defaults to today.""" + if err := _check_auth(): + return err + cdate = date or _today() + try: + result = _client.get_heart_rates(cdate) + if not result: + return f"No heart rate data found for {cdate}" + return json.dumps(result, indent=2) + except GarminConnectAuthenticationError: + return "Authentication error. Call authenticate() again." + except Exception as exc: + return f"Error fetching heart rate data: {exc}" + + +@mcp.tool() +def get_stress(date: str = "") -> str: + """Get stress level timeline for a date (YYYY-MM-DD). Defaults to today.""" + if err := _check_auth(): + return err + cdate = date or _today() + try: + result = _client.get_stress_data(cdate) + if not result: + return f"No stress data found for {cdate}" + return json.dumps(result, indent=2) + except GarminConnectAuthenticationError: + return "Authentication error. Call authenticate() again." + except Exception as exc: + return f"Error fetching stress data: {exc}" + + +@mcp.tool() +def get_body_battery(date: str = "") -> str: + """Get body battery charge/drain values for a date (YYYY-MM-DD). Defaults to today.""" + if err := _check_auth(): + return err + cdate = date or _today() + try: + result = _client.get_body_battery(cdate, cdate) + if not result: + return f"No body battery data found for {cdate}" + return json.dumps(result, indent=2) + except GarminConnectAuthenticationError: + return "Authentication error. Call authenticate() again." + except Exception as exc: + return f"Error fetching body battery data: {exc}" + + +@mcp.tool() +def get_hrv(date: str = "") -> str: + """Get HRV status and nightly average for a date (YYYY-MM-DD). Defaults to today.""" + if err := _check_auth(): + return err + cdate = date or _today() + try: + result = _client.get_hrv_data(cdate) + if not result: + return f"No HRV data found for {cdate}" + return json.dumps(result, indent=2) + except GarminConnectAuthenticationError: + return "Authentication error. Call authenticate() again." + except Exception as exc: + return f"Error fetching HRV data: {exc}" + + +@mcp.tool() +def get_spo2(date: str = "") -> str: + """Get SpO2 (blood oxygen) readings for a date (YYYY-MM-DD). Defaults to today.""" + if err := _check_auth(): + return err + cdate = date or _today() + try: + result = _client.get_spo2_data(cdate) + if not result: + return f"No SpO2 data found for {cdate}" + return json.dumps(result, indent=2) + except GarminConnectAuthenticationError: + return "Authentication error. Call authenticate() again." + except Exception as exc: + return f"Error fetching SpO2 data: {exc}" + + if __name__ == "__main__": _startup_login() mcp.run() diff --git a/tests/test_server.py b/tests/test_server.py index 51777fd..b3e6d60 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -145,3 +145,70 @@ def test_get_activity_details_error(): server._client = MagicMock() server._client.get_activity_details.side_effect = Exception("not found") assert "Error fetching activity details" in server.get_activity_details("999") + + +def test_get_sleep_success(): + server._auth_state = "authenticated" + server._client = MagicMock() + server._client.get_sleep_data.return_value = { + "dailySleepDTO": {"sleepScores": {"overall": {"value": 85}}} + } + data = json.loads(server.get_sleep("2026-05-17")) + assert data["dailySleepDTO"]["sleepScores"]["overall"]["value"] == 85 + server._client.get_sleep_data.assert_called_once_with("2026-05-17") + + +def test_get_sleep_no_data(): + server._auth_state = "authenticated" + server._client = MagicMock() + server._client.get_sleep_data.return_value = None + assert server.get_sleep("2026-05-17") == "No sleep data found for 2026-05-17" + + +def test_get_heart_rate_success(): + server._auth_state = "authenticated" + server._client = MagicMock() + server._client.get_heart_rates.return_value = {"restingHeartRate": 52} + data = json.loads(server.get_heart_rate("2026-05-17")) + assert data["restingHeartRate"] == 52 + server._client.get_heart_rates.assert_called_once_with("2026-05-17") + + +def test_get_stress_success(): + server._auth_state = "authenticated" + server._client = MagicMock() + server._client.get_stress_data.return_value = {"stressValuesArray": [[1000, 35]]} + data = json.loads(server.get_stress("2026-05-17")) + assert "stressValuesArray" in data + + +def test_get_body_battery_success(): + server._auth_state = "authenticated" + server._client = MagicMock() + server._client.get_body_battery.return_value = [{"bodyBatteryStatList": []}] + data = json.loads(server.get_body_battery("2026-05-17")) + assert isinstance(data, list) + server._client.get_body_battery.assert_called_once_with("2026-05-17", "2026-05-17") + + +def test_get_hrv_success(): + server._auth_state = "authenticated" + server._client = MagicMock() + server._client.get_hrv_data.return_value = {"hrvSummary": {"weeklyAvg": 45}} + data = json.loads(server.get_hrv("2026-05-17")) + assert data["hrvSummary"]["weeklyAvg"] == 45 + + +def test_get_spo2_success(): + server._auth_state = "authenticated" + server._client = MagicMock() + server._client.get_spo2_data.return_value = {"spO2HourlyAverages": []} + data = json.loads(server.get_spo2("2026-05-17")) + assert "spO2HourlyAverages" in data + + +def test_health_tools_unauthenticated(): + server._auth_state = "unauthenticated" + for fn in (server.get_sleep, server.get_heart_rate, server.get_stress, + server.get_body_battery, server.get_hrv, server.get_spo2): + assert "Not authenticated" in fn("2026-05-17")