@trainheroic-unofficial/athlete-mcp 0.6.5 → 1.1.0
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/dist/server.mjs +192 -6
- package/package.json +3 -3
package/dist/server.mjs
CHANGED
|
@@ -152,6 +152,14 @@ z.object({
|
|
|
152
152
|
endDate: dateString
|
|
153
153
|
});
|
|
154
154
|
/**
|
|
155
|
+
* One entered set: the value of each parameter slot (param 1 / param 2 — e.g. reps / weight).
|
|
156
|
+
* Shared by every logging write so the per-set shape is defined once.
|
|
157
|
+
*/
|
|
158
|
+
const loggedSetSchema = z.object({
|
|
159
|
+
param1: z.union([z.number(), z.string()]).optional(),
|
|
160
|
+
param2: z.union([z.number(), z.string()]).optional()
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
155
163
|
* Args for the set-logging write. `date` (the workout's day) locates the saved
|
|
156
164
|
* workout via the range endpoint; `savedWorkoutSetId` picks the set to complete; `results`
|
|
157
165
|
* gives, per exercise in it, the entered value of each set (param 1 / param 2 by entry slot).
|
|
@@ -161,13 +169,25 @@ const logSetArgsSchema = z.object({
|
|
|
161
169
|
savedWorkoutSetId: idArgSchema,
|
|
162
170
|
results: z.array(z.object({
|
|
163
171
|
savedWorkoutSetExerciseId: idArgSchema,
|
|
164
|
-
sets: z.array(
|
|
165
|
-
param1: z.union([z.number(), z.string()]).optional(),
|
|
166
|
-
param2: z.union([z.number(), z.string()]).optional()
|
|
167
|
-
})).min(1)
|
|
172
|
+
sets: z.array(loggedSetSchema).min(1)
|
|
168
173
|
})).min(1)
|
|
169
174
|
});
|
|
170
175
|
logSetArgsSchema.extend({ athleteId: idArgSchema });
|
|
176
|
+
/**
|
|
177
|
+
* Args for logging a whole session by exercise (rather than by saved-workout-set id). Each
|
|
178
|
+
* exercise carries its entered sets and an optional 1-based `order`. The athlete path creates
|
|
179
|
+
* or reuses a personal session for the date and logs against it; the coach path resolves each
|
|
180
|
+
* exercise to a set already prescribed on that date and logs against that.
|
|
181
|
+
*/
|
|
182
|
+
const logSessionArgsSchema = z.object({
|
|
183
|
+
date: dateString,
|
|
184
|
+
exercises: z.array(z.object({
|
|
185
|
+
exerciseId: idArgSchema,
|
|
186
|
+
order: z.number().int().positive().optional(),
|
|
187
|
+
sets: z.array(loggedSetSchema).min(1)
|
|
188
|
+
})).min(1)
|
|
189
|
+
});
|
|
190
|
+
logSessionArgsSchema.extend({ athleteId: idArgSchema });
|
|
171
191
|
z.looseObject({
|
|
172
192
|
title: z.string().min(1),
|
|
173
193
|
param_1_type: z.number().optional(),
|
|
@@ -654,6 +674,15 @@ function fetchExerciseStats(client, exerciseId, userId, date) {
|
|
|
654
674
|
function fetchAthleteWorkouts(client, startDate, endDate) {
|
|
655
675
|
return getArray(client, `/3.0/athlete/programworkout/range?startDate=${startDate}&endDate=${endDate}`, "athlete workouts");
|
|
656
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* A coach's view of a roster athlete's scheduled + completed workouts in an inclusive
|
|
679
|
+
* YYYY-MM-DD window (`/3.0/coach/athlete/programworkout/range/{athleteId}`). Returns the same
|
|
680
|
+
* `ProgramWorkout[]` shape as `fetchAthleteWorkouts`, so the same presenters and
|
|
681
|
+
* `findSavedWorkoutSet` apply — it just reads another athlete's data through the coach surface.
|
|
682
|
+
*/
|
|
683
|
+
function fetchCoachAthleteWorkouts(client, athleteId, startDate, endDate) {
|
|
684
|
+
return getArray(client, `/3.0/coach/athlete/programworkout/range/${athleteId}?startDate=${startDate}&endDate=${endDate}`, "coach athlete workouts");
|
|
685
|
+
}
|
|
657
686
|
function fetchLeaderboard(client, workoutId, opts = {}) {
|
|
658
687
|
const qs = new URLSearchParams();
|
|
659
688
|
if (opts.page !== void 0) qs.set("page", String(opts.page));
|
|
@@ -988,6 +1017,23 @@ async function logAthleteSet(client, args) {
|
|
|
988
1017
|
return writeSetResults(client, { role: "athlete" }, await fetchAthleteWorkouts(client, args.date, args.date), args.savedWorkoutSetId, args.results);
|
|
989
1018
|
}
|
|
990
1019
|
/**
|
|
1020
|
+
* Coach "Log for Athlete": record set results for a roster athlete on their behalf, via the
|
|
1021
|
+
* coach surface — `PUT /1.0/coach/savedworkoutsetexercise/{id}/{athleteId}` (the data write)
|
|
1022
|
+
* then `PUT /1.0/coach/savedworkoutset/{id}/{athleteId}` (mark complete). Same two-step
|
|
1023
|
+
* contract as {@link logAthleteSet}; the bodies are identical except each is stamped with
|
|
1024
|
+
* `athleteId`. The day is located through the coach range endpoint for that athlete.
|
|
1025
|
+
*
|
|
1026
|
+
* NOTE: TrainHeroic's seeded *demo* athletes are read-only for results and return 401 on the
|
|
1027
|
+
* data-write step; real (invited) athletes accept it.
|
|
1028
|
+
*/
|
|
1029
|
+
async function logForAthlete(client, args) {
|
|
1030
|
+
const workouts = await fetchCoachAthleteWorkouts(client, args.athleteId, args.date, args.date);
|
|
1031
|
+
return writeSetResults(client, {
|
|
1032
|
+
role: "coach",
|
|
1033
|
+
athleteId: args.athleteId
|
|
1034
|
+
}, workouts, args.savedWorkoutSetId, args.results);
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
991
1037
|
* Shared two-step set-log write behind {@link logAthleteSet} and {@link logForAthlete}.
|
|
992
1038
|
* `target` selects the surface: `athlete` writes `/1.0/athlete/...`; `coach` writes
|
|
993
1039
|
* `/1.0/coach/...{athleteId}` and stamps `athleteId` into each body. Step 1 PUTs each
|
|
@@ -1073,6 +1119,114 @@ async function addExercisesToWorkout(client, workoutId, exercises) {
|
|
|
1073
1119
|
if (!res.ok) throw new Error(`Add exercises to workout failed (HTTP ${res.status}).`);
|
|
1074
1120
|
return res.data;
|
|
1075
1121
|
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Find a personal-calendar session already on the given day. The range marks these with
|
|
1124
|
+
* `personal_cal === true`; the addable id is the program workout's `workout_id`. Returns the
|
|
1125
|
+
* first one's workoutId, or null when the day has no personal session.
|
|
1126
|
+
*/
|
|
1127
|
+
function findPersonalSessionWorkoutId(workouts) {
|
|
1128
|
+
for (const pw of workouts) {
|
|
1129
|
+
const rec = pw;
|
|
1130
|
+
if (rec.personal_cal === true) {
|
|
1131
|
+
const workoutId = coerceInt(rec.workout_id);
|
|
1132
|
+
if (workoutId !== null) return workoutId;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Map the `addExercisesToWorkout` response (an array of saved sets) to per-exercise saved ids.
|
|
1139
|
+
* Each set's `id` is a savedWorkoutSetId; each `savedWorkoutSetExercises[].id` is a
|
|
1140
|
+
* savedWorkoutSetExerciseId and `.exerciseId` is the catalog exercise id that was added.
|
|
1141
|
+
*/
|
|
1142
|
+
function indexAddedExercises(added) {
|
|
1143
|
+
const out = [];
|
|
1144
|
+
const sets = Array.isArray(added) ? added : [];
|
|
1145
|
+
for (const s of sets) {
|
|
1146
|
+
if (!isRecord(s)) continue;
|
|
1147
|
+
const savedWorkoutSetId = coerceInt(s.id);
|
|
1148
|
+
if (savedWorkoutSetId === null) continue;
|
|
1149
|
+
const exercises = Array.isArray(s.savedWorkoutSetExercises) ? s.savedWorkoutSetExercises : [];
|
|
1150
|
+
for (const ex of exercises) {
|
|
1151
|
+
if (!isRecord(ex)) continue;
|
|
1152
|
+
const savedWorkoutSetExerciseId = coerceInt(ex.id);
|
|
1153
|
+
const exerciseId = coerceInt(ex.exerciseId ?? ex.exercise_id);
|
|
1154
|
+
if (savedWorkoutSetExerciseId === null || exerciseId === null) continue;
|
|
1155
|
+
out.push({
|
|
1156
|
+
exerciseId,
|
|
1157
|
+
savedWorkoutSetId,
|
|
1158
|
+
savedWorkoutSetExerciseId
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return out;
|
|
1163
|
+
}
|
|
1164
|
+
/** Group resolved exercises by saved set and write each set via the given log target. */
|
|
1165
|
+
async function logResolvedExercises(client, target, date, resolved) {
|
|
1166
|
+
const bySet = /* @__PURE__ */ new Map();
|
|
1167
|
+
for (const r of resolved) {
|
|
1168
|
+
const list = bySet.get(r.savedWorkoutSetId) ?? [];
|
|
1169
|
+
list.push({
|
|
1170
|
+
savedWorkoutSetExerciseId: r.savedWorkoutSetExerciseId,
|
|
1171
|
+
sets: [...r.sets]
|
|
1172
|
+
});
|
|
1173
|
+
bySet.set(r.savedWorkoutSetId, list);
|
|
1174
|
+
}
|
|
1175
|
+
const out = [];
|
|
1176
|
+
for (const [savedWorkoutSetId, results] of bySet) {
|
|
1177
|
+
const written = target.role === "coach" ? await logForAthlete(client, {
|
|
1178
|
+
athleteId: target.athleteId,
|
|
1179
|
+
date,
|
|
1180
|
+
savedWorkoutSetId,
|
|
1181
|
+
results
|
|
1182
|
+
}) : await logAthleteSet(client, {
|
|
1183
|
+
date,
|
|
1184
|
+
savedWorkoutSetId,
|
|
1185
|
+
results
|
|
1186
|
+
});
|
|
1187
|
+
out.push(written);
|
|
1188
|
+
}
|
|
1189
|
+
return out;
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Log a whole session for the logged-in athlete by exercise, with no pre-existing prescription
|
|
1193
|
+
* required. Reuses a personal session already on the date when one exists (the API marks these
|
|
1194
|
+
* `personal_cal`), otherwise creates one; then adds the exercises and logs their entered sets.
|
|
1195
|
+
* This is the "log whatever I just did" path — extra accessory work, a makeup lift, an off-plan
|
|
1196
|
+
* gym session.
|
|
1197
|
+
*/
|
|
1198
|
+
async function logAdHocSession(client, args) {
|
|
1199
|
+
if (args.exercises.length === 0) throw new Error("Provide at least one exercise to log.");
|
|
1200
|
+
const existing = findPersonalSessionWorkoutId(await fetchAthleteWorkouts(client, args.date, args.date));
|
|
1201
|
+
const created = existing === null;
|
|
1202
|
+
const workoutId = existing ?? (await createPersonalWorkout(client, args.date)).workoutId;
|
|
1203
|
+
const withOrder = args.exercises.map((e, i) => ({
|
|
1204
|
+
...e,
|
|
1205
|
+
order: e.order ?? i + 1
|
|
1206
|
+
}));
|
|
1207
|
+
const index = indexAddedExercises(await addExercisesToWorkout(client, workoutId, withOrder.map((e) => ({
|
|
1208
|
+
exerciseId: e.exerciseId,
|
|
1209
|
+
order: e.order
|
|
1210
|
+
}))));
|
|
1211
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
1212
|
+
const resolved = withOrder.map((e) => {
|
|
1213
|
+
const hit = index.find((m) => m.exerciseId === e.exerciseId && !consumed.has(m.savedWorkoutSetExerciseId));
|
|
1214
|
+
if (!hit) throw new Error(`Could not place exercise ${e.exerciseId} into the session after adding it. The add-exercises response did not return a saved set for it.`);
|
|
1215
|
+
consumed.add(hit.savedWorkoutSetExerciseId);
|
|
1216
|
+
return {
|
|
1217
|
+
exerciseId: e.exerciseId,
|
|
1218
|
+
savedWorkoutSetId: hit.savedWorkoutSetId,
|
|
1219
|
+
savedWorkoutSetExerciseId: hit.savedWorkoutSetExerciseId,
|
|
1220
|
+
sets: e.sets
|
|
1221
|
+
};
|
|
1222
|
+
});
|
|
1223
|
+
const sets = await logResolvedExercises(client, { role: "athlete" }, args.date, resolved);
|
|
1224
|
+
return {
|
|
1225
|
+
date: args.date,
|
|
1226
|
+
created,
|
|
1227
|
+
sets
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1076
1230
|
/** Flatten `/v5/exercises/{id}/history` into PRs + a session time-series. */
|
|
1077
1231
|
function presentExerciseHistory(detail) {
|
|
1078
1232
|
return {
|
|
@@ -1280,8 +1434,40 @@ function registerSessionTools(server, ctx) {
|
|
|
1280
1434
|
return jsonResult(await addExercisesToWorkout(ctx.client, toId(workoutId), mapped), { hint: "Each top-level id is a savedWorkoutSetId; savedWorkoutSetExercises[].id is the savedWorkoutSetExerciseId for athlete_log_set." });
|
|
1281
1435
|
}));
|
|
1282
1436
|
}
|
|
1283
|
-
/**
|
|
1437
|
+
/** Map validated logSession exercises to the SDK's SessionExercise[] (ids coerced, slots trimmed). */
|
|
1438
|
+
function mapSessionExercises(exercises) {
|
|
1439
|
+
return exercises.map((e) => {
|
|
1440
|
+
const sets = e.sets.map((s) => {
|
|
1441
|
+
const slot = {};
|
|
1442
|
+
if (s.param1 !== void 0) slot.param1 = s.param1;
|
|
1443
|
+
if (s.param2 !== void 0) slot.param2 = s.param2;
|
|
1444
|
+
return slot;
|
|
1445
|
+
});
|
|
1446
|
+
const mapped = {
|
|
1447
|
+
exerciseId: toId(e.exerciseId),
|
|
1448
|
+
sets
|
|
1449
|
+
};
|
|
1450
|
+
if (e.order !== void 0) mapped.order = e.order;
|
|
1451
|
+
return mapped;
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
/** The gated set-logging writes (by saved-set id, and the by-exercise ad-hoc session). */
|
|
1284
1455
|
function registerLogTool(server, ctx) {
|
|
1456
|
+
server.registerTool("athlete_log_session", {
|
|
1457
|
+
title: "Log a whole session by exercise",
|
|
1458
|
+
description: "Athlete-facing write for logging what you actually did, with NO coach-scheduled workout required — use this for off-plan training (accessory work, a makeup lift, an unplanned gym session). Give a YYYY-MM-DD date and a list of exercises, each with its entered sets (param1/param2, e.g. reps/weight). It reuses a personal session already on that date or creates one, then logs the sets. Get each exerciseId from athlete_exercises. To log against a workout a coach already scheduled, use athlete_log_set instead. Requires confirmation (elicitation or confirm:true).",
|
|
1459
|
+
inputSchema: {
|
|
1460
|
+
...logSessionArgsSchema.shape,
|
|
1461
|
+
confirm: z.boolean().optional()
|
|
1462
|
+
},
|
|
1463
|
+
annotations: DESTRUCTIVE
|
|
1464
|
+
}, ({ date, exercises, confirm }, extra) => attempt(async () => {
|
|
1465
|
+
if (!await confirmGate(server, extra.requestId, `Log a session of ${exercises.length} exercise(s) on ${date}? This writes to your coach-visible training log.`, confirm)) return errorResult(NOT_CONFIRMED);
|
|
1466
|
+
return jsonResult(await logAdHocSession(ctx.client, {
|
|
1467
|
+
date,
|
|
1468
|
+
exercises: mapSessionExercises(exercises)
|
|
1469
|
+
}));
|
|
1470
|
+
}));
|
|
1285
1471
|
server.registerTool("athlete_log_set", {
|
|
1286
1472
|
title: "Log completed set results",
|
|
1287
1473
|
description: "Athlete-facing write: record entered results (reps/weight per set) for a saved workout set on a given day, marking the set completed. Writes to the athlete's (coach-visible) training log and shows in exercise history. Get savedWorkoutSetId + savedWorkoutSetExerciseId from athlete_workouts (raw:true). Requires confirmation (elicitation or confirm:true).",
|
|
@@ -1335,7 +1521,7 @@ function registerAthleteTrainingTools(server, ctx) {
|
|
|
1335
1521
|
}
|
|
1336
1522
|
//#endregion
|
|
1337
1523
|
//#region package.json
|
|
1338
|
-
var version = "
|
|
1524
|
+
var version = "1.1.0";
|
|
1339
1525
|
//#endregion
|
|
1340
1526
|
//#region src/server.ts
|
|
1341
1527
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trainheroic-unofficial/athlete-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
23
23
|
"zod": "^4.4.3",
|
|
24
|
-
"@trainheroic-unofficial/core": "
|
|
25
|
-
"@trainheroic-unofficial/js": "
|
|
24
|
+
"@trainheroic-unofficial/core": "1.1.0",
|
|
25
|
+
"@trainheroic-unofficial/js": "1.1.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^26.0.0",
|