@trainheroic-unofficial/athlete-mcp 1.1.1 → 1.3.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.
Files changed (2) hide show
  1. package/dist/server.mjs +60 -31
  2. package/package.json +3 -3
package/dist/server.mjs CHANGED
@@ -173,6 +173,11 @@ const logSetArgsSchema = z.object({
173
173
  })).min(1)
174
174
  });
175
175
  logSetArgsSchema.extend({ athleteId: idArgSchema });
176
+ logSetArgsSchema.extend({ athleteId: idArgSchema });
177
+ z.object({
178
+ savedWorkoutSetExerciseId: idArgSchema,
179
+ exerciseId: idArgSchema
180
+ });
176
181
  /**
177
182
  * Args for logging a whole session by exercise (rather than by saved-workout-set id). Each
178
183
  * exercise carries its entered sets and an optional 1-based `order`. The athlete path creates
@@ -891,29 +896,37 @@ function summarizeAthleteWorkouts(list) {
891
896
  }
892
897
  const MAX_PARAM_SLOTS = 10;
893
898
  /**
894
- * Build the body for `PUT /1.0/athlete/savedworkoutsetexercise/{id}`. The body uses
895
- * snake_case keys matching the live API response shape. Each set slot (1-10) gets a
896
- * `param_N_made` flag (1 if data is present, 0 otherwise) and `param_1_data_N` /
897
- * `param_2_data_N` string values.
899
+ * Build the body for `PUT /1.0/{role}/savedworkoutsetexercise/{id}`. The body uses snake_case
900
+ * keys matching the live API response shape. Each set slot (1-10) carries `param_1_data_N` /
901
+ * `param_2_data_N` string values plus a `param_N_made` flag.
902
+ *
903
+ * `mode` selects which write this is — the same endpoint serves both:
904
+ * - `"log"`: the values ARE a performed result, so `param_N_made` is 1 where the slot has data
905
+ * and the exercise `completed` flag is 1 when any set has data.
906
+ * - `"prescribe"`: the values are prescribed targets, written with every `param_N_made` and
907
+ * `completed` left at 0 so the set is not marked done. This matches what the app sends when a
908
+ * coach edits an athlete's prescribed reps/weight.
898
909
  *
899
910
  * Only `savedWorkoutSetExerciseId`, `savedWorkoutSetId`, and `workoutSetExerciseId` are
900
911
  * required from the live exercise record; everything else is derived from `results`.
901
912
  *
902
- * Exported for unit testing — callers should use `logAthleteSet` instead.
913
+ * Exported for unit testing — callers should use `logAthleteSet` / `prescribeForAthlete` instead.
903
914
  */
