@etweisberg/garmin-connect-mcp 0.1.14 → 0.1.16
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.
- package/README.md +84 -56
- package/dist/garmin-client.js +49 -0
- package/dist/test.js +148 -0
- package/dist/tools.js +168 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,51 +59,85 @@ Session cookies expire after a few hours. Re-run the login flow when they do.
|
|
|
59
59
|
## Available Tools
|
|
60
60
|
|
|
61
61
|
### Session & Auth
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
62
|
+
|
|
63
|
+
| Tool | Description |
|
|
64
|
+
| --------------- | --------------------------------------------------------- |
|
|
65
|
+
| `garmin-login` | Returns login instructions for the Playwright MCP browser |
|
|
66
|
+
| `check-session` | Validates the saved session is still active |
|
|
67
|
+
| `run-tests` | Returns a test plan to verify all tools work |
|
|
67
68
|
|
|
68
69
|
### Activities
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
|
72
|
-
| `
|
|
73
|
-
| `get-activity
|
|
74
|
-
| `get-activity-
|
|
75
|
-
| `get-activity-
|
|
76
|
-
| `get-activity-
|
|
77
|
-
| `get-activity-
|
|
78
|
-
| `
|
|
70
|
+
|
|
71
|
+
| Tool | Description |
|
|
72
|
+
| ----------------------- | -------------------------------------------------------- |
|
|
73
|
+
| `list-activities` | List activities with pagination |
|
|
74
|
+
| `get-activity` | Full activity summary (distance, duration, HR, calories) |
|
|
75
|
+
| `get-activity-details` | Time-series metrics (HR, cadence, elevation over time) |
|
|
76
|
+
| `get-activity-splits` | Lap/split data |
|
|
77
|
+
| `get-activity-hr-zones` | Heart rate time-in-zone breakdown |
|
|
78
|
+
| `get-activity-polyline` | Full-resolution GPS track |
|
|
79
|
+
| `get-activity-weather` | Weather conditions during activity |
|
|
80
|
+
| `download-fit` | Download original FIT file |
|
|
79
81
|
|
|
80
82
|
### Daily Health
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
|
84
|
-
| `get-daily-
|
|
85
|
-
| `get-daily-
|
|
86
|
-
| `get-daily-
|
|
87
|
-
| `get-daily-
|
|
88
|
-
| `get-daily-
|
|
89
|
-
| `get-daily-
|
|
83
|
+
|
|
84
|
+
| Tool | Description |
|
|
85
|
+
| ----------------------------- | -------------------------------------------- |
|
|
86
|
+
| `get-daily-summary` | Steps, calories, distance, intensity minutes |
|
|
87
|
+
| `get-daily-heart-rate` | Heart rate data throughout the day |
|
|
88
|
+
| `get-daily-stress` | Stress levels throughout the day |
|
|
89
|
+
| `get-daily-summary-chart` | Combined wellness chart data |
|
|
90
|
+
| `get-daily-intensity-minutes` | Intensity minutes for a date |
|
|
91
|
+
| `get-daily-movement` | Movement/activity data |
|
|
92
|
+
| `get-daily-respiration` | Respiration rate data |
|
|
90
93
|
|
|
91
94
|
### Sleep / Body Battery / HRV
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
|
95
|
+
|
|
96
|
+
| Tool | Description |
|
|
97
|
+
| ------------------ | ----------------------------------- |
|
|
98
|
+
| `get-sleep` | Sleep score, duration, stages, SpO2 |
|
|
95
99
|
| `get-body-battery` | Body battery charged/drained values |
|
|
96
|
-
| `get-hrv`
|
|
100
|
+
| `get-hrv` | Heart rate variability data |
|
|
101
|
+
|
|
102
|
+
### Training & Recovery
|
|
103
|
+
|
|
104
|
+
| Tool | Description |
|
|
105
|
+
| ------------------------ | ------------------------------------------------ |
|
|
106
|
+
| `get-training-readiness` | Training readiness score (sleep, recovery, load) |
|
|
107
|
+
| `get-sleep-stats` | Sleep statistics over a date range |
|
|
108
|
+
| `get-hydration` | Daily hydration/water intake data |
|
|
97
109
|
|
|
98
110
|
### Weight / Records / Fitness
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
|
102
|
-
| `get-
|
|
103
|
-
| `get-
|
|
104
|
-
| `get-
|
|
105
|
-
| `get-
|
|
106
|
-
| `get-
|
|
111
|
+
|
|
112
|
+
| Tool | Description |
|
|
113
|
+
| ---------------------- | ------------------------------------- |
|
|
114
|
+
| `get-weight` | Weight measurements over a date range |
|
|
115
|
+
| `get-personal-records` | All personal records with history |
|
|
116
|
+
| `get-fitness-stats` | Aggregated activity stats by type |
|
|
117
|
+
| `get-vo2max` | Latest VO2 Max estimate |
|
|
118
|
+
| `get-hr-zones-config` | Heart rate zone boundaries |
|
|
119
|
+
| `get-power-zones` | Power zone config for all sports |
|
|
120
|
+
| `get-user-profile` | User profile and settings |
|
|
121
|
+
|
|
122
|
+
### Calendar, Goals & Badges
|
|
123
|
+
|
|
124
|
+
| Tool | Description |
|
|
125
|
+
| ----------------------- | ------------------------------------------- |
|
|
126
|
+
| `get-calendar` | Monthly calendar with activities and events |
|
|
127
|
+
| `get-goals` | Active, future, or past fitness goals |
|
|
128
|
+
| `get-badges` | All earned badges/achievements |
|
|
129
|
+
| `get-badge-leaderboard` | Badge leaderboard among connections |
|
|
130
|
+
|
|
131
|
+
### Workouts
|
|
132
|
+
|
|
133
|
+
| Tool | Description |
|
|
134
|
+
| ---------------------- | -------------------------------------------------- |
|
|
135
|
+
| `list-workouts` | List saved workouts |
|
|
136
|
+
| `get-workout` | Get workout details (steps, segments) |
|
|
137
|
+
| `create-workout` | Create a new workout (warmup, intervals, cooldown) |
|
|
138
|
+
| `schedule-workout` | Schedule a workout to a date (syncs to device) |
|
|
139
|
+
| `delete-workout` | Delete a workout |
|
|
140
|
+
| `download-workout-fit` | Download a workout as a FIT file |
|
|
107
141
|
|
|
108
142
|
## Architecture
|
|
109
143
|
|
|
@@ -141,13 +175,13 @@ npm run build
|
|
|
141
175
|
|
|
142
176
|
### Scripts
|
|
143
177
|
|
|
144
|
-
| Command
|
|
145
|
-
|
|
146
|
-
| `npm run build`
|
|
147
|
-
| `npm run lint`
|
|
148
|
-
| `npm run format`
|
|
149
|
-
| `npm run typecheck` | Type check without emitting
|
|
150
|
-
| `npm test`
|
|
178
|
+
| Command | Description |
|
|
179
|
+
| ------------------- | ---------------------------------------------- |
|
|
180
|
+
| `npm run build` | Compile TypeScript |
|
|
181
|
+
| `npm run lint` | Run ESLint |
|
|
182
|
+
| `npm run format` | Format with Prettier |
|
|
183
|
+
| `npm run typecheck` | Type check without emitting |
|
|
184
|
+
| `npm test` | Run integration tests (requires valid session) |
|
|
151
185
|
|
|
152
186
|
### Local Integration Testing
|
|
153
187
|
|
|
@@ -159,7 +193,7 @@ npm test
|
|
|
159
193
|
|
|
160
194
|
## Contributing
|
|
161
195
|
|
|
162
|
-
1.
|
|
196
|
+
1. Create a feature branch off `main`
|
|
163
197
|
2. Make your changes
|
|
164
198
|
3. Run checks:
|
|
165
199
|
```bash
|
|
@@ -175,20 +209,14 @@ CI runs lint, format check, typecheck, and build on every PR. Integration tests
|
|
|
175
209
|
|
|
176
210
|
### Releasing
|
|
177
211
|
|
|
178
|
-
Releases are automated
|
|
212
|
+
Releases are fully automated. Every merge to `main` triggers the release workflow which:
|
|
179
213
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
214
|
+
1. Runs CI (lint, format, typecheck, build)
|
|
215
|
+
2. Bumps the patch version
|
|
216
|
+
3. Publishes to npm with provenance
|
|
217
|
+
4. Creates a GitHub Release
|
|
183
218
|
|
|
184
|
-
|
|
185
|
-
git push --follow-tags
|
|
186
|
-
|
|
187
|
-
# GitHub Actions will:
|
|
188
|
-
# 1. Build the package
|
|
189
|
-
# 2. Publish to npm with provenance
|
|
190
|
-
# 3. Create a GitHub Release
|
|
191
|
-
```
|
|
219
|
+
No manual version bumping or tagging needed — just merge your PR.
|
|
192
220
|
|
|
193
221
|
## License
|
|
194
222
|
|
package/dist/garmin-client.js
CHANGED
|
@@ -142,6 +142,55 @@ export class GarminClient {
|
|
|
142
142
|
}
|
|
143
143
|
return Buffer.from(result.data, "base64");
|
|
144
144
|
}
|
|
145
|
+
async post(path, body) {
|
|
146
|
+
await this.init();
|
|
147
|
+
const url = `/gc-api/${path}`;
|
|
148
|
+
const csrfToken = this.csrfToken;
|
|
149
|
+
const bodyStr = JSON.stringify(body);
|
|
150
|
+
const result = await this.page.evaluate(async ({ url, csrfToken, bodyStr, }) => {
|
|
151
|
+
const resp = await fetch(url, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: {
|
|
154
|
+
"connect-csrf-token": csrfToken,
|
|
155
|
+
"Content-Type": "application/json",
|
|
156
|
+
Accept: "application/json, */*",
|
|
157
|
+
},
|
|
158
|
+
body: bodyStr,
|
|
159
|
+
});
|
|
160
|
+
const text = await resp.text();
|
|
161
|
+
return { status: resp.status, body: text };
|
|
162
|
+
}, { url, csrfToken, bodyStr });
|
|
163
|
+
if (result.status === 204 || (result.status === 200 && !result.body)) {
|
|
164
|
+
return { noData: true, status: result.status, path };
|
|
165
|
+
}
|
|
166
|
+
if (result.status < 200 || result.status >= 300) {
|
|
167
|
+
throw new Error(`Garmin API ${result.status}: ${path} — ${result.body}`);
|
|
168
|
+
}
|
|
169
|
+
return JSON.parse(result.body);
|
|
170
|
+
}
|
|
171
|
+
async delete(path) {
|
|
172
|
+
await this.init();
|
|
173
|
+
const url = `/gc-api/${path}`;
|
|
174
|
+
const csrfToken = this.csrfToken;
|
|
175
|
+
const result = await this.page.evaluate(async ({ url, csrfToken }) => {
|
|
176
|
+
const resp = await fetch(url, {
|
|
177
|
+
method: "DELETE",
|
|
178
|
+
headers: {
|
|
179
|
+
"connect-csrf-token": csrfToken,
|
|
180
|
+
Accept: "*/*",
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
const text = await resp.text();
|
|
184
|
+
return { status: resp.status, body: text };
|
|
185
|
+
}, { url, csrfToken });
|
|
186
|
+
if (result.status === 204 || (result.status === 200 && !result.body)) {
|
|
187
|
+
return { noData: true, status: result.status, path };
|
|
188
|
+
}
|
|
189
|
+
if (result.status < 200 || result.status >= 300) {
|
|
190
|
+
throw new Error(`Garmin API ${result.status}: ${path} — ${result.body}`);
|
|
191
|
+
}
|
|
192
|
+
return result.body ? JSON.parse(result.body) : { success: true };
|
|
193
|
+
}
|
|
145
194
|
}
|
|
146
195
|
// Singleton client for reuse across tool calls
|
|
147
196
|
let _sharedClient = null;
|
package/dist/test.js
CHANGED
|
@@ -304,6 +304,154 @@ const tests = [
|
|
|
304
304
|
throw new Error("no zones returned");
|
|
305
305
|
},
|
|
306
306
|
},
|
|
307
|
+
// ── Training & Recovery ─────────────────────────────────────────
|
|
308
|
+
{
|
|
309
|
+
name: "get-training-readiness",
|
|
310
|
+
run: async (server) => {
|
|
311
|
+
const result = await callTool(server, "get-training-readiness");
|
|
312
|
+
if (result.isError)
|
|
313
|
+
throw new Error(getToolText(result));
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: "get-sleep-stats",
|
|
318
|
+
run: async (server) => {
|
|
319
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 86400000)
|
|
320
|
+
.toISOString()
|
|
321
|
+
.slice(0, 10);
|
|
322
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
323
|
+
const result = await callTool(server, "get-sleep-stats", {
|
|
324
|
+
startDate: sevenDaysAgo,
|
|
325
|
+
endDate: today,
|
|
326
|
+
});
|
|
327
|
+
if (result.isError)
|
|
328
|
+
throw new Error(getToolText(result));
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
// ── Calendar, Goals, Badges ────────────────────────────────────────
|
|
332
|
+
{
|
|
333
|
+
name: "get-calendar",
|
|
334
|
+
run: async (server) => {
|
|
335
|
+
const result = await callTool(server, "get-calendar", {
|
|
336
|
+
year: 2026,
|
|
337
|
+
month: 2,
|
|
338
|
+
});
|
|
339
|
+
if (result.isError)
|
|
340
|
+
throw new Error(getToolText(result));
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: "get-goals",
|
|
345
|
+
run: async (server) => {
|
|
346
|
+
const result = await callTool(server, "get-goals", { status: "active" });
|
|
347
|
+
if (result.isError)
|
|
348
|
+
throw new Error(getToolText(result));
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: "get-badges",
|
|
353
|
+
run: async (server) => {
|
|
354
|
+
const result = await callTool(server, "get-badges");
|
|
355
|
+
if (result.isError)
|
|
356
|
+
throw new Error(getToolText(result));
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: "get-badge-leaderboard",
|
|
361
|
+
run: async (server) => {
|
|
362
|
+
const result = await callTool(server, "get-badge-leaderboard", {
|
|
363
|
+
limit: 5,
|
|
364
|
+
});
|
|
365
|
+
if (result.isError)
|
|
366
|
+
throw new Error(getToolText(result));
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
// ── Hydration & Power Zones ────────────────────────────────────────
|
|
370
|
+
{
|
|
371
|
+
name: "get-hydration",
|
|
372
|
+
run: async (server) => {
|
|
373
|
+
const result = await callTool(server, "get-hydration");
|
|
374
|
+
if (result.isError)
|
|
375
|
+
throw new Error(getToolText(result));
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: "get-power-zones",
|
|
380
|
+
run: async (server) => {
|
|
381
|
+
const result = await callTool(server, "get-power-zones");
|
|
382
|
+
if (result.isError)
|
|
383
|
+
throw new Error(getToolText(result));
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
// ── Workouts ───────────────────────────────────────────────────────
|
|
387
|
+
{
|
|
388
|
+
name: "list-workouts",
|
|
389
|
+
run: async (server) => {
|
|
390
|
+
const result = await callTool(server, "list-workouts", {
|
|
391
|
+
start: 0,
|
|
392
|
+
limit: 5,
|
|
393
|
+
});
|
|
394
|
+
if (result.isError)
|
|
395
|
+
throw new Error(getToolText(result));
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: "workout CRUD (create -> schedule -> delete)",
|
|
400
|
+
run: async (server) => {
|
|
401
|
+
// Create
|
|
402
|
+
const workout = {
|
|
403
|
+
workoutName: "MCP Test Workout (safe to delete)",
|
|
404
|
+
sportType: { sportTypeId: 1, sportTypeKey: "running" },
|
|
405
|
+
workoutSegments: [
|
|
406
|
+
{
|
|
407
|
+
segmentOrder: 1,
|
|
408
|
+
sportType: { sportTypeId: 1, sportTypeKey: "running" },
|
|
409
|
+
workoutSteps: [
|
|
410
|
+
{
|
|
411
|
+
type: "ExecutableStepDTO",
|
|
412
|
+
stepOrder: 1,
|
|
413
|
+
stepType: { stepTypeId: 3, stepTypeKey: "interval" },
|
|
414
|
+
endCondition: {
|
|
415
|
+
conditionTypeId: 2,
|
|
416
|
+
conditionTypeKey: "time",
|
|
417
|
+
},
|
|
418
|
+
endConditionValue: 600,
|
|
419
|
+
targetType: {
|
|
420
|
+
workoutTargetTypeId: 1,
|
|
421
|
+
workoutTargetTypeKey: "no.target",
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
};
|
|
428
|
+
const createResult = await callTool(server, "create-workout", {
|
|
429
|
+
workout: JSON.stringify(workout),
|
|
430
|
+
});
|
|
431
|
+
if (createResult.isError)
|
|
432
|
+
throw new Error(getToolText(createResult));
|
|
433
|
+
const created = getToolJson(createResult);
|
|
434
|
+
if (!created.workoutId)
|
|
435
|
+
throw new Error("no workoutId returned");
|
|
436
|
+
const wid = String(created.workoutId);
|
|
437
|
+
// Schedule to tomorrow
|
|
438
|
+
const tomorrow = new Date(Date.now() + 86400000)
|
|
439
|
+
.toISOString()
|
|
440
|
+
.slice(0, 10);
|
|
441
|
+
const schedResult = await callTool(server, "schedule-workout", {
|
|
442
|
+
workoutId: wid,
|
|
443
|
+
date: tomorrow,
|
|
444
|
+
});
|
|
445
|
+
if (schedResult.isError)
|
|
446
|
+
throw new Error(getToolText(schedResult));
|
|
447
|
+
// Delete (cleanup)
|
|
448
|
+
const delResult = await callTool(server, "delete-workout", {
|
|
449
|
+
workoutId: wid,
|
|
450
|
+
});
|
|
451
|
+
if (delResult.isError)
|
|
452
|
+
throw new Error(getToolText(delResult));
|
|
453
|
+
},
|
|
454
|
+
},
|
|
307
455
|
];
|
|
308
456
|
// Resolved during bootstrap
|
|
309
457
|
let activityId = "";
|
package/dist/tools.js
CHANGED
|
@@ -336,6 +336,174 @@ To authenticate, you need the Playwright MCP server installed (\`@playwright/mcp
|
|
|
336
336
|
return jsonResult(data);
|
|
337
337
|
});
|
|
338
338
|
// ══════════════════════════════════════════════════════════════════
|
|
339
|
+
// Training & Recovery
|
|
340
|
+
// ══════════════════════════════════════════════════════════════════
|
|
341
|
+
server.tool("get-training-readiness", "Get training readiness score for a date (based on sleep, recovery, training load)", {
|
|
342
|
+
date: z.string().optional().describe("YYYY-MM-DD, defaults to today"),
|
|
343
|
+
}, async ({ date }) => {
|
|
344
|
+
const client = getClient();
|
|
345
|
+
const d = date ?? todayDate();
|
|
346
|
+
const data = await client.get(`metrics-service/metrics/trainingreadiness/${d}`);
|
|
347
|
+
return jsonResult(data);
|
|
348
|
+
});
|
|
349
|
+
server.tool("get-sleep-stats", "Get sleep statistics over a date range (averages, trends)", {
|
|
350
|
+
startDate: z.string().describe("Start date YYYY-MM-DD"),
|
|
351
|
+
endDate: z.string().describe("End date YYYY-MM-DD"),
|
|
352
|
+
}, async ({ startDate, endDate }) => {
|
|
353
|
+
const client = getClient();
|
|
354
|
+
const data = await client.get(`sleep-service/stats/sleep/daily/${startDate}/${endDate}`);
|
|
355
|
+
return jsonResult(data);
|
|
356
|
+
});
|
|
357
|
+
// ══════════════════════════════════════════════════════════════════
|
|
358
|
+
// Calendar, Goals, Badges
|
|
359
|
+
// ══════════════════════════════════════════════════════════════════
|
|
360
|
+
server.tool("get-calendar", "Get monthly calendar with activities, workouts, and events", {
|
|
361
|
+
year: z.number().describe("Year (e.g. 2026)"),
|
|
362
|
+
month: z.number().describe("Month number 0-11 (0=January, 11=December)"),
|
|
363
|
+
}, async ({ year, month }) => {
|
|
364
|
+
const client = getClient();
|
|
365
|
+
const data = await client.get(`calendar-service/year/${year}/month/${month}`);
|
|
366
|
+
return jsonResult(data);
|
|
367
|
+
});
|
|
368
|
+
server.tool("get-goals", "Get fitness goals", {
|
|
369
|
+
status: z
|
|
370
|
+
.string()
|
|
371
|
+
.default("active")
|
|
372
|
+
.describe("Goal status: active, future, or past"),
|
|
373
|
+
}, async ({ status }) => {
|
|
374
|
+
const client = getClient();
|
|
375
|
+
const data = await client.get("goal-service/goal/goals", { status });
|
|
376
|
+
return jsonResult(data);
|
|
377
|
+
});
|
|
378
|
+
server.tool("get-badges", "Get all earned badges/achievements", {}, async () => {
|
|
379
|
+
const client = getClient();
|
|
380
|
+
const data = await client.get("badge-service/badge/earned");
|
|
381
|
+
return jsonResult(data);
|
|
382
|
+
});
|
|
383
|
+
server.tool("get-badge-leaderboard", "Get badge leaderboard among your connections", {
|
|
384
|
+
limit: z.number().default(25).describe("Max entries to return"),
|
|
385
|
+
}, async ({ limit }) => {
|
|
386
|
+
const client = getClient();
|
|
387
|
+
const data = await client.get("badge-service/badge/leaderboard", {
|
|
388
|
+
limit,
|
|
389
|
+
});
|
|
390
|
+
return jsonResult(data);
|
|
391
|
+
});
|
|
392
|
+
// ══════════════════════════════════════════════════════════════════
|
|
393
|
+
// Hydration & Power Zones
|
|
394
|
+
// ══════════════════════════════════════════════════════════════════
|
|
395
|
+
server.tool("get-hydration", "Get daily hydration/water intake data", {
|
|
396
|
+
date: z.string().optional().describe("YYYY-MM-DD, defaults to today"),
|
|
397
|
+
}, async ({ date }) => {
|
|
398
|
+
const client = getClient();
|
|
399
|
+
const d = date ?? todayDate();
|
|
400
|
+
const data = await client.get(`usersummary-service/usersummary/hydration/allData/${d}`);
|
|
401
|
+
return jsonResult(data);
|
|
402
|
+
});
|
|
403
|
+
server.tool("get-power-zones", "Get power zone configuration for all sports", {}, async () => {
|
|
404
|
+
const client = getClient();
|
|
405
|
+
const data = await client.get("biometric-service/powerZones/sports/all");
|
|
406
|
+
return jsonResult(data);
|
|
407
|
+
});
|
|
408
|
+
// ══════════════════════════════════════════════════════════════════
|
|
409
|
+
// Workouts (read + write)
|
|
410
|
+
// ══════════════════════════════════════════════════════════════════
|
|
411
|
+
server.tool("list-workouts", "List your saved workouts", {
|
|
412
|
+
start: z.number().default(0).describe("Pagination offset"),
|
|
413
|
+
limit: z.number().default(100).describe("Max workouts to return"),
|
|
414
|
+
}, async ({ start, limit }) => {
|
|
415
|
+
const client = getClient();
|
|
416
|
+
const data = await client.get("workout-service/workouts", {
|
|
417
|
+
start,
|
|
418
|
+
limit,
|
|
419
|
+
});
|
|
420
|
+
return jsonResult(data);
|
|
421
|
+
});
|
|
422
|
+
server.tool("get-workout", "Get a single workout by ID with full step/segment details", {
|
|
423
|
+
workoutId: z.string().describe("The workout ID"),
|
|
424
|
+
}, async ({ workoutId }) => {
|
|
425
|
+
const client = getClient();
|
|
426
|
+
const data = await client.get(`workout-service/workout/${workoutId}`);
|
|
427
|
+
return jsonResult(data);
|
|
428
|
+
});
|
|
429
|
+
server.tool("download-workout-fit", "Download a workout as a FIT file", {
|
|
430
|
+
workoutId: z.string().describe("The workout ID"),
|
|
431
|
+
outputDir: z
|
|
432
|
+
.string()
|
|
433
|
+
.default("./fit_files")
|
|
434
|
+
.describe("Directory to save the FIT file"),
|
|
435
|
+
}, async ({ workoutId, outputDir }) => {
|
|
436
|
+
const client = getClient();
|
|
437
|
+
const fitBytes = await client.getBytes(`workout-service/workout/FIT/${workoutId}`);
|
|
438
|
+
mkdirSync(outputDir, { recursive: true });
|
|
439
|
+
const outPath = join(outputDir, `workout_${workoutId}.fit`);
|
|
440
|
+
writeFileSync(outPath, fitBytes);
|
|
441
|
+
return textResult(`Downloaded workout FIT: ${outPath} (${fitBytes.length} bytes)`);
|
|
442
|
+
});
|
|
443
|
+
server.tool("create-workout", `Create a new workout on Garmin Connect. Pass a workout JSON object with workoutName, sportType, and workoutSegments containing steps.
|
|
444
|
+
|
|
445
|
+
Sport type IDs: 1=running, 2=cycling, 3=swimming, 4=walking, 5=multi, 6=fitness, 7=hiking.
|
|
446
|
+
Step types: warmup (1), cooldown (2), interval (3), recovery (4), rest (5).
|
|
447
|
+
End conditions: distance (1, meters), time (2, seconds), open (7).
|
|
448
|
+
|
|
449
|
+
Example minimal running workout:
|
|
450
|
+
{
|
|
451
|
+
"workoutName": "Easy 30min Run",
|
|
452
|
+
"sportType": {"sportTypeId": 1, "sportTypeKey": "running"},
|
|
453
|
+
"workoutSegments": [{
|
|
454
|
+
"segmentOrder": 1,
|
|
455
|
+
"sportType": {"sportTypeId": 1, "sportTypeKey": "running"},
|
|
456
|
+
"workoutSteps": [{
|
|
457
|
+
"type": "ExecutableStepDTO",
|
|
458
|
+
"stepOrder": 1,
|
|
459
|
+
"stepType": {"stepTypeId": 1, "stepTypeKey": "warmup"},
|
|
460
|
+
"endCondition": {"conditionTypeId": 2, "conditionTypeKey": "time"},
|
|
461
|
+
"endConditionValue": 300,
|
|
462
|
+
"targetType": {"workoutTargetTypeId": 1, "workoutTargetTypeKey": "no.target"}
|
|
463
|
+
}, {
|
|
464
|
+
"type": "ExecutableStepDTO",
|
|
465
|
+
"stepOrder": 2,
|
|
466
|
+
"stepType": {"stepTypeId": 3, "stepTypeKey": "interval"},
|
|
467
|
+
"endCondition": {"conditionTypeId": 2, "conditionTypeKey": "time"},
|
|
468
|
+
"endConditionValue": 1200,
|
|
469
|
+
"targetType": {"workoutTargetTypeId": 1, "workoutTargetTypeKey": "no.target"}
|
|
470
|
+
}, {
|
|
471
|
+
"type": "ExecutableStepDTO",
|
|
472
|
+
"stepOrder": 3,
|
|
473
|
+
"stepType": {"stepTypeId": 2, "stepTypeKey": "cooldown"},
|
|
474
|
+
"endCondition": {"conditionTypeId": 2, "conditionTypeKey": "time"},
|
|
475
|
+
"endConditionValue": 300,
|
|
476
|
+
"targetType": {"workoutTargetTypeId": 1, "workoutTargetTypeKey": "no.target"}
|
|
477
|
+
}]
|
|
478
|
+
}]
|
|
479
|
+
}`, {
|
|
480
|
+
workout: z
|
|
481
|
+
.string()
|
|
482
|
+
.describe("JSON string of the workout object to create"),
|
|
483
|
+
}, async ({ workout }) => {
|
|
484
|
+
const client = getClient();
|
|
485
|
+
const workoutObj = JSON.parse(workout);
|
|
486
|
+
const data = await client.post("workout-service/workout", workoutObj);
|
|
487
|
+
return jsonResult(data);
|
|
488
|
+
});
|
|
489
|
+
server.tool("schedule-workout", "Schedule an existing workout to a date on your calendar. The workout will sync to your device.", {
|
|
490
|
+
workoutId: z.string().describe("The workout ID"),
|
|
491
|
+
date: z.string().describe("Date to schedule YYYY-MM-DD"),
|
|
492
|
+
}, async ({ workoutId, date }) => {
|
|
493
|
+
const client = getClient();
|
|
494
|
+
const data = await client.post(`workout-service/schedule/${workoutId}`, {
|
|
495
|
+
date,
|
|
496
|
+
});
|
|
497
|
+
return jsonResult(data);
|
|
498
|
+
});
|
|
499
|
+
server.tool("delete-workout", "Delete a workout from Garmin Connect", {
|
|
500
|
+
workoutId: z.string().describe("The workout ID to delete"),
|
|
501
|
+
}, async ({ workoutId }) => {
|
|
502
|
+
const client = getClient();
|
|
503
|
+
await client.delete(`workout-service/workout/${workoutId}`);
|
|
504
|
+
return textResult(`Workout ${workoutId} deleted`);
|
|
505
|
+
});
|
|
506
|
+
// ══════════════════════════════════════════════════════════════════
|
|
339
507
|
// Testing
|
|
340
508
|
// ══════════════════════════════════════════════════════════════════
|
|
341
509
|
server.tool("run-tests", "Returns a test plan for verifying all garmin-connect-mcp tools work. Call each tool listed and report results.", {}, async () => {
|