Skip to content

Commit 7e5dd40

Browse files
authored
Add comprehensive Garmin Connect tools (#5)
* Fix pre-commit hook formatting issues * Remove async from all tools to fix coroutine object issue - All tools now return actual data instead of coroutine objects - Fixes the '<coroutine object at 0x...>' responses when using tools - Tools remain fully functional but execute synchronously * Address CodeRabbit feedback - Add .claude/settings.local.json to .gitignore - Bump version to 0.0.7 (micro bump for new tools) - Use urllib.parse.urlencode for proper URL parameter encoding - Fix sleep_movement attribute handling with hasattr() check - Remove async-related changes (already addressed by removing async) Note: Intentionally not adding date validation as it adds unnecessary complexity for this use case. Users can handle invalid dates through the natural API error responses.
1 parent cdb08c7 commit 7e5dd40

File tree

6 files changed

+433
-94
lines changed

6 files changed

+433
-94
lines changed

.claude/settings.local.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"WebFetch(domain:raw.githubusercontent.com)",
5+
"WebFetch(domain:modelcontextprotocol.io)",
6+
"Bash(git checkout:*)",
7+
"Bash(git add:*)",
8+
"Bash(make format)",
9+
"Bash(make lint)",
10+
"Bash(uv sync:*)",
11+
"Bash(gh pr view:*)"
12+
],
13+
"deny": []
14+
}
15+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,6 @@ tmp/
5151

5252
# exclude saved oauth tokens
5353
oauth*_token.json
54+
55+
# Claude local settings
56+
.claude/settings.local.json

README.md

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,43 @@ use the same PATH your shell does. On macOS, it's typically
3434

3535
## Tools
3636

37-
- sleep
38-
- stress (weekly and daily)
39-
- daily intensity minutes
40-
- monthly activity summary
41-
- snapshot
37+
### Health & Wellness (using Garth data classes)
38+
39+
- `user_profile` - Get user profile information
40+
- `user_settings` - Get user settings and preferences
41+
- `nightly_sleep` - Get detailed sleep data with optional movement data
42+
- `daily_sleep` - Get daily sleep summary data
43+
- `daily_stress` / `weekly_stress` - Get stress data
44+
- `daily_intensity_minutes` / `weekly_intensity_minutes` - Get intensity minutes
45+
- `daily_body_battery` - Get body battery data
46+
- `daily_hydration` - Get hydration data
47+
- `daily_steps` / `weekly_steps` - Get steps data
48+
- `daily_hrv` / `hrv_data` - Get heart rate variability data
49+
50+
### Activities (using Garmin Connect API)
51+
52+
- `get_activities` - Get list of activities with optional filters
53+
- `get_activities_by_date` - Get activities for a specific date
54+
- `get_activity_details` - Get detailed activity information
55+
- `get_activity_splits` - Get activity lap/split data
56+
- `get_activity_weather` - Get weather data for activities
57+
58+
### Additional Health Data (using Garmin Connect API)
59+
60+
- `get_body_composition` - Get body composition data
61+
- `get_respiration_data` - Get respiration data
62+
- `get_spo2_data` - Get SpO2 (blood oxygen) data
63+
- `get_blood_pressure` - Get blood pressure readings
64+
65+
### Device & Gear (using Garmin Connect API)
66+
67+
- `get_devices` - Get connected devices
68+
- `get_device_settings` - Get device settings
69+
- `get_gear` - Get gear information
70+
- `get_gear_stats` - Get gear usage statistics
71+
72+
### Utility Tools
73+
74+
- `monthly_activity_summary` - Get monthly activity overview
75+
- `snapshot` - Get snapshot data for date ranges
76+
- `get_connectapi_endpoint` - Direct access to any Garmin Connect API endpoint

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ authors = [
88
{name = "Matin Tamizi", email = "[email protected]"},
99
]
1010
dependencies = [
11-
"garth>=0.5.9,<0.6.0",
11+
"garth>=0.5.14,<0.6.0",
1212
"mcp>=1.9.0,<2.0.0",
1313
]
1414
license = {text = "MIT"}

src/garth_mcp_server/__init__.py

Lines changed: 281 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import os
22
from datetime import date
33
from functools import wraps
4+
from urllib.parse import urlencode
45

56
import garth
67
from mcp.server.fastmcp import FastMCP
78

89

9-
__version__ = "0.0.6"
10+
__version__ = "0.0.7"
1011

1112
server = FastMCP("Garth - Garmin Connect", dependencies=["garth"], version=__version__)
1213

@@ -23,6 +24,278 @@ def wrapper(*args, **kwargs):
2324
return wrapper
2425

2526