904
- function buildExerciseLogPayload(savedWorkoutSetExerciseId, savedWorkoutSetId, workoutSetExerciseId, results) {
915
+ function buildExerciseSetPayload(savedWorkoutSetExerciseId, savedWorkoutSetId, workoutSetExerciseId, results, mode) {
905
916
  if (results.length > MAX_PARAM_SLOTS) throw new Error(`At most ${MAX_PARAM_SLOTS} sets are supported per exercise; got ${results.length}.`);
917
+ const performed = mode === "log";
918
+ const hasData = results.some((s) => s.param1 !== void 0 || s.param2 !== void 0);
906
919
  const body = {
907
920
  id: savedWorkoutSetExerciseId,
908
921
  saved_workout_set_id: savedWorkoutSetId,
909
922
  workout_set_exercise_id: workoutSetExerciseId,
910
- completed: results.some((s) => s.param1 !== void 0 || s.param2 !== void 0) ? 1 : 0
923
+ completed: performed && hasData ? 1 : 0
911
924
  };
912
925
  for (let i = 1; i <= MAX_PARAM_SLOTS; i += 1) {
913
926
  const slot = results[i - 1];
914
927
  const p1 = slot?.param1 !== void 0 ? String(slot.param1) : "";
915
928
  const p2 = slot?.param2 !== void 0 ? String(slot.param2) : "";
916
- body[`param_${i}_made`] = p1 !== "" || p2 !== "" ? 1 : 0;
929
+ body[`param_${i}_made`] = performed && (p1 !== "" || p2 !== "") ? 1 : 0;
917
930
  body[`param_1_data_${i}`] = p1;
918
931
  body[`param_2_data_${i}`] = p2;
919
932
  }
@@ -1014,7 +1027,11 @@ function buildSetCompletePayload(rawSet, exerciseIds, complete) {
1014
1027
  * by the respective PUT bodies.
1015
1028
  */
1016
1029
  async function logAthleteSet(client, args) {
1017
- return writeSetResults(client, { role: "athlete" }, await fetchAthleteWorkouts(client, args.date, args.date), args.savedWorkoutSetId, args.results);
1030
+ const r = await writeSetResults(client, { role: "athlete" }, await fetchAthleteWorkouts(client, args.date, args.date), args.savedWorkoutSetId, args.results, "log");
1031
+ return {
1032
+ savedWorkoutSetId: r.savedWorkoutSetId,
1033
+ exercisesLogged: r.exercisesWritten
1034
+ };
1018
1035
  }
1019
1036
  /**
1020
1037
  * Coach "Log for Athlete": record set results for a roster athlete on their behalf, via the
@@ -1028,24 +1045,32 @@ async function logAthleteSet(client, args) {
1028
1045
  */
1029
1046
  async function logForAthlete(client, args) {
1030
1047
  const workouts = await fetchCoachAthleteWorkouts(client, args.athleteId, args.date, args.date);
1031
- return writeSetResults(client, {
1048
+ const r = await writeSetResults(client, {
1032
1049
  role: "coach",
1033
1050
  athleteId: args.athleteId
1034
- }, workouts, args.savedWorkoutSetId, args.results);
1051
+ }, workouts, args.savedWorkoutSetId, args.results, "log");
1052
+ return {
1053
+ savedWorkoutSetId: r.savedWorkoutSetId,
1054
+ exercisesLogged: r.exercisesWritten
1055
+ };
1035
1056
  }
1036
1057
  /**
1037
- * Shared two-step set-log write behind {@link logAthleteSet} and {@link logForAthlete}.
1038
- * `target` selects the surface: `athlete` writes `/1.0/athlete/...`; `coach` writes
1039
- * `/1.0/coach/...{athleteId}` and stamps `athleteId` into each body. Step 1 PUTs each
1040
- * exercise's entered data to its own endpoint (the only path that actually stores reps and
1041
- * weight). Step 2 marks the set completed; that body needs the app's camelCase in-memory
1042
- * shape and the full list of savedWorkoutSetExercise IDs in the set (not only the logged ones).
1058
+ * Shared set-write behind {@link logAthleteSet}, {@link logForAthlete}, and
1059
+ * {@link prescribeForAthlete}. `target` selects the surface: `athlete` writes `/1.0/athlete/...`;
1060
+ * `coach` writes `/1.0/coach/...{athleteId}` and stamps `athleteId` into each body.
1061
+ *
1062
+ * Step 1 PUTs each exercise's per-set values to its own endpoint (the only path that actually
1063
+ * stores reps and weight). `mode` decides what those values mean: `"log"` records them as a
1064
+ * performed result and runs Step 2, which marks the set completed (that body needs the app's
1065
+ * camelCase in-memory shape and the full list of savedWorkoutSetExercise IDs in the set, not only
1066
+ * the written ones); `"prescribe"` writes them as a prescription and skips Step 2, leaving the set
1067
+ * open.
1043
1068
  */
1044
- async function writeSetResults(client, target, workouts, savedWorkoutSetId, results) {
1069
+ async function writeSetResults(client, target, workouts, savedWorkoutSetId, results, mode) {
1045
1070
  const { exercises, rawSet } = findSavedWorkoutSet(workouts, savedWorkoutSetId);
1046
1071
  const suffix = target.role === "coach" ? `/${target.athleteId}` : "";
1047
1072
  const extra = target.role === "coach" ? { athleteId: target.athleteId } : {};
1048
- let exercisesLogged = 0;
1073
+ let exercisesWritten = 0;
1049
1074
  for (const result of results) {
1050
1075
  const ex = exercises.find((e) => coerceInt(e.id) === result.savedWorkoutSetExerciseId);
1051
1076
  if (!ex) {
@@ -1059,25 +1084,27 @@ async function writeSetResults(client, target, workouts, savedWorkoutSetId, resu
1059
1084
  const workoutSetExerciseId = coerceInt(ex.workout_set_exercise_id);
1060
1085
  if (!workoutSetExerciseId) throw new Error(`Could not resolve workout_set_exercise_id for exercise ${result.savedWorkoutSetExerciseId}.`);
1061
1086
  const body = {
1062
- ...buildExerciseLogPayload(result.savedWorkoutSetExerciseId, savedWorkoutSetId, workoutSetExerciseId, result.sets),
1087
+ ...buildExerciseSetPayload(result.savedWorkoutSetExerciseId, savedWorkoutSetId, workoutSetExerciseId, result.sets, mode),
1063
1088
  ...extra
1064
1089
  };
1065
1090
  const res = await client.request("PUT", `/1.0/${target.role}/savedworkoutsetexercise/${result.savedWorkoutSetExerciseId}${suffix}`, { body });
1066
1091
  if (!res.ok) {
1067
- const readOnly = target.role === "coach" && (res.status === 401 || res.status === 403) ? ` Athlete ${target.athleteId} appears to be read-only for results — TrainHeroic's seeded demo/sample athletes return ${res.status} here; results only persist for real (invited) athletes.` : "";
1068
- throw new Error(`Failed to log exercise ${result.savedWorkoutSetExerciseId} (HTTP ${res.status}).${readOnly}`);
1092
+ const readOnly = target.role === "coach" && (res.status === 401 || res.status === 403) ? ` Athlete ${target.athleteId} appears to be read-only for changes — TrainHeroic's seeded demo/sample athletes return ${res.status} here; writes only persist for real (invited) athletes.` : "";
1093
+ throw new Error(`Failed to write exercise ${result.savedWorkoutSetExerciseId} (HTTP ${res.status}).${readOnly}`);
1069
1094
  }
1070
- exercisesLogged += 1;
1095
+ exercisesWritten += 1;
1096
+ }
1097
+ if (mode === "log") {
1098
+ const setBody = {
1099
+ ...buildSetCompletePayload(rawSet, exercises.map((e) => coerceInt(e.id)).filter((n) => n !== null), true),
1100
+ ...extra
1101
+ };
1102
+ const setRes = await client.request("PUT", `/1.0/${target.role}/savedworkoutset/${savedWorkoutSetId}${suffix}`, { body: setBody });
1103
+ if (!setRes.ok) throw new Error(`Failed to mark workout set ${savedWorkoutSetId} completed (HTTP ${setRes.status}).`);
1071
1104
  }
1072
- const setBody = {
1073
- ...buildSetCompletePayload(rawSet, exercises.map((e) => coerceInt(e.id)).filter((n) => n !== null), true),
1074
- ...extra
1075
- };
1076
- const setRes = await client.request("PUT", `/1.0/${target.role}/savedworkoutset/${savedWorkoutSetId}${suffix}`, { body: setBody });
1077
- if (!setRes.ok) throw new Error(`Failed to mark workout set ${savedWorkoutSetId} completed (HTTP ${setRes.status}).`);
1078
1105
  return {
1079
1106
  savedWorkoutSetId,
1080
- exercisesLogged
1107
+ exercisesWritten
1081
1108
  };
1082
1109
  }
1083
1110
  /**
@@ -1234,6 +1261,7 @@ function presentExerciseHistory(detail) {
1234
1261
  description: p.description ?? null,
1235
1262
  reps: p.reps ?? null,
1236
1263
  weight: p.weight ?? null,
1264
+ units: p.units ?? null,
1237
1265
  date: p.dateCompleted ?? null
1238
1266
  })),
1239
1267
  sessions: (detail.history ?? []).map((h) => ({
@@ -1268,6 +1296,7 @@ function historyInRange(presented, since, until) {
1268
1296
  sessions
1269
1297
  };
1270
1298
  }
1299
+ z.number().int().positive().max(36).optional();
1271
1300
  //#endregion
1272
1301
  //#region ../core/src/tools/athlete-training.ts
1273
1302
  /** Identity, profile, prefs, working maxes, leaderboard. */
@@ -1521,7 +1550,7 @@ function registerAthleteTrainingTools(server, ctx) {
1521
1550
  }
1522
1551
  //#endregion
1523
1552
  //#region package.json
1524
- var version = "1.1.1";
1553
+ var version = "1.3.0";
1525
1554
  //#endregion
1526
1555
  //#region src/server.ts
1527
1556
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trainheroic-unofficial/athlete-mcp",
3
- "version": "1.1.1",
3
+ "version": "1.3.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": "1.1.1",
25
- "@trainheroic-unofficial/js": "1.1.1"
24
+ "@trainheroic-unofficial/core": "1.3.0",
25
+ "@trainheroic-unofficial/js": "1.3.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^26.0.0",