thryve-mcp-server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/Overview.csv +351 -0
  2. package/README.md +229 -0
  3. package/dist/category-mappings.d.ts +34 -0
  4. package/dist/category-mappings.d.ts.map +1 -0
  5. package/dist/category-mappings.js +146 -0
  6. package/dist/category-mappings.js.map +1 -0
  7. package/dist/data-enrichment.d.ts +19 -0
  8. package/dist/data-enrichment.d.ts.map +1 -0
  9. package/dist/data-enrichment.js +105 -0
  10. package/dist/data-enrichment.js.map +1 -0
  11. package/dist/data-types-generated.d.ts +70 -0
  12. package/dist/data-types-generated.d.ts.map +1 -0
  13. package/dist/data-types-generated.js +2597 -0
  14. package/dist/data-types-generated.js.map +1 -0
  15. package/dist/data-types.d.ts +53 -0
  16. package/dist/data-types.d.ts.map +1 -0
  17. package/dist/data-types.js +138 -0
  18. package/dist/data-types.js.map +1 -0
  19. package/dist/debug-logger.d.ts +17 -0
  20. package/dist/debug-logger.d.ts.map +1 -0
  21. package/dist/debug-logger.js +35 -0
  22. package/dist/debug-logger.js.map +1 -0
  23. package/dist/debug-middleware.d.ts +12 -0
  24. package/dist/debug-middleware.d.ts.map +1 -0
  25. package/dist/debug-middleware.js +36 -0
  26. package/dist/debug-middleware.js.map +1 -0
  27. package/dist/helpers.d.ts +19 -0
  28. package/dist/helpers.d.ts.map +1 -0
  29. package/dist/helpers.js +166 -0
  30. package/dist/helpers.js.map +1 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +754 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/schemas.d.ts +557 -0
  36. package/dist/schemas.d.ts.map +1 -0
  37. package/dist/schemas.js +338 -0
  38. package/dist/schemas.js.map +1 -0
  39. package/dist/tool-loader.d.ts +34 -0
  40. package/dist/tool-loader.d.ts.map +1 -0
  41. package/dist/tool-loader.js +108 -0
  42. package/dist/tool-loader.js.map +1 -0
  43. package/dist/tool-registry.d.ts +44 -0
  44. package/dist/tool-registry.d.ts.map +1 -0
  45. package/dist/tool-registry.js +54 -0
  46. package/dist/tool-registry.js.map +1 -0
  47. package/dist/tools/baseCategoryHandler.d.ts +28 -0
  48. package/dist/tools/baseCategoryHandler.d.ts.map +1 -0
  49. package/dist/tools/baseCategoryHandler.js +184 -0
  50. package/dist/tools/baseCategoryHandler.js.map +1 -0
  51. package/dist/tools/categories/getActivity.d.ts +10 -0
  52. package/dist/tools/categories/getActivity.d.ts.map +1 -0
  53. package/dist/tools/categories/getActivity.js +5 -0
  54. package/dist/tools/categories/getActivity.js.map +1 -0
  55. package/dist/tools/categories/getAudioAndHearing.d.ts +10 -0
  56. package/dist/tools/categories/getAudioAndHearing.d.ts.map +1 -0
  57. package/dist/tools/categories/getAudioAndHearing.js +5 -0
  58. package/dist/tools/categories/getAudioAndHearing.js.map +1 -0
  59. package/dist/tools/categories/getBloodGlucose.d.ts +10 -0
  60. package/dist/tools/categories/getBloodGlucose.d.ts.map +1 -0
  61. package/dist/tools/categories/getBloodGlucose.js +5 -0
  62. package/dist/tools/categories/getBloodGlucose.js.map +1 -0
  63. package/dist/tools/categories/getBodyComposition.d.ts +10 -0
  64. package/dist/tools/categories/getBodyComposition.d.ts.map +1 -0
  65. package/dist/tools/categories/getBodyComposition.js +5 -0
  66. package/dist/tools/categories/getBodyComposition.js.map +1 -0
  67. package/dist/tools/categories/getCardiovascular.d.ts +10 -0
  68. package/dist/tools/categories/getCardiovascular.d.ts.map +1 -0
  69. package/dist/tools/categories/getCardiovascular.js +5 -0
  70. package/dist/tools/categories/getCardiovascular.js.map +1 -0
  71. package/dist/tools/categories/getHeartRate.d.ts +10 -0
  72. package/dist/tools/categories/getHeartRate.d.ts.map +1 -0
  73. package/dist/tools/categories/getHeartRate.js +5 -0
  74. package/dist/tools/categories/getHeartRate.js.map +1 -0
  75. package/dist/tools/categories/getLocation.d.ts +10 -0
  76. package/dist/tools/categories/getLocation.d.ts.map +1 -0
  77. package/dist/tools/categories/getLocation.js +5 -0
  78. package/dist/tools/categories/getLocation.js.map +1 -0
  79. package/dist/tools/categories/getMicrobiome.d.ts +10 -0
  80. package/dist/tools/categories/getMicrobiome.d.ts.map +1 -0
  81. package/dist/tools/categories/getMicrobiome.js +5 -0
  82. package/dist/tools/categories/getMicrobiome.js.map +1 -0
  83. package/dist/tools/categories/getMovementAnalysis.d.ts +10 -0
  84. package/dist/tools/categories/getMovementAnalysis.d.ts.map +1 -0
  85. package/dist/tools/categories/getMovementAnalysis.js +5 -0
  86. package/dist/tools/categories/getMovementAnalysis.js.map +1 -0
  87. package/dist/tools/categories/getNutrition.d.ts +10 -0
  88. package/dist/tools/categories/getNutrition.d.ts.map +1 -0
  89. package/dist/tools/categories/getNutrition.js +5 -0
  90. package/dist/tools/categories/getNutrition.js.map +1 -0
  91. package/dist/tools/categories/getRespiratory.d.ts +10 -0
  92. package/dist/tools/categories/getRespiratory.d.ts.map +1 -0
  93. package/dist/tools/categories/getRespiratory.js +5 -0
  94. package/dist/tools/categories/getRespiratory.js.map +1 -0
  95. package/dist/tools/categories/getSleep.d.ts +10 -0
  96. package/dist/tools/categories/getSleep.d.ts.map +1 -0
  97. package/dist/tools/categories/getSleep.js +5 -0
  98. package/dist/tools/categories/getSleep.js.map +1 -0
  99. package/dist/tools/categories/getStressAndHRV.d.ts +10 -0
  100. package/dist/tools/categories/getStressAndHRV.d.ts.map +1 -0
  101. package/dist/tools/categories/getStressAndHRV.js +5 -0
  102. package/dist/tools/categories/getStressAndHRV.js.map +1 -0
  103. package/dist/tools/categories/getWellness.d.ts +10 -0
  104. package/dist/tools/categories/getWellness.d.ts.map +1 -0
  105. package/dist/tools/categories/getWellness.js +5 -0
  106. package/dist/tools/categories/getWellness.js.map +1 -0
  107. package/dist/tools/categories/getWomensHealth.d.ts +10 -0
  108. package/dist/tools/categories/getWomensHealth.d.ts.map +1 -0
  109. package/dist/tools/categories/getWomensHealth.js +5 -0
  110. package/dist/tools/categories/getWomensHealth.js.map +1 -0
  111. package/dist/tools/categories/getWorkouts.d.ts +10 -0
  112. package/dist/tools/categories/getWorkouts.d.ts.map +1 -0
  113. package/dist/tools/categories/getWorkouts.js +5 -0
  114. package/dist/tools/categories/getWorkouts.js.map +1 -0
  115. package/dist/tools/categoryTools.d.ts +100 -0
  116. package/dist/tools/categoryTools.d.ts.map +1 -0
  117. package/dist/tools/categoryTools.js +66 -0
  118. package/dist/tools/categoryTools.js.map +1 -0
  119. package/dist/tools/get.d.ts +39 -0
  120. package/dist/tools/get.d.ts.map +1 -0
  121. package/dist/tools/get.js +118 -0
  122. package/dist/tools/get.js.map +1 -0
  123. package/dist/tools/getAvailableDataTypes.d.ts +101 -0
  124. package/dist/tools/getAvailableDataTypes.d.ts.map +1 -0
  125. package/dist/tools/getAvailableDataTypes.js +120 -0
  126. package/dist/tools/getAvailableDataTypes.js.map +1 -0
  127. package/dist/tools/getConnectionWidgetUrl.d.ts +12 -0
  128. package/dist/tools/getConnectionWidgetUrl.d.ts.map +1 -0
  129. package/dist/tools/getConnectionWidgetUrl.js +26 -0
  130. package/dist/tools/getConnectionWidgetUrl.js.map +1 -0
  131. package/dist/tools/getDailyData.d.ts +30 -0
  132. package/dist/tools/getDailyData.d.ts.map +1 -0
  133. package/dist/tools/getDailyData.js +114 -0
  134. package/dist/tools/getDailyData.js.map +1 -0
  135. package/dist/tools/getEpochData.d.ts +30 -0
  136. package/dist/tools/getEpochData.d.ts.map +1 -0
  137. package/dist/tools/getEpochData.js +111 -0
  138. package/dist/tools/getEpochData.js.map +1 -0
  139. package/dist/tools/getHealthDataByCategory.d.ts +38 -0
  140. package/dist/tools/getHealthDataByCategory.d.ts.map +1 -0
  141. package/dist/tools/getHealthDataByCategory.js +231 -0
  142. package/dist/tools/getHealthDataByCategory.js.map +1 -0
  143. package/dist/tools/getUserInformation.d.ts +8 -0
  144. package/dist/tools/getUserInformation.d.ts.map +1 -0
  145. package/dist/tools/getUserInformation.js +24 -0
  146. package/dist/tools/getUserInformation.js.map +1 -0
  147. package/dist/tools/index.d.ts +7 -0
  148. package/dist/tools/index.d.ts.map +1 -0
  149. package/dist/tools/index.js +12 -0
  150. package/dist/tools/index.js.map +1 -0
  151. package/dist/tools/legacy.d.ts +37 -0
  152. package/dist/tools/legacy.d.ts.map +1 -0
  153. package/dist/tools/legacy.js +105 -0
  154. package/dist/tools/legacy.js.map +1 -0
  155. package/dist/tools/listConnectedSources.d.ts +11 -0
  156. package/dist/tools/listConnectedSources.d.ts.map +1 -0
  157. package/dist/tools/listConnectedSources.js +29 -0
  158. package/dist/tools/listConnectedSources.js.map +1 -0
  159. package/dist/tools/listDevices.d.ts +11 -0
  160. package/dist/tools/listDevices.d.ts.map +1 -0
  161. package/dist/tools/listDevices.js +29 -0
  162. package/dist/tools/listDevices.js.map +1 -0
  163. package/dist/tools/search-filters.d.ts +42 -0
  164. package/dist/tools/search-filters.d.ts.map +1 -0
  165. package/dist/tools/search-filters.js +210 -0
  166. package/dist/tools/search-filters.js.map +1 -0
  167. package/dist/tools/search-keyword.d.ts +32 -0
  168. package/dist/tools/search-keyword.d.ts.map +1 -0
  169. package/dist/tools/search-keyword.js +122 -0
  170. package/dist/tools/search-keyword.js.map +1 -0
  171. package/dist/tools/search-tools.d.ts +8 -0
  172. package/dist/tools/search-tools.d.ts.map +1 -0
  173. package/dist/tools/search-tools.js +273 -0
  174. package/dist/tools/search-tools.js.map +1 -0
  175. package/dist/types.d.ts +37 -0
  176. package/dist/types.d.ts.map +1 -0
  177. package/dist/types.js +2 -0
  178. package/dist/types.js.map +1 -0
  179. package/package.json +30 -0
  180. package/parse-csv.py +152 -0
  181. package/src/category-mappings.ts +181 -0
  182. package/src/data-enrichment.ts +125 -0
  183. package/src/data-types-generated.ts +2652 -0
  184. package/src/helpers.ts +198 -0
  185. package/src/index.ts +859 -0
  186. package/src/schemas.ts +372 -0
  187. package/src/tools/baseCategoryHandler.ts +243 -0
  188. package/src/tools/categoryTools.ts +101 -0
  189. package/src/tools/get.ts +147 -0
  190. package/src/tools/getAvailableDataTypes.ts +148 -0
  191. package/src/tools/index.ts +32 -0
  192. package/src/tools/listConnectedSources.ts +45 -0
  193. package/src/tools/listDevices.ts +45 -0
  194. package/src/tools/search-filters.ts +253 -0
  195. package/src/tools/search-keyword.ts +162 -0
  196. package/src/types.ts +44 -0
  197. package/tsconfig.json +20 -0
