@etweisberg/garmin-connect-mcp 0.1.15 → 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 +29 -0
- 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
|
@@ -99,6 +99,14 @@ Session cookies expire after a few hours. Re-run the login flow when they do.
|
|
|
99
99
|
| `get-body-battery` | Body battery charged/drained values |
|
|
100
100
|
| `get-hrv` | Heart rate variability data |
|
|
101
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 |
|
|
109
|
+
|
|
102
110
|
### Weight / Records / Fitness
|
|
103
111
|
|
|
104
112
|
| Tool | Description |
|
|
@@ -108,8 +116,29 @@ Session cookies expire after a few hours. Re-run the login flow when they do.
|
|
|
108
116
|
| `get-fitness-stats` | Aggregated activity stats by type |
|
|
109
117
|
| `get-vo2max` | Latest VO2 Max estimate |
|
|
110
118
|
| `get-hr-zones-config` | Heart rate zone boundaries |
|
|
119
|
+
| `get-power-zones` | Power zone config for all sports |
|
|
111
120
|
| `get-user-profile` | User profile and settings |
|
|
112
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 |
|
|
141
|
+
|
|
113
142
|
## Architecture
|
|
114
143
|
|
|
115
144
|
```
|
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 () => {
|