@etweisberg/garmin-connect-mcp 0.1.19 → 0.1.22

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.
@@ -0,0 +1,390 @@
1
+ // ── Workout Templates (MCP Resources) ──────────────────────────────────────
2
+ // Templates adapted from Taxuspt/garmin_mcp (MIT License, Copyright (c) 2025 Alexandre Domingues)
3
+ // https://github.com/Taxuspt/garmin_mcp/blob/main/src/garmin_mcp/workout_templates.py
4
+ const WORKOUT_TEMPLATES = {
5
+ "simple-run": {
6
+ workoutName: "Simple Run",
7
+ sportType: { sportTypeId: 1, sportTypeKey: "running" },
8
+ workoutSegments: [
9
+ {
10
+ segmentOrder: 1,
11
+ sportType: { sportTypeId: 1, sportTypeKey: "running" },
12
+ workoutSteps: [
13
+ {
14
+ type: "ExecutableStepDTO",
15
+ stepOrder: 1,
16
+ stepType: { stepTypeId: 1, stepTypeKey: "warmup" },
17
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
18
+ endConditionValue: 300.0,
19
+ targetType: {
20
+ workoutTargetTypeId: 1,
21
+ workoutTargetTypeKey: "no.target",
22
+ },
23
+ },
24
+ {
25
+ type: "ExecutableStepDTO",
26
+ stepOrder: 2,
27
+ stepType: { stepTypeId: 3, stepTypeKey: "interval" },
28
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
29
+ endConditionValue: 1800.0,
30
+ targetType: {
31
+ workoutTargetTypeId: 1,
32
+ workoutTargetTypeKey: "no.target",
33
+ },
34
+ },
35
+ {
36
+ type: "ExecutableStepDTO",
37
+ stepOrder: 3,
38
+ stepType: { stepTypeId: 2, stepTypeKey: "cooldown" },
39
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
40
+ endConditionValue: 300.0,
41
+ targetType: {
42
+ workoutTargetTypeId: 1,
43
+ workoutTargetTypeKey: "no.target",
44
+ },
45
+ },
46
+ ],
47
+ },
48
+ ],
49
+ },
50
+ "interval-running": {
51
+ workoutName: "Interval Running",
52
+ sportType: { sportTypeId: 1, sportTypeKey: "running" },
53
+ workoutSegments: [
54
+ {
55
+ segmentOrder: 1,
56
+ sportType: { sportTypeId: 1, sportTypeKey: "running" },
57
+ workoutSteps: [
58
+ {
59
+ type: "ExecutableStepDTO",
60
+ stepOrder: 1,
61
+ stepType: { stepTypeId: 1, stepTypeKey: "warmup" },
62
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
63
+ endConditionValue: 600.0,
64
+ targetType: {
65
+ workoutTargetTypeId: 1,
66
+ workoutTargetTypeKey: "no.target",
67
+ },
68
+ },
69
+ {
70
+ type: "RepeatGroupDTO",
71
+ stepOrder: 2,
72
+ numberOfIterations: 6,
73
+ workoutSteps: [
74
+ {
75
+ type: "ExecutableStepDTO",
76
+ stepOrder: 1,
77
+ stepType: { stepTypeId: 3, stepTypeKey: "interval" },
78
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
79
+ endConditionValue: 60.0,
80
+ targetType: {
81
+ workoutTargetTypeId: 4,
82
+ workoutTargetTypeKey: "heart.rate.zone",
83
+ },
84
+ zoneNumber: 5,
85
+ },
86
+ {
87
+ type: "ExecutableStepDTO",
88
+ stepOrder: 2,
89
+ stepType: { stepTypeId: 4, stepTypeKey: "recovery" },
90
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
91
+ endConditionValue: 90.0,
92
+ targetType: {
93
+ workoutTargetTypeId: 4,
94
+ workoutTargetTypeKey: "heart.rate.zone",
95
+ },
96
+ zoneNumber: 2,
97
+ },
98
+ ],
99
+ },
100
+ {
101
+ type: "ExecutableStepDTO",
102
+ stepOrder: 3,
103
+ stepType: { stepTypeId: 2, stepTypeKey: "cooldown" },
104
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
105
+ endConditionValue: 600.0,
106
+ targetType: {
107
+ workoutTargetTypeId: 1,
108
+ workoutTargetTypeKey: "no.target",
109
+ },
110
+ },
111
+ ],
112
+ },
113
+ ],
114
+ },
115
+ "tempo-run": {
116
+ workoutName: "Tempo Run",
117
+ sportType: { sportTypeId: 1, sportTypeKey: "running" },
118
+ workoutSegments: [
119
+ {
120
+ segmentOrder: 1,
121
+ sportType: { sportTypeId: 1, sportTypeKey: "running" },
122
+ workoutSteps: [
123
+ {
124
+ type: "ExecutableStepDTO",
125
+ stepOrder: 1,
126
+ stepType: { stepTypeId: 1, stepTypeKey: "warmup" },
127
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
128
+ endConditionValue: 600.0,
129
+ targetType: {
130
+ workoutTargetTypeId: 4,
131
+ workoutTargetTypeKey: "heart.rate.zone",
132
+ },
133
+ zoneNumber: 2,
134
+ },
135
+ {
136
+ type: "ExecutableStepDTO",
137
+ stepOrder: 2,
138
+ stepType: { stepTypeId: 3, stepTypeKey: "interval" },
139
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
140
+ endConditionValue: 1200.0,
141
+ targetType: {
142
+ workoutTargetTypeId: 4,
143
+ workoutTargetTypeKey: "heart.rate.zone",
144
+ },
145
+ zoneNumber: 4,
146
+ },
147
+ {
148
+ type: "ExecutableStepDTO",
149
+ stepOrder: 3,
150
+ stepType: { stepTypeId: 2, stepTypeKey: "cooldown" },
151
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
152
+ endConditionValue: 600.0,
153
+ targetType: {
154
+ workoutTargetTypeId: 4,
155
+ workoutTargetTypeKey: "heart.rate.zone",
156
+ },
157
+ zoneNumber: 2,
158
+ },
159
+ ],
160
+ },
161
+ ],
162
+ },
163
+ "strength-circuit": {
164
+ workoutName: "Strength Circuit",
165
+ description: "Strength training workout template",
166
+ sportType: { sportTypeId: 5, sportTypeKey: "strength_training" },
167
+ workoutSegments: [
168
+ {
169
+ segmentOrder: 1,
170
+ sportType: { sportTypeId: 5, sportTypeKey: "strength_training" },
171
+ workoutSteps: [
172
+ {
173
+ type: "ExecutableStepDTO",
174
+ stepOrder: 1,
175
+ stepType: { stepTypeId: 1, stepTypeKey: "warmup" },
176
+ description: "Warmup description",
177
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
178
+ endConditionValue: 600.0,
179
+ targetType: {
180
+ workoutTargetTypeId: 1,
181
+ workoutTargetTypeKey: "no.target",
182
+ },
183
+ category: "WARM_UP",
184
+ exerciseName: "",
185
+ },
186
+ {
187
+ type: "RepeatGroupDTO",
188
+ stepOrder: 2,
189
+ numberOfIterations: 3,
190
+ skipLastRestStep: true,
191
+ workoutSteps: [
192
+ {
193
+ type: "ExecutableStepDTO",
194
+ stepOrder: 1,
195
+ stepType: { stepTypeId: 3, stepTypeKey: "interval" },
196
+ description: "Bench press 115lb × 10 reps",
197
+ endCondition: { conditionTypeId: 10, conditionTypeKey: "reps" },
198
+ endConditionValue: 10,
199
+ targetType: {
200
+ workoutTargetTypeId: 1,
201
+ workoutTargetTypeKey: "no.target",
202
+ },
203
+ category: "BENCH_PRESS",
204
+ exerciseName: "",
205
+ weightValue: 115.0,
206
+ weightUnit: { unitId: 9, unitKey: "pound", factor: 453.59237 },
207
+ },
208
+ {
209
+ type: "ExecutableStepDTO",
210
+ stepOrder: 2,
211
+ stepType: { stepTypeId: 4, stepTypeKey: "recovery" },
212
+ description: "Transition to push-ups (60 sec)",
213
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
214
+ endConditionValue: 60.0,
215
+ targetType: {
216
+ workoutTargetTypeId: 1,
217
+ workoutTargetTypeKey: "no.target",
218
+ },
219
+ },
220
+ {
221
+ type: "ExecutableStepDTO",
222
+ stepOrder: 3,
223
+ stepType: { stepTypeId: 3, stepTypeKey: "interval" },
224
+ description: "Push-ups × 10 reps (bodyweight)",
225
+ endCondition: { conditionTypeId: 10, conditionTypeKey: "reps" },
226
+ endConditionValue: 10,
227
+ targetType: {
228
+ workoutTargetTypeId: 1,
229
+ workoutTargetTypeKey: "no.target",
230
+ },
231
+ category: "PUSH_UP",
232
+ exerciseName: "PUSH_UP",
233
+ weightValue: null,
234
+ weightUnit: null,
235
+ },
236
+ {
237
+ type: "ExecutableStepDTO",
238
+ stepOrder: 4,
239
+ stepType: { stepTypeId: 4, stepTypeKey: "recovery" },
240
+ description: "Transition to bicep curls (60 sec)",
241
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
242
+ endConditionValue: 60.0,
243
+ targetType: {
244
+ workoutTargetTypeId: 1,
245
+ workoutTargetTypeKey: "no.target",
246
+ },
247
+ },
248
+ {
249
+ type: "ExecutableStepDTO",
250
+ stepOrder: 5,
251
+ stepType: { stepTypeId: 3, stepTypeKey: "interval" },
252
+ description: "Bicep curls 25lb × 12 reps",
253
+ endCondition: { conditionTypeId: 10, conditionTypeKey: "reps" },
254
+ endConditionValue: 12,
255
+ targetType: {
256
+ workoutTargetTypeId: 1,
257
+ workoutTargetTypeKey: "no.target",
258
+ },
259
+ category: "CURL",
260
+ exerciseName: "ALTERNATING_DUMBBELL_BICEPS_CURL",
261
+ weightValue: 25.0,
262
+ weightUnit: { unitId: 9, unitKey: "pound", factor: 453.59237 },
263
+ },
264
+ {
265
+ type: "ExecutableStepDTO",
266
+ stepOrder: 6,
267
+ stepType: { stepTypeId: 4, stepTypeKey: "recovery" },
268
+ description: "Rest 2 min before next round",
269
+ endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
270
+ endConditionValue: 120.0,
271
+ targetType: {
272
+ workoutTargetTypeId: 1,
273
+ workoutTargetTypeKey: "no.target",
274
+ },
275
+ },
276
+ ],
277
+ },
278
+ ],
279
+ },
280
+ ],
281
+ },
282
+ };
283
+ const WORKOUT_STRUCTURE_REFERENCE = `# Garmin Connect Workout JSON Structure Reference
284
+
285
+ ## Top-level fields
286
+ - workoutName: string (required)
287
+ - sportType: { sportTypeId: number, sportTypeKey: string } (required)
288
+ - IDs: 1=running, 2=cycling, 3=swimming, 4=walking, 5=strength_training
289
+ - workoutSegments: array of segment objects (required)
290
+ - description: string (optional)
291
+
292
+ ## Segment fields
293
+ - segmentOrder: number (1-based, required)
294
+ - sportType: same as top-level (required)
295
+ - workoutSteps: array of step objects (required)
296
+
297
+ ## Step types
298
+
299
+ ### ExecutableStepDTO (regular steps)
300
+ - type: "ExecutableStepDTO" (required)
301
+ - stepOrder: number (1-based within the containing steps array)
302
+ - stepType: { stepTypeId: number, stepTypeKey: string }
303
+ - 1="warmup", 2="cooldown", 3="interval", 4="recovery", 5="rest"
304
+ - endCondition: { conditionTypeId: number, conditionTypeKey: string }
305
+ - 2="time" (endConditionValue in seconds)
306
+ - 3="distance" (endConditionValue in meters)
307
+ - 7="lap.button" (press lap button; no endConditionValue needed)
308
+ - 10="reps" (endConditionValue = rep count — use for strength exercises)
309
+ - endConditionValue: number (required for time/distance/reps conditions)
310
+ - targetType: { workoutTargetTypeId: number, workoutTargetTypeKey: string }
311
+ - 1="no.target"
312
+ - 2="speed" — use targetValueOne/targetValueTwo (m/s)
313
+ - 4="heart.rate.zone" — use zoneNumber (1-5), NOT targetValueOne/targetValueTwo
314
+ - 6="cadence" — use targetValueOne/targetValueTwo (steps per minute)
315
+ - 11="power.zone" — use zoneNumber
316
+ - zoneNumber: number 1-5 (for heart.rate.zone or power.zone targets only)
317
+ - targetValueOne: number (lower bound for speed/cadence ranges)
318
+ - targetValueTwo: number (upper bound for speed/cadence ranges)
319
+ - description: string (optional, displayed on device)
320
+
321
+ ### Strength exercise fields (interval steps in strength_training workouts)
322
+ - category: string — exercise category (e.g. "BENCH_PRESS", "SQUAT", "PUSH_UP", "CURL")
323
+ - exerciseName: string — specific variant (e.g. "PUSH_UP", "ALTERNATING_DUMBBELL_BICEPS_CURL"); use "" if only one variant
324
+ - weightValue: number | null — weight in pounds as float; null/omit for bodyweight
325
+ - weightUnit: { unitId: 9, unitKey: "pound", factor: 453.59237 } | null — omit/null for bodyweight
326
+ - Warmup steps: set category="WARM_UP", exerciseName=""
327
+ - Recovery/transition steps: omit category and exerciseName (or set to null)
328
+
329
+ Exercise names from FIT SDK (not yet fully validated against Connect API):
330
+ | Category | exerciseName variants |
331
+ |----------|-----------------------|
332
+ | BENCH_PRESS | BARBELL_BENCH_PRESS, DUMBBELL_BENCH_PRESS, INCLINE_BARBELL_BENCH_PRESS, INCLINE_DUMBBELL_BENCH_PRESS, DECLINE_BARBELL_BENCH_PRESS, CLOSE_GRIP_BARBELL_BENCH_PRESS |
333
+ | SQUAT | BARBELL_BACK_SQUAT, BARBELL_FRONT_SQUAT, BODYWEIGHT_SQUAT, DUMBBELL_SQUAT, GOBLET_SQUAT, HACK_SQUAT, LEG_PRESS, SUMO_SQUAT, WALL_SIT |
334
+ | DEADLIFT | BARBELL_DEADLIFT, DUMBBELL_DEADLIFT, ROMANIAN_DEADLIFT, SINGLE_LEG_DEADLIFT, STIFF_LEG_DEADLIFT, SUMO_DEADLIFT |
335
+ | LUNGE | BARBELL_LUNGE, BODYWEIGHT_LUNGE, DUMBBELL_LUNGE, FORWARD_LUNGE, LATERAL_LUNGE, REVERSE_LUNGE, WALKING_LUNGE |
336
+ | ROW | CABLE_ROW, DUMBBELL_ROW, INVERTED_ROW, SEATED_CABLE_ROW, SINGLE_ARM_DUMBBELL_ROW, T_BAR_ROW |
337
+ | PULL_UP | PULL_UP, CHIN_UP, WEIGHTED_PULL_UP, CLOSE_GRIP_PULLDOWN, BANDED_PULL_UPS |
338
+ | SHOULDER_PRESS | OVERHEAD_DUMBBELL_PRESS, BARBELL_SHOULDER_PRESS, DUMBBELL_SHOULDER_PRESS, ARNOLD_PRESS, ALTERNATING_DUMBBELL_SHOULDER_PRESS, SEATED_DUMBBELL_SHOULDER_PRESS |
339
+ | CURL | ALTERNATING_DUMBBELL_BICEPS_CURL, BARBELL_BICEP_CURL, CABLE_BICEP_CURL, CONCENTRATION_CURL, DUMBBELL_BICEP_CURL, HAMMER_CURL, INCLINE_DUMBBELL_BICEP_CURL |
340
+ | TRICEP_EXTENSION | BENCH_DIPS, CABLE_OVERHEAD_TRICEP_EXTENSION, DUMBBELL_LYING_TRICEP_EXTENSION, SKULL_CRUSHERS, TRICEP_PUSHDOWN, TRICEP_ROPE_PUSHDOWN |
341
+ | FLYE | CABLE_CROSSOVER, DUMBBELL_FLYE, INCLINE_DUMBBELL_FLYE |
342
+ | PUSH_UP | PUSH_UP, CLOSE_GRIP_PUSH_UP, DECLINE_PUSH_UP, INCLINE_PUSH_UP, PIKE_PUSH_UP, WEIGHTED_PUSH_UP |
343
+ | PLANK | PLANK, SIDE_PLANK, WEIGHTED_PLANK, PLANK_WITH_KNEE_TO_CHEST |
344
+ | CRUNCH | CRUNCH, BICYCLE_CRUNCH, CABLE_CRUNCH, REVERSE_CRUNCH, SITUP, WEIGHTED_CRUNCH |
345
+ | HIP_RAISE | BARBELL_HIP_THRUST, GLUTE_BRIDGE, HIP_RAISE, WEIGHTED_HIP_RAISE |
346
+ | CALF_RAISE | CALF_RAISE, DONKEY_CALF_RAISE, SEATED_CALF_RAISE |
347
+ | STEP_UP | BARBELL_STEP_UPS, BODYWEIGHT_STEP_UP, DUMBBELL_STEP_UPS |
348
+ | LATERAL_RAISE | ALTERNATING_LATERAL_RAISE, DUMBBELL_LATERAL_RAISE |
349
+ | CORE | TURKISH_GET_UP, DEAD_BUG, PALLOF_PRESS |
350
+ | WARM_UP | JUMPING_JACK, MOUNTAIN_CLIMBER |
351
+
352
+
353
+ ### RepeatGroupDTO (repeat blocks)
354
+ - type: "RepeatGroupDTO" (required)
355
+ - stepOrder: number (1-based within the containing steps array)
356
+ - numberOfIterations: number (how many times to repeat)
357
+ - skipLastRestStep: true — set this on strength circuits to skip the final rest after the last round
358
+ - workoutSteps: array of ExecutableStepDTO (the steps to repeat)
359
+ - stepOrder within this array is 1-based and independent of the parent
360
+
361
+ ## Notes
362
+ - NEVER use targetValueOne/targetValueTwo for heart rate zones — use zoneNumber instead.
363
+ Using targetValueOne/targetValueTwo with heart.rate.zone target type causes Garmin to
364
+ misinterpret the values as pace (m/s), resulting in impossible paces like ~11 sec/mile.
365
+ - RepeatGroupDTO cannot be nested inside another RepeatGroupDTO.
366
+ - All stepOrder values within the same array must be sequential starting from 1.
367
+ `;
368
+ export function registerResources(server) {
369
+ for (const [name, template] of Object.entries(WORKOUT_TEMPLATES)) {
370
+ const uri = `workout://templates/${name}`;
371
+ server.resource(name, uri, async (resourceUri) => ({
372
+ contents: [
373
+ {
374
+ uri: resourceUri.href,
375
+ mimeType: "application/json",
376
+ text: JSON.stringify(template, null, 2),
377
+ },
378
+ ],
379
+ }));
380
+ }
381
+ server.resource("workout-structure-reference", "workout://reference/structure", async (resourceUri) => ({
382
+ contents: [
383
+ {
384
+ uri: resourceUri.href,
385
+ mimeType: "text/markdown",
386
+ text: WORKOUT_STRUCTURE_REFERENCE,
387
+ },
388
+ ],
389
+ }));
390
+ }
package/dist/test.js CHANGED
@@ -98,8 +98,8 @@ const resourceTests = [
98
98
  const data = JSON.parse(content.text);
99
99
  if (data.workoutName !== "Strength Circuit")
100
100
  throw new Error(`unexpected workoutName: ${data.workoutName}`);
101
- if (data.sportType.sportTypeKey !== "fitness_equipment")
102
- throw new Error("expected fitness_equipment sport type");
101
+ if (data.sportType.sportTypeKey !== "strength_training")
102
+ throw new Error("expected strength_training sport type");
103
103
  // Must have a RepeatGroupDTO
104
104
  const steps = data.workoutSegments[0]?.workoutSteps ?? [];
105
105
  const hasRepeatGroup = steps.some((s) => s.type === "RepeatGroupDTO");
package/dist/tools.js CHANGED
@@ -3,6 +3,7 @@ import { writeFileSync, mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { inflateRawSync } from "node:zlib";
5
5
  import { getSharedClient, resetSharedClient, sessionExists, getSessionFile, } from "./garmin-client.js";
6
+ export { registerResources } from "./resources.js";
6
7
  function jsonResult(data) {
7
8
  return {
8
9
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
@@ -454,9 +455,9 @@ IMPORTANT: Step types must use Garmin's DTO format:
454
455
  IMPORTANT: For heart rate zone targets, use "zoneNumber" (1-5), NOT targetValueOne/targetValueTwo.
455
456
  targetValueOne/targetValueTwo are only for absolute value ranges (e.g. pace in m/s, power in watts).
456
457
 
457
- Sport type IDs: 1=running, 2=cycling, 3=swimming, 4=walking, 5=multi, 6=fitness, 7=hiking.
458
+ Sport type IDs: 1=running, 2=cycling, 3=swimming, 4=walking, 5=strength_training, 6=fitness_equipment, 7=hiking.
458
459
  Step type IDs: warmup (1), cooldown (2), interval (3), recovery (4), rest (5).
459
- End condition IDs: distance (1, value in meters), time (2, value in seconds), open (7, no value needed).
460
+ End condition IDs: time (2, value in seconds), distance (3, value in meters), lap.button (7, no value), reps (10, value = rep count — use for strength exercises).
460
461
  Target type IDs: no.target (1), speed (2, m/s range via targetValueOne/targetValueTwo), heart.rate.zone (4, use zoneNumber 1-5), power.zone (11, use zoneNumber).
461
462
 
462
463
  **Available Templates:**
@@ -628,307 +629,6 @@ Present results as a markdown table: | Tool | Status | Notes |
628
629
  Count total passed vs failed at the end.`);
629
630
  });
630
631
  }
631
- // ── Workout Templates (MCP Resources) ──────────────────────────────────────
632
- // Templates adapted from Taxuspt/garmin_mcp (MIT License, Copyright (c) 2025 Alexandre Domingues)
633
- // https://github.com/Taxuspt/garmin_mcp/blob/main/src/garmin_mcp/workout_templates.py
634
- const WORKOUT_TEMPLATES = {
635
- "simple-run": {
636
- workoutName: "Simple Run",
637
- sportType: { sportTypeId: 1, sportTypeKey: "running" },
638
- workoutSegments: [
639
- {
640
- segmentOrder: 1,
641
- sportType: { sportTypeId: 1, sportTypeKey: "running" },
642
- workoutSteps: [
643
- {
644
- type: "ExecutableStepDTO",
645
- stepOrder: 1,
646
- stepType: { stepTypeId: 1, stepTypeKey: "warmup" },
647
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
648
- endConditionValue: 300.0,
649
- targetType: {
650
- workoutTargetTypeId: 1,
651
- workoutTargetTypeKey: "no.target",
652
- },
653
- },
654
- {
655
- type: "ExecutableStepDTO",
656
- stepOrder: 2,
657
- stepType: { stepTypeId: 3, stepTypeKey: "interval" },
658
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
659
- endConditionValue: 1800.0,
660
- targetType: {
661
- workoutTargetTypeId: 1,
662
- workoutTargetTypeKey: "no.target",
663
- },
664
- },
665
- {
666
- type: "ExecutableStepDTO",
667
- stepOrder: 3,
668
- stepType: { stepTypeId: 2, stepTypeKey: "cooldown" },
669
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
670
- endConditionValue: 300.0,
671
- targetType: {
672
- workoutTargetTypeId: 1,
673
- workoutTargetTypeKey: "no.target",
674
- },
675
- },
676
- ],
677
- },
678
- ],
679
- },
680
- "interval-running": {
681
- workoutName: "Interval Running",
682
- sportType: { sportTypeId: 1, sportTypeKey: "running" },
683
- workoutSegments: [
684
- {
685
- segmentOrder: 1,
686
- sportType: { sportTypeId: 1, sportTypeKey: "running" },
687
- workoutSteps: [
688
- {
689
- type: "ExecutableStepDTO",
690
- stepOrder: 1,
691
- stepType: { stepTypeId: 1, stepTypeKey: "warmup" },
692
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
693
- endConditionValue: 600.0,
694
- targetType: {
695
- workoutTargetTypeId: 1,
696
- workoutTargetTypeKey: "no.target",
697
- },
698
- },
699
- {
700
- type: "RepeatGroupDTO",
701
- stepOrder: 2,
702
- numberOfIterations: 6,
703
- workoutSteps: [
704
- {
705
- type: "ExecutableStepDTO",
706
- stepOrder: 1,
707
- stepType: { stepTypeId: 3, stepTypeKey: "interval" },
708
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
709
- endConditionValue: 60.0,
710
- targetType: {
711
- workoutTargetTypeId: 4,
712
- workoutTargetTypeKey: "heart.rate.zone",
713
- },
714
- zoneNumber: 5,
715
- },
716
- {
717
- type: "ExecutableStepDTO",
718
- stepOrder: 2,
719
- stepType: { stepTypeId: 4, stepTypeKey: "recovery" },
720
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
721
- endConditionValue: 90.0,
722
- targetType: {
723
- workoutTargetTypeId: 4,
724
- workoutTargetTypeKey: "heart.rate.zone",
725
- },
726
- zoneNumber: 2,
727
- },
728
- ],
729
- },
730
- {
731
- type: "ExecutableStepDTO",
732
- stepOrder: 3,
733
- stepType: { stepTypeId: 2, stepTypeKey: "cooldown" },
734
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
735
- endConditionValue: 600.0,
736
- targetType: {
737
- workoutTargetTypeId: 1,
738
- workoutTargetTypeKey: "no.target",
739
- },
740
- },
741
- ],
742
- },
743
- ],
744
- },
745
- "tempo-run": {
746
- workoutName: "Tempo Run",
747
- sportType: { sportTypeId: 1, sportTypeKey: "running" },
748
- workoutSegments: [
749
- {
750
- segmentOrder: 1,
751
- sportType: { sportTypeId: 1, sportTypeKey: "running" },
752
- workoutSteps: [
753
- {
754
- type: "ExecutableStepDTO",
755
- stepOrder: 1,
756
- stepType: { stepTypeId: 1, stepTypeKey: "warmup" },
757
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
758
- endConditionValue: 600.0,
759
- targetType: {
760
- workoutTargetTypeId: 4,
761
- workoutTargetTypeKey: "heart.rate.zone",
762
- },
763
- zoneNumber: 2,
764
- },
765
- {
766
- type: "ExecutableStepDTO",
767
- stepOrder: 2,
768
- stepType: { stepTypeId: 3, stepTypeKey: "interval" },
769
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
770
- endConditionValue: 1200.0,
771
- targetType: {
772
- workoutTargetTypeId: 4,
773
- workoutTargetTypeKey: "heart.rate.zone",
774
- },
775
- zoneNumber: 4,
776
- },
777
- {
778
- type: "ExecutableStepDTO",
779
- stepOrder: 3,
780
- stepType: { stepTypeId: 2, stepTypeKey: "cooldown" },
781
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
782
- endConditionValue: 600.0,
783
- targetType: {
784
- workoutTargetTypeId: 4,
785
- workoutTargetTypeKey: "heart.rate.zone",
786
- },
787
- zoneNumber: 2,
788
- },
789
- ],
790
- },
791
- ],
792
- },
793
- "strength-circuit": {
794
- workoutName: "Strength Circuit",
795
- sportType: { sportTypeId: 6, sportTypeKey: "fitness_equipment" },
796
- workoutSegments: [
797
- {
798
- segmentOrder: 1,
799
- sportType: { sportTypeId: 6, sportTypeKey: "fitness_equipment" },
800
- workoutSteps: [
801
- {
802
- type: "ExecutableStepDTO",
803
- stepOrder: 1,
804
- stepType: { stepTypeId: 1, stepTypeKey: "warmup" },
805
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
806
- endConditionValue: 300.0,
807
- targetType: {
808
- workoutTargetTypeId: 1,
809
- workoutTargetTypeKey: "no.target",
810
- },
811
- },
812
- {
813
- type: "RepeatGroupDTO",
814
- stepOrder: 2,
815
- numberOfIterations: 3,
816
- workoutSteps: [
817
- {
818
- type: "ExecutableStepDTO",
819
- stepOrder: 1,
820
- stepType: { stepTypeId: 3, stepTypeKey: "interval" },
821
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
822
- endConditionValue: 40.0,
823
- targetType: {
824
- workoutTargetTypeId: 1,
825
- workoutTargetTypeKey: "no.target",
826
- },
827
- description: "Exercise (e.g. push-ups, squats, rows)",
828
- },
829
- {
830
- type: "ExecutableStepDTO",
831
- stepOrder: 2,
832
- stepType: { stepTypeId: 5, stepTypeKey: "rest" },
833
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
834
- endConditionValue: 20.0,
835
- targetType: {
836
- workoutTargetTypeId: 1,
837
- workoutTargetTypeKey: "no.target",
838
- },
839
- },
840
- ],
841
- },
842
- {
843
- type: "ExecutableStepDTO",
844
- stepOrder: 3,
845
- stepType: { stepTypeId: 2, stepTypeKey: "cooldown" },
846
- endCondition: { conditionTypeId: 2, conditionTypeKey: "time" },
847
- endConditionValue: 300.0,
848
- targetType: {
849
- workoutTargetTypeId: 1,
850
- workoutTargetTypeKey: "no.target",
851
- },
852
- },
853
- ],
854
- },
855
- ],
856
- },
857
- };
858
- const WORKOUT_STRUCTURE_REFERENCE = `# Garmin Connect Workout JSON Structure Reference
859
-
860
- ## Top-level fields
861
- - workoutName: string (required)
862
- - sportType: { sportTypeId: number, sportTypeKey: string } (required)
863
- - IDs: 1=running, 2=cycling, 3=swimming, 4=walking, 5=multi, 6=fitness_equipment, 7=hiking
864
- - workoutSegments: array of segment objects (required)
865
- - description: string (optional)
866
-
867
- ## Segment fields
868
- - segmentOrder: number (1-based, required)
869
- - sportType: same as top-level (required)
870
- - workoutSteps: array of step objects (required)
871
-
872
- ## Step types
873
-
874
- ### ExecutableStepDTO (regular steps)
875
- - type: "ExecutableStepDTO" (required)
876
- - stepOrder: number (1-based within the containing steps array)
877
- - stepType: { stepTypeId: number, stepTypeKey: string }
878
- - 1="warmup", 2="cooldown", 3="interval", 4="recovery", 5="rest"
879
- - endCondition: { conditionTypeId: number, conditionTypeKey: string }
880
- - 1="distance" (endConditionValue in meters)
881
- - 2="time" (endConditionValue in seconds)
882
- - 7="lap.button" (press lap button; no endConditionValue needed)
883
- - endConditionValue: number (required for distance/time conditions)
884
- - targetType: { workoutTargetTypeId: number, workoutTargetTypeKey: string }
885
- - 1="no.target"
886
- - 2="speed" — use targetValueOne/targetValueTwo (m/s)
887
- - 4="heart.rate.zone" — use zoneNumber (1-5), NOT targetValueOne/targetValueTwo
888
- - 6="cadence" — use targetValueOne/targetValueTwo (steps per minute)
889
- - 11="power.zone" — use zoneNumber
890
- - zoneNumber: number 1-5 (for heart.rate.zone or power.zone targets only)
891
- - targetValueOne: number (lower bound for speed/cadence ranges)
892
- - targetValueTwo: number (upper bound for speed/cadence ranges)
893
- - description: string (optional, displayed on device)
894
-
895
- ### RepeatGroupDTO (repeat blocks)
896
- - type: "RepeatGroupDTO" (required)
897
- - stepOrder: number (1-based within the containing steps array)
898
- - numberOfIterations: number (how many times to repeat)
899
- - workoutSteps: array of ExecutableStepDTO (the steps to repeat)
900
- - stepOrder within this array is 1-based and independent of the parent
901
-
902
- ## Notes
903
- - NEVER use targetValueOne/targetValueTwo for heart rate zones — use zoneNumber instead.
904
- Using targetValueOne/targetValueTwo with heart.rate.zone target type causes Garmin to
905
- misinterpret the values as pace (m/s), resulting in impossible paces like ~11 sec/mile.
906
- - RepeatGroupDTO cannot be nested inside another RepeatGroupDTO.
907
- - All stepOrder values within the same array must be sequential starting from 1.
908
- `;
909
- export function registerResources(server) {
910
- for (const [name, template] of Object.entries(WORKOUT_TEMPLATES)) {
911
- const uri = `workout://templates/${name}`;
912
- server.resource(name, uri, async (resourceUri) => ({
913
- contents: [
914
- {
915
- uri: resourceUri.href,
916
- mimeType: "application/json",
917
- text: JSON.stringify(template, null, 2),
918
- },
919
- ],
920
- }));
921
- }
922
- server.resource("workout-structure-reference", "workout://reference/structure", async (resourceUri) => ({
923
- contents: [
924
- {
925
- uri: resourceUri.href,
926
- mimeType: "text/markdown",
927
- text: WORKOUT_STRUCTURE_REFERENCE,
928
- },
929
- ],
930
- }));
931
- }
932
632
  /**
933
633
  * Minimal zip extraction — finds the first .fit file using the central
934
634
  * directory (which always has correct sizes, unlike local headers that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etweisberg/garmin-connect-mcp",
3
- "version": "0.1.19",
3
+ "version": "0.1.22",
4
4
  "description": "MCP server for Garmin Connect — access activities, metrics, and FIT files via Claude Code",
5
5
  "type": "module",
6
6
  "bin": {