27+
# Tools using Garth data classes
28+
29+
30+
@server.tool()
31+
@requires_garth_session
32+
def user_profile() -> str | garth.UserProfile:
33+
"""
34+
Get user profile information using Garth's UserProfile data class.
35+
"""
36+
return garth.UserProfile.get()
37+
38+
39+
@server.tool()
40+
@requires_garth_session
41+
def user_settings() -> str | garth.UserSettings:
42+
"""
43+
Get user settings using Garth's UserSettings data class.
44+
"""
45+
return garth.UserSettings.get()
46+
47+
48+
@server.tool()
49+
@requires_garth_session
50+
def weekly_intensity_minutes(
51+
end_date: date | None = None, weeks: int = 1
52+
) -> str | list[garth.WeeklyIntensityMinutes]:
53+
"""
54+
Get weekly intensity minutes data for a given date and number of weeks.
55+
If no date is provided, the current date will be used.
56+
If no weeks are provided, 1 week will be used.
57+
"""
58+
return garth.WeeklyIntensityMinutes.list(end_date, weeks)
59+
60+
61+
@server.tool()
62+
@requires_garth_session
63+
def daily_body_battery(
64+
end_date: date | None = None, days: int = 1
65+
) -> str | list[garth.DailyBodyBatteryStress]:
66+
"""
67+
Get daily body battery data for a given date and number of days.
68+
If no date is provided, the current date will be used.
69+
If no days are provided, 1 day will be used.
70+
"""
71+
return garth.DailyBodyBatteryStress.list(end_date, days)
72+
73+
74+
@server.tool()
75+
@requires_garth_session
76+
def daily_hydration(
77+
end_date: date | None = None, days: int = 1
78+
) -> str | list[garth.DailyHydration]:
79+
"""
80+
Get daily hydration data for a given date and number of days.
81+
If no date is provided, the current date will be used.
82+
If no days are provided, 1 day will be used.
83+
"""
84+
return garth.DailyHydration.list(end_date, days)
85+
86+
87+
@server.tool()
88+
@requires_garth_session
89+
def daily_steps(
90+
end_date: date | None = None, days: int = 1
91+
) -> str | list[garth.DailySteps]:
92+
"""
93+
Get daily steps data for a given date and number of days.
94+
If no date is provided, the current date will be used.
95+
If no days are provided, 1 day will be used.
96+
"""
97+
return garth.DailySteps.list(end_date, days)
98+
99+
100+
@server.tool()
101+
@requires_garth_session
102+
def weekly_steps(
103+
end_date: date | None = None, weeks: int = 1
104+
) -> str | list[garth.WeeklySteps]:
105+
"""
106+
Get weekly steps data for a given date and number of weeks.
107+
If no date is provided, the current date will be used.
108+
If no weeks are provided, 1 week will be used.
109+
"""
110+
return garth.WeeklySteps.list(end_date, weeks)
111+
112+
113+
@server.tool()
114+
@requires_garth_session
115+
def daily_hrv(
116+
end_date: date | None = None, days: int = 1
117+
) -> str | list[garth.DailyHRV]:
118+
"""
119+
Get daily heart rate variability data for a given date and number of days.
120+
If no date is provided, the current date will be used.
121+
If no days are provided, 1 day will be used.
122+
"""
123+
return garth.DailyHRV.list(end_date, days)
124+
125+
126+
@server.tool()
127+
@requires_garth_session
128+
def hrv_data(end_date: date | None = None, days: int = 1) -> str | list[garth.HRVData]:
129+
"""
130+
Get detailed HRV data for a given date and number of days.
131+
If no date is provided, the current date will be used.
132+
If no days are provided, 1 day will be used.
133+
"""
134+
return garth.HRVData.list(end_date, days)
135+
136+
137+
@server.tool()
138+
@requires_garth_session
139+
def daily_sleep(
140+
end_date: date | None = None, days: int = 1
141+
) -> str | list[garth.DailySleep]:
142+
"""
143+
Get daily sleep summary data for a given date and number of days.
144+
If no date is provided, the current date will be used.
145+
If no days are provided, 1 day will be used.
146+
"""
147+
return garth.DailySleep.list(end_date, days)
148+
149+
150+
# Tools using direct API calls
151+
152+
153+
@server.tool()
154+
@requires_garth_session
155+
def get_activities(
156+
start_date: str | None = None, limit: int | None = None
157+
) -> str | dict | None:
158+
"""
159+
Get list of activities from Garmin Connect.
160+
start_date: Start date for activities (YYYY-MM-DD format)
161+
limit: Maximum number of activities to return
162+
"""
163+
params = {}
164+
if start_date:
165+
params["startDate"] = start_date
166+
if limit:
167+
params["limit"] = str(limit)
168+
169+
endpoint = "activitylist-service/activities/search/activities"
170+
if params:
171+
endpoint += "?" + urlencode(params)
172+
return garth.connectapi(endpoint)
173+
174+
175+
@server.tool()
176+
@requires_garth_session
177+
def get_activities_by_date(date: str) -> str | dict | None:
178+
"""
179+
Get activities for a specific date from Garmin Connect.
180+
date: Date for activities (YYYY-MM-DD format)
181+
"""
182+
return garth.connectapi(f"wellness-service/wellness/dailySummaryChart/{date}")
183+
184+
185+
@server.tool()
186+
@requires_garth_session
187+
def get_activity_details(activity_id: str) -> str | dict | None:
188+
"""
189+
Get detailed information for a specific activity.
190+
activity_id: Garmin Connect activity ID
191+
"""
192+
return garth.connectapi(f"activity-service/activity/{activity_id}")
193+
194+
195+
@server.tool()
196+
@requires_garth_session
197+
def get_activity_splits(activity_id: str) -> str | dict | None:
198+
"""
199+
Get lap/split data for a specific activity.
200+
activity_id: Garmin Connect activity ID
201+
"""
202+
return garth.connectapi(f"activity-service/activity/{activity_id}/splits")
203+
204+
205+
@server.tool()
206+
@requires_garth_session
207+
def get_activity_weather(activity_id: str) -> str | dict | None:
208+
"""
209+
Get weather data for a specific activity.
210+
activity_id: Garmin Connect activity ID
211+
"""
212+
return garth.connectapi(f"activity-service/activity/{activity_id}/weather")
213+
214+
215+
@server.tool()
216+
@requires_garth_session
217+
def get_body_composition(date: str | None = None) -> str | dict | None:
218+
"""
219+
Get body composition data from Garmin Connect.
220+
date: Date for body composition data (YYYY-MM-DD format), if not provided returns latest
221+
"""
222+
if date:
223+
endpoint = f"wellness-service/wellness/bodyComposition/{date}"
224+
else:
225+
endpoint = "wellness-service/wellness/bodyComposition"
226+
return garth.connectapi(endpoint)
227+
228+
229+
@server.tool()
230+
@requires_garth_session
231+
def get_respiration_data(date: str) -> str | dict | None:
232+
"""
233+
Get respiration data from Garmin Connect.
234+
date: Date for respiration data (YYYY-MM-DD format)
235+
"""
236+
return garth.connectapi(f"wellness-service/wellness/dailyRespiration/{date}")
237+
238+
239+
@server.tool()
240+
@requires_garth_session
241+
def get_spo2_data(date: str) -> str | dict | None:
242+
"""
243+
Get SpO2 (blood oxygen) data from Garmin Connect.
244+
date: Date for SpO2 data (YYYY-MM-DD format)
245+
"""
246+
return garth.connectapi(f"wellness-service/wellness/dailyPulseOx/{date}")
247+
248+
249+
@server.tool()
250+
@requires_garth_session
251+
def get_blood_pressure(date: str) -> str | dict | None:
252+
"""
253+
Get blood pressure readings from Garmin Connect.
254+
date: Date for blood pressure data (YYYY-MM-DD format)
255+
"""
256+
return garth.connectapi(f"wellness-service/wellness/dailyBloodPressure/{date}")
257+
258+
259+
@server.tool()
260+
@requires_garth_session
261+
def get_devices() -> str | dict | None:
262+
"""
263+
Get connected devices from Garmin Connect.
264+
"""
265+
return garth.connectapi("device-service/deviceregistration/devices")
266+
267+
268+
@server.tool()
269+
@requires_garth_session
270+
def get_device_settings(device_id: str) -> str | dict | None:
271+
"""
272+
Get settings for a specific device.
273+
device_id: Device ID from Garmin Connect
274+
"""
275+
return garth.connectapi(
276+
f"device-service/deviceservice/device-info/settings/{device_id}"
277+
)
278+
279+
280+
@server.tool()
281+
@requires_garth_session
282+
def get_gear() -> str | dict | None:
283+
"""
284+
Get gear information from Garmin Connect.
285+
"""
286+
return garth.connectapi("gear-service/gear")
287+
288+
289+
@server.tool()
290+
@requires_garth_session
291+
def get_gear_stats(gear_uuid: str) -> str | dict | None:
292+
"""
293+
Get usage statistics for specific gear.
294+
gear_uuid: UUID of the gear item
295+
"""
296+
return garth.connectapi(f"gear-service/gear/stats/{gear_uuid}")
297+
298+
26299
@server.tool()
27300
@requires_garth_session
28301
def get_connectapi_endpoint(endpoint: str) -> str | dict | None:
@@ -36,16 +309,20 @@ def get_connectapi_endpoint(endpoint: str) -> str | dict | None:
36309
@server.tool()
37310
@requires_garth_session
38311
def nightly_sleep(
39-
end_date: date | None = None, nights: int = 1
312+
end_date: date | None = None, nights: int = 1, sleep_movement: bool = False
40313
) -> str | list[garth.SleepData]:
41314
"""
42315
Get sleep stats for a given date and number of nights.
43316
If no date is provided, the current date will be used.
44317
If no nights are provided, 1 night will be used.
318+
sleep_movement provides detailed sleep movement data. If looking at
319+
multiple nights, it'll be a lot of data.
45320
"""
46321
sleep_data = garth.SleepData.list(end_date, nights)
47-
for night in sleep_data:
48-
del night.sleep_movement
322+
if not sleep_movement:
323+
for night in sleep_data:
324+
if hasattr(night, "sleep_movement"):
325+
del night.sleep_movement
49326
return sleep_data
50327

51328

0 commit comments

Comments
 (0)