package/src/schemas.ts ADDED
@@ -0,0 +1,372 @@
1
+ import { z } from "zod";
2
+
3
+ // Zod schemas for tool inputs
4
+
5
+ // CATEGORY-BASED TOOLS (Primary LLM-friendly interface)
6
+ export const GetDailyDataSchema = z.object({
7
+ categories: z
8
+ .array(
9
+ z.enum([
10
+ "Activity",
11
+ "Heart Rate",
12
+ "Sleep",
13
+ "Workouts",
14
+ "Stress & HRV",
15
+ "Respiratory",
16
+ "Blood Glucose",
17
+ "Body Composition",
18
+ "Womens Health",
19
+ "Nutrition",
20
+ "Cardiovascular",
21
+ "Wellness",
22
+ "Audio & Hearing",
23
+ "Microbiome",
24
+ "Location",
25
+ "Movement Analysis",
26
+ ])
27
+ )
28
+ .describe(
29
+ "Health data categories to retrieve. Choose from: Activity (steps, distance, calories), Heart Rate, Sleep, Workouts, Stress & HRV, Respiratory, Blood Glucose, Body Composition, Womens Health, Nutrition, Cardiovascular, Wellness, Audio & Hearing, Microbiome, Location, Movement Analysis"
30
+ ),
31
+ startTimestamp: z
32
+ .string()
33
+ .optional()
34
+ .describe(
35
+ "Start time in ISO 8601 format (e.g., '2025-10-28T00:00:00Z'). Defaults to 7 days ago"
36
+ ),
37
+ endTimestamp: z
38
+ .string()
39
+ .optional()
40
+ .describe(
41
+ "End time in ISO 8601 format (e.g., '2025-10-28T23:59:59Z'). Defaults to now"
42
+ ),
43
+ dataSources: z
44
+ .array(z.number())
45
+ .optional()
46
+ .describe(
47
+ "Filter by specific data source IDs (e.g., [1, 2] for Fitbit and Garmin). Leave empty for all sources"
48
+ ),
49
+ limit: z
50
+ .number()
51
+ .int()
52
+ .positive()
53
+ .optional()
54
+ .describe(
55
+ "Maximum number of records to return per category (default: no limit)"
56
+ ),
57
+ });
58
+
59
+ export const GetEpochDataSchema = z.object({
60
+ categories: z
61
+ .array(
62
+ z.enum([
63
+ "Activity",
64
+ "Heart Rate",
65
+ "Sleep",
66
+ "Workouts",
67
+ "Stress & HRV",
68
+ "Respiratory",
69
+ "Blood Glucose",
70
+ "Body Composition",
71
+ "Womens Health",
72
+ "Nutrition",
73
+ "Cardiovascular",
74
+ "Wellness",
75
+ "Audio & Hearing",
76
+ "Microbiome",
77
+ "Location",
78
+ "Movement Analysis",
79
+ ])
80
+ )
81
+ .describe(
82
+ "Health data categories to retrieve. Choose from: Activity (steps, distance, calories), Heart Rate, Sleep, Workouts, Stress & HRV, Respiratory, Blood Glucose, Body Composition, Womens Health, Nutrition, Cardiovascular, Wellness, Audio & Hearing, Microbiome, Location, Movement Analysis"
83
+ ),
84
+ startTimestamp: z
85
+ .string()
86
+ .optional()
87
+ .describe(
88
+ "Start time in ISO 8601 format (e.g., '2025-10-28T00:00:00Z'). Defaults to 7 days ago"
89
+ ),
90
+ endTimestamp: z
91
+ .string()
92
+ .optional()
93
+ .describe(
94
+ "End time in ISO 8601 format (e.g., '2025-10-28T23:59:59Z'). Defaults to now"
95
+ ),
96
+ dataSources: z
97
+ .array(z.number())
98
+ .optional()
99
+ .describe(
100
+ "Filter by specific data source IDs (e.g., [1, 2] for Fitbit and Garmin). Leave empty for all sources"
101
+ ),
102
+ limit: z
103
+ .number()
104
+ .int()
105
+ .positive()
106
+ .optional()
107
+ .describe(
108
+ "Maximum number of records to return per category (default: no limit)"
109
+ ),
110
+ });
111
+
112
+ export const SearchKeywordSchema = z.object({
113
+ query: z
114
+ .string()
115
+ .describe("Property name to search for (e.g., 'SleepBinary', 'Steps', 'HeartRate'). Searches for exact or partial matches in data type property names"),
116
+ startDate: z
117
+ .string()
118
+ .optional()
119
+ .describe("Filter by start date in YYYY-MM-DD format"),
120
+ endDate: z
121
+ .string()
122
+ .optional()
123
+ .describe("Filter by end date in YYYY-MM-DD format"),
124
+ dataType: z
125
+ .string()
126
+ .optional()
127
+ .describe("Filter by data type category (e.g., 'Activity', 'Sleep', 'Heart Rate')"),
128
+ dataSource: z
129
+ .string()
130
+ .optional()
131
+ .describe("Filter by data source ID (e.g., '1' for Fitbit, '2' for Garmin)"),
132
+ page: z
133
+ .number()
134
+ .int()
135
+ .positive()
136
+ .optional()
137
+ .default(1)
138
+ .describe("Page number for pagination (default: 1)"),
139
+ pageSize: z
140
+ .number()
141
+ .int()
142
+ .positive()
143
+ .max(200)
144
+ .optional()
145
+ .default(50)
146
+ .describe("Number of results per page (default: 50, max: 200)"),
147
+ sortBy: z
148
+ .enum(["timestamp", "day", "value", "dataType", "dataSource"])
149
+ .optional()
150
+ .default("timestamp")
151
+ .describe("Field to sort by (default: 'timestamp')"),
152
+ sortOrder: z
153
+ .enum(["asc", "desc"])
154
+ .optional()
155
+ .default("desc")
156
+ .describe("Sort order (default: 'desc')"),
157
+ });
158
+
159
+ export const SearchFiltersSchema = z.object({
160
+ resolution: z
161
+ .enum(["daily", "epoch", "both"])
162
+ .optional()
163
+ .default("both")
164
+ .describe("Data resolution: 'daily' for daily aggregated data, 'epoch' for intraday time-series data, 'both' for combined (default: 'both')"),
165
+ dataTypes: z
166
+ .array(z.number())
167
+ .optional()
168
+ .describe("Filter by specific data type IDs (e.g., [1000, 3000] for Steps and HeartRate)"),
169
+ dataSources: z
170
+ .array(z.number())
171
+ .optional()
172
+ .describe("Filter by data source IDs (e.g., [1, 2] for Fitbit and Garmin)"),
173
+ categories: z
174
+ .array(z.enum([
175
+ "Activity",
176
+ "Heart Rate",
177
+ "Sleep",
178
+ "Workouts",
179
+ "Stress & HRV",
180
+ "Respiratory",
181
+ "Blood Glucose",
182
+ "Body Composition",
183
+ "Womens Health",
184
+ "Nutrition",
185
+ "Cardiovascular",
186
+ "Wellness",
187
+ "Audio & Hearing",
188
+ "Microbiome",
189
+ "Location",
190
+ "Movement Analysis",
191
+ ]))
192
+ .optional()
193
+ .describe("Filter by high-level health categories. When specified, results can be grouped by category using groupByCategory parameter"),
194
+ groupByCategory: z
195
+ .boolean()
196
+ .optional()
197
+ .default(false)
198
+ .describe("Group results by category (only applies when categories are specified). Returns { results: { Category: [...] }, metadata: {...} } format instead of flat paginated list"),
199
+ limit: z
200
+ .number()
201
+ .int()
202
+ .positive()
203
+ .optional()
204
+ .describe("Maximum number of records to return (applied before pagination when groupByCategory is false)"),
205
+ startDate: z
206
+ .string()
207
+ .optional()
208
+ .describe("Filter by start date in YYYY-MM-DD format"),
209
+ endDate: z
210
+ .string()
211
+ .optional()
212
+ .describe("Filter by end date in YYYY-MM-DD format"),
213
+ startTimestamp: z
214
+ .string()
215
+ .optional()
216
+ .describe("Filter by start timestamp in ISO 8601 format (e.g., '2025-10-28T00:00:00Z')"),
217
+ endTimestamp: z
218
+ .string()
219
+ .optional()
220
+ .describe("Filter by end timestamp in ISO 8601 format (e.g., '2025-10-29T00:00:00Z')"),
221
+ valueMin: z
222
+ .number()
223
+ .optional()
224
+ .describe("Minimum value threshold for filtering"),
225
+ valueMax: z
226
+ .number()
227
+ .optional()
228
+ .describe("Maximum value threshold for filtering"),
229
+ page: z
230
+ .number()
231
+ .int()
232
+ .positive()
233
+ .optional()
234
+ .default(1)
235
+ .describe("Page number for pagination (default: 1)"),
236
+ pageSize: z
237
+ .number()
238
+ .int()
239
+ .positive()
240
+ .max(200)
241
+ .optional()
242
+ .default(50)
243
+ .describe("Number of results per page (default: 50, max: 200)"),
244
+ sortBy: z
245
+ .enum(["timestamp", "day", "value", "dataType", "dataSource"])
246
+ .optional()
247
+ .default("timestamp")
248
+ .describe("Field to sort by (default: 'timestamp')"),
249
+ sortOrder: z
250
+ .enum(["asc", "desc"])
251
+ .optional()
252
+ .default("desc")
253
+ .describe("Sort order (default: 'desc')"),
254
+ });
255
+
256
+ export const GetSchema = z.object({
257
+ dataType: z
258
+ .number()
259
+ .int()
260
+ .positive()
261
+ .describe("Data type ID (e.g., 1000 for Steps, 3000 for HeartRate)"),
262
+ dataSource: z
263
+ .number()
264
+ .int()
265
+ .positive()
266
+ .describe("Data source ID (e.g., 1 for Fitbit, 2 for Garmin)"),
267
+ day: z
268
+ .string()
269
+ .optional()
270
+ .describe("Specific day in YYYY-MM-DD format for daily data. Provide either 'day' or 'timestamp', not both"),
271
+ timestamp: z
272
+ .string()
273
+ .optional()
274
+ .describe("Specific timestamp in ISO 8601 format for epoch data (e.g., '2025-10-28T14:30:00Z'). Provide either 'day' or 'timestamp', not both"),
275
+ includeMetadata: z
276
+ .boolean()
277
+ .optional()
278
+ .default(true)
279
+ .describe("Include additional metadata about the record (default: true)"),
280
+ });
281
+
282
+ // INTERNAL/ADVANCED TOOLS
283
+ export const GetAvailableDataTypesSchema = z.object({
284
+ category: z
285
+ .enum([
286
+ "Activity",
287
+ "Heart Rate",
288
+ "Sleep",
289
+ "Workouts",
290
+ "Stress & HRV",
291
+ "Respiratory",
292
+ "Blood Glucose",
293
+ "Body Composition",
294
+ "Womens Health",
295
+ "Nutrition",
296
+ "Cardiovascular",
297
+ "Wellness",
298
+ "Audio & Hearing",
299
+ "Microbiome",
300
+ "Location",
301
+ "Movement Analysis",
302
+ ])
303
+ .optional()
304
+ .describe("Filter by high-level health category to see available data types in that category"),
305
+ search: z
306
+ .string()
307
+ .optional()
308
+ .describe("Search keyword to find specific data types (e.g., 'calories', 'steps', 'heart')"),
309
+ dataSource: z
310
+ .string()
311
+ .optional()
312
+ .describe("Filter by wearable brand (e.g., 'Fitbit', 'Garmin', 'Whoop', 'Oura')"),
313
+ });
314
+
315
+ // END USER INFORMATION TOOLS
316
+ export const ListConnectedSourcesSchema = z.object({});
317
+
318
+ export const ListDevicesSchema = z.object({});
319
+
320
+ // CATEGORY-SPECIFIC TOOLS (16 specialized tools)
321
+ // Base schema factory for category-specific tools
322
+ const createCategorySchema = () => z.object({
323
+ resolution: z
324
+ .enum(["daily", "epoch"])
325
+ .optional()
326
+ .default("daily")
327
+ .describe("Data resolution: 'daily' for daily aggregated data, 'epoch' for intraday time-series data (default: 'daily')"),
328
+ startTimestamp: z
329
+ .string()
330
+ .optional()
331
+ .describe("Start time in ISO 8601 format (e.g., '2025-10-28T00:00:00Z'). Defaults to 7 days ago"),
332
+ endTimestamp: z
333
+ .string()
334
+ .optional()
335
+ .describe("End time in ISO 8601 format (e.g., '2025-10-28T23:59:59Z'). Defaults to now"),
336
+ dataSources: z
337
+ .array(z.number())
338
+ .optional()
339
+ .describe("Filter by specific data source IDs (e.g., [1, 2] for Fitbit and Garmin). Leave empty for all sources"),
340
+ limit: z
341
+ .number()
342
+ .int()
343
+ .positive()
344
+ .optional()
345
+ .describe("Maximum number of records to return (default: no limit)"),
346
+ includeFields: z
347
+ .array(z.string())
348
+ .optional()
349
+ .describe("Sub-categories to include (e.g., ['Cardiovascular Health', 'Advanced Cardiac Analysis']). Leave empty for all sub-categories in this category"),
350
+ excludeFields: z
351
+ .array(z.string())
352
+ .optional()
353
+ .describe("Sub-categories to exclude (e.g., ['Advanced Cardiac Analysis']). Leave empty to exclude none"),
354
+ });
355
+
356
+ // Individual schemas for each category
357
+ export const GetActivitySchema = createCategorySchema();
358
+ export const GetHeartRateSchema = createCategorySchema();
359
+ export const GetSleepSchema = createCategorySchema();
360
+ export const GetWorkoutsSchema = createCategorySchema();
361
+ export const GetStressAndHRVSchema = createCategorySchema();
362
+ export const GetRespiratorySchema = createCategorySchema();
363
+ export const GetBloodGlucoseSchema = createCategorySchema();
364
+ export const GetBodyCompositionSchema = createCategorySchema();
365
+ export const GetWomensHealthSchema = createCategorySchema();
366
+ export const GetNutritionSchema = createCategorySchema();
367
+ export const GetCardiovascularSchema = createCategorySchema();
368
+ export const GetWellnessSchema = createCategorySchema();
369
+ export const GetAudioAndHearingSchema = createCategorySchema();
370
+ export const GetMicrobiomeSchema = createCategorySchema();
371
+ export const GetLocationSchema = createCategorySchema();
372
+ export const GetMovementAnalysisSchema = createCategorySchema();
@@ -0,0 +1,243 @@
1
+ import type { Request } from "express";
2
+ import type { HealthCategory } from "../category-mappings.js";
3
+ import type { HealthDataRecord } from "../types.js";
4
+ import {
5
+ extractThryveHeaders,
6
+ buildUrlEncodedBody,
7
+ getDefaultTimestampRange,
8
+ THRYVE_API_BASE_URL,
9
+ } from "../helpers.js";
10
+ import { getDataTypeIdsForCategories, CATEGORY_MAPPINGS } from "../category-mappings.js";
11
+ import { flattenThryveResponse, enrichWithDataTypes } from "../data-enrichment.js";
12
+ import { ALL_DATA_TYPES } from "../data-types-generated.js";
13
+
14
+ export interface BaseCategoryParams {
15
+ resolution?: "daily" | "epoch";
16
+ startTimestamp?: string;
17
+ endTimestamp?: string;
18
+ dataSources?: number[];
19
+ limit?: number;
20
+ includeFields?: string[];
21
+ excludeFields?: string[];
22
+ }
23
+
24
+ /**
25
+ * Base handler for category-specific tools
26
+ * Fetches health data for a single category with configurable resolution
27
+ */
28
+ export async function baseCategoryHandler(
29
+ req: Request,
30
+ category: HealthCategory,
31
+ params: BaseCategoryParams
32
+ ) {
33
+ const { headers, authToken } = extractThryveHeaders(req);
34
+
35
+ // Apply defaults
36
+ const resolution = params.resolution || "daily";
37
+ const defaults = getDefaultTimestampRange();
38
+ const startTimestamp = params.startTimestamp || defaults.startTimestamp;
39
+ const endTimestamp = params.endTimestamp || defaults.endTimestamp;
40
+
41
+ // Get data type IDs for this category
42
+ const allDataTypeIds = getDataTypeIdsForCategories([category]);
43
+
44
+ if (allDataTypeIds.length === 0) {
45
+ return {
46
+ category,
47
+ resolution,
48
+ data: [],
49
+ metadata: {
50
+ timeRange: { start: startTimestamp, end: endTimestamp },
51
+ totalRecords: 0,
52
+ message: "No data types found for this category",
53
+ },
54
+ };
55
+ }
56
+
57
+ // Filter data types by includeFields/excludeFields (which are actually sub-category names)
58
+ let filteredDataTypeIds = allDataTypeIds;
59
+
60
+ if (params.includeFields && params.includeFields.length > 0) {
61
+ const includeSet = new Set(params.includeFields);
62
+ filteredDataTypeIds = allDataTypeIds.filter((id) => {
63
+ const dataType = ALL_DATA_TYPES.find((dt) => dt.id === id);
64
+ if (!dataType) return false;
65
+ // Check if any of the data type's categories match the include list
66
+ const dataTypeCategories = dataType.category.split(",").map((c) => c.trim());
67
+ return dataTypeCategories.some((cat) => includeSet.has(cat));
68
+ });
69
+ }
70
+
71
+ if (params.excludeFields && params.excludeFields.length > 0) {
72
+ const excludeSet = new Set(params.excludeFields);
73
+ filteredDataTypeIds = filteredDataTypeIds.filter((id) => {
74
+ const dataType = ALL_DATA_TYPES.find((dt) => dt.id === id);
75
+ if (!dataType) return false;
76
+ // Check if any of the data type's categories match the exclude list
77
+ const dataTypeCategories = dataType.category.split(",").map((c) => c.trim());
78
+ return !dataTypeCategories.some((cat) => excludeSet.has(cat));
79
+ });
80
+ }
81
+
82
+ if (filteredDataTypeIds.length === 0) {
83
+ return {
84
+ category,
85
+ resolution,
86
+ data: [],
87
+ metadata: {
88
+ timeRange: { start: startTimestamp, end: endTimestamp },
89
+ totalRecords: 0,
90
+ message: "No data types match the field filters",
91
+ appliedFilters: {
92
+ includeFields: params.includeFields,
93
+ excludeFields: params.excludeFields,
94
+ },
95
+ },
96
+ };
97
+ }
98
+
99
+ // Fetch data based on resolution
100
+ let rawData: any;
101
+ if (resolution === "daily") {
102
+ rawData = await fetchDailyData(headers, authToken, startTimestamp, endTimestamp, params.dataSources);
103
+ } else {
104
+ rawData = await fetchEpochData(headers, authToken, startTimestamp, endTimestamp, params.dataSources);
105
+ }
106
+
107
+ // Flatten and enrich data
108
+ const flattenedData = flattenThryveResponse(rawData);
109
+ enrichWithDataTypes(flattenedData);
110
+
111
+ // Filter by category's data type IDs
112
+ const dataTypeIdSet = new Set(filteredDataTypeIds);
113
+ const categoryData = flattenedData.filter((record) => {
114
+ const typeId = record.dataType || record.dailyDynamicValueType || record.dynamicEpochValueType;
115
+ return typeId && dataTypeIdSet.has(typeId);
116
+ });
117
+
118
+ // Apply limit if specified
119
+ const limitedData = params.limit ? categoryData.slice(0, params.limit) : categoryData;
120
+
121
+ // Build metadata
122
+ const dataTypesFound = new Set<string>();
123
+ const fieldsFound = new Set<string>();
124
+ limitedData.forEach((record) => {
125
+ const typeName = record.dailyDynamicValueTypeName || record.dynamicEpochValueTypeName;
126
+ if (typeName) {
127
+ dataTypesFound.add(typeName);
128
+ fieldsFound.add(typeName);
129
+ }
130
+ });
131
+
132
+ const metadata: any = {
133
+ category,
134
+ resolution,
135
+ timeRange: {
136
+ start: startTimestamp,
137
+ end: endTimestamp,
138
+ },
139
+ totalRecords: limitedData.length,
140
+ fieldsFound: Array.from(fieldsFound).sort(),
141
+ dataSources: params.dataSources,
142
+ };
143
+
144
+ if (params.limit) {
145
+ metadata.limitApplied = params.limit;
146
+ }
147
+
148
+ if (params.includeFields || params.excludeFields) {
149
+ metadata.appliedFilters = {
150
+ includeFields: params.includeFields,
151
+ excludeFields: params.excludeFields,
152
+ };
153
+ }
154
+
155
+ return {
156
+ category,
157
+ resolution,
158
+ data: limitedData,
159
+ metadata,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Fetch daily data from Thryve API
165
+ */
166
+ async function fetchDailyData(
167
+ headers: Record<string, string>,
168
+ authToken: string,
169
+ startTimestamp: string,
170
+ endTimestamp: string,
171
+ dataSources?: number[]
172
+ ) {
173
+ // Extract dates from timestamps for daily API
174
+ const startDay = startTimestamp.split('T')[0];
175
+ const endDay = endTimestamp.split('T')[0];
176
+
177
+ const dataSourcesParam = dataSources?.join(',');
178
+
179
+ const body = buildUrlEncodedBody({
180
+ authenticationToken: authToken,
181
+ startDay,
182
+ endDay,
183
+ dataSources: dataSourcesParam,
184
+ displayTypeName: "true",
185
+ });
186
+
187
+ const response = await fetch(`${THRYVE_API_BASE_URL}/v5/dailyDynamicValues`, {
188
+ method: "POST",
189
+ headers,
190
+ body,
191
+ });
192
+
193
+ if (!response.ok) {
194
+ throw new Error(`Thryve API error: ${response.status} ${response.statusText}`);
195
+ }
196
+
197
+ return await response.json();
198
+ }
199
+
200
+ /**
201
+ * Fetch epoch data from Thryve API
202
+ */
203
+ async function fetchEpochData(
204
+ headers: Record<string, string>,
205
+ authToken: string,
206
+ startTimestamp: string,
207
+ endTimestamp: string,
208
+ dataSources?: number[]
209
+ ) {
210
+ const dataSourcesParam = dataSources?.join(',');
211
+
212
+ const body = buildUrlEncodedBody({
213
+ authenticationToken: authToken,
214
+ startTimestamp,
215
+ endTimestamp,
216
+ dataSources: dataSourcesParam,
217
+ displayTypeName: "true",
218
+ });
219
+
220
+ const response = await fetch(`${THRYVE_API_BASE_URL}/v5/dynamicEpochValues`, {
221
+ method: "POST",
222
+ headers,
223
+ body,
224
+ });
225
+
226
+ if (!response.ok) {
227
+ throw new Error(`Thryve API error: ${response.status} ${response.statusText}`);
228
+ }
229
+
230
+ return await response.json();
231
+ }
232
+
233
+ /**
234
+ * Get available sub-categories for a category
235
+ * Returns the list of Thryve sub-category names that can be used with includeFields/excludeFields
236
+ */
237
+ export function getAvailableFieldsForCategory(category: HealthCategory): string[] {
238
+ const mapping = CATEGORY_MAPPINGS[category];
239
+ if (!mapping) return [];
240
+
241
+ // Return the Thryve sub-categories that this high-level category aggregates
242
+ return mapping.thryveCategories;
243
+ }