@trainheroic-unofficial/athlete-mcp 0.6.3 → 0.6.5

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 +55 -13
  2. package/package.json +3 -3
package/dist/server.mjs CHANGED
@@ -167,6 +167,7 @@ const logSetArgsSchema = z.object({
167
167
  })).min(1)
168
168
  })).min(1)
169
169
  });
170
+ logSetArgsSchema.extend({ athleteId: idArgSchema });
170
171
  z.looseObject({
171
172
  title: z.string().min(1),
172
173
  param_1_type: z.number().optional(),
@@ -896,6 +897,7 @@ function buildExerciseLogPayload(savedWorkoutSetExerciseId, savedWorkoutSetId, w
896
897
  * record so callers can build the set-completion PUT body via `buildSetCompletePayload`.
897
898
  */
898
899
  function findSavedWorkoutSet(workouts, savedWorkoutSetId) {
900
+ const available = [];
899
901
  for (const pw of workouts) {
900
902
  const rec = pw;
901
903
  const ssw = isRecord(rec.summarizedSavedWorkout) ? rec.summarizedSavedWorkout : {};
@@ -904,18 +906,29 @@ function findSavedWorkoutSet(workouts, savedWorkoutSetId) {
904
906
  const sets = Array.isArray(sw.workoutSets) ? sw.workoutSets : [];
905
907
  for (const s of sets) {
906
908
  if (!isRecord(s)) continue;
909
+ const exercises = Array.isArray(s.workoutSetExercises) ? s.workoutSetExercises.filter(isRecord) : [];
907
910
  if (coerceInt(s.id) === savedWorkoutSetId) {
908
911
  const savedWorkoutId = coerceInt(sw.id);
909
912
  if (!savedWorkoutId) continue;
910
913
  return {
911
914
  savedWorkoutId,
912
- exercises: Array.isArray(s.workoutSetExercises) ? s.workoutSetExercises.filter(isRecord) : [],
915
+ exercises,
913
916
  rawSet: s
914
917
  };
915
918
  }
919
+ const setId = coerceInt(s.id);
920
+ if (setId !== null) {
921
+ const exLabels = exercises.map((ex) => {
922
+ const exId = coerceInt(ex.id);
923
+ const title = typeof ex.exercise_title === "string" && ex.exercise_title || typeof ex.title === "string" && ex.title || "exercise";
924
+ return exId === null ? null : `${exId} (${title})`;
925
+ }).filter((x) => x !== null);
926
+ available.push(`set ${setId} → exercise ids: ${exLabels.join(", ") || "none"}`);
927
+ }
916
928
  }
917
929
  }
918
- throw new Error(`Saved workout set ${savedWorkoutSetId} not found on this date.`);
930
+ const hint = available.length > 0 ? ` Saved workout sets present on this date: ${available.join("; ")}. Pass a "set N" id to --set (the savedWorkoutSetId), and the matching "exercise id" as savedWorkoutSetExerciseId in the results.` : " No saved workout is scheduled on this date — log against a date that has a workout (find it via athlete-workouts / coach athlete-workouts).";
931
+ throw new Error(`Saved workout set ${savedWorkoutSetId} not found on this date.${hint}`);
919
932
  }
920
933
  /**
921
934
  * Build the body for `PUT /1.0/athlete/savedworkoutset/{id}` that marks the set completed.
@@ -972,23 +985,52 @@ function buildSetCompletePayload(rawSet, exerciseIds, complete) {
972
985
  * by the respective PUT bodies.
973
986
  */
974
987
  async function logAthleteSet(client, args) {
975
- const { exercises, rawSet } = findSavedWorkoutSet(await fetchAthleteWorkouts(client, args.date, args.date), args.savedWorkoutSetId);
988
+ return writeSetResults(client, { role: "athlete" }, await fetchAthleteWorkouts(client, args.date, args.date), args.savedWorkoutSetId, args.results);
989
+ }
990
+ /**
991
+ * Shared two-step set-log write behind {@link logAthleteSet} and {@link logForAthlete}.
992
+ * `target` selects the surface: `athlete` writes `/1.0/athlete/...`; `coach` writes
993
+ * `/1.0/coach/...{athleteId}` and stamps `athleteId` into each body. Step 1 PUTs each
994
+ * exercise's entered data to its own endpoint (the only path that actually stores reps and
995
+ * weight). Step 2 marks the set completed; that body needs the app's camelCase in-memory
996
+ * shape and the full list of savedWorkoutSetExercise IDs in the set (not only the logged ones).
997
+ */
998
+ async function writeSetResults(client, target, workouts, savedWorkoutSetId, results) {
999
+ const { exercises, rawSet } = findSavedWorkoutSet(workouts, savedWorkoutSetId);
1000
+ const suffix = target.role === "coach" ? `/${target.athleteId}` : "";
1001
+ const extra = target.role === "coach" ? { athleteId: target.athleteId } : {};
976
1002
  let exercisesLogged = 0;
977
- for (const result of args.results) {
1003
+ for (const result of results) {
978
1004
  const ex = exercises.find((e) => coerceInt(e.id) === result.savedWorkoutSetExerciseId);
979
- if (!ex) throw new Error(`savedWorkoutSetExerciseId ${result.savedWorkoutSetExerciseId} not found in saved workout set ${args.savedWorkoutSetId}.`);
1005
+ if (!ex) {
1006
+ const valid = exercises.map((e) => {
1007
+ const id = coerceInt(e.id);
1008
+ const title = typeof e.exercise_title === "string" && e.exercise_title || typeof e.title === "string" && e.title || "exercise";
1009
+ return id === null ? null : `${id} (${title})`;
1010
+ }).filter((x) => x !== null);
1011
+ throw new Error(`savedWorkoutSetExerciseId ${result.savedWorkoutSetExerciseId} not found in saved workout set ${savedWorkoutSetId}. Exercises in this set: ${valid.join(", ") || "none"}.`);
1012
+ }
980
1013
  const workoutSetExerciseId = coerceInt(ex.workout_set_exercise_id);
981
1014
  if (!workoutSetExerciseId) throw new Error(`Could not resolve workout_set_exercise_id for exercise ${result.savedWorkoutSetExerciseId}.`);
982
- const body = buildExerciseLogPayload(result.savedWorkoutSetExerciseId, args.savedWorkoutSetId, workoutSetExerciseId, result.sets);
983
- const res = await client.request("PUT", `/1.0/athlete/savedworkoutsetexercise/${result.savedWorkoutSetExerciseId}`, { body });
984
- if (!res.ok) throw new Error(`Failed to log exercise ${result.savedWorkoutSetExerciseId} (HTTP ${res.status}).`);
1015
+ const body = {
1016
+ ...buildExerciseLogPayload(result.savedWorkoutSetExerciseId, savedWorkoutSetId, workoutSetExerciseId, result.sets),
1017
+ ...extra
1018
+ };
1019
+ const res = await client.request("PUT", `/1.0/${target.role}/savedworkoutsetexercise/${result.savedWorkoutSetExerciseId}${suffix}`, { body });
1020
+ if (!res.ok) {
1021
+ 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.` : "";
1022
+ throw new Error(`Failed to log exercise ${result.savedWorkoutSetExerciseId} (HTTP ${res.status}).${readOnly}`);
1023
+ }
985
1024
  exercisesLogged += 1;
986
1025
  }
987
- const setBody = buildSetCompletePayload(rawSet, exercises.map((e) => coerceInt(e.id)).filter((n) => n !== null), true);
988
- const setRes = await client.request("PUT", `/1.0/athlete/savedworkoutset/${args.savedWorkoutSetId}`, { body: setBody });
989
- if (!setRes.ok) throw new Error(`Failed to mark workout set ${args.savedWorkoutSetId} completed (HTTP ${setRes.status}).`);
1026
+ const setBody = {
1027
+ ...buildSetCompletePayload(rawSet, exercises.map((e) => coerceInt(e.id)).filter((n) => n !== null), true),
1028
+ ...extra
1029
+ };
1030
+ const setRes = await client.request("PUT", `/1.0/${target.role}/savedworkoutset/${savedWorkoutSetId}${suffix}`, { body: setBody });
1031
+ if (!setRes.ok) throw new Error(`Failed to mark workout set ${savedWorkoutSetId} completed (HTTP ${setRes.status}).`);
990
1032
  return {
991
- savedWorkoutSetId: args.savedWorkoutSetId,
1033
+ savedWorkoutSetId,
992
1034
  exercisesLogged
993
1035
  };
994
1036
  }
@@ -1293,7 +1335,7 @@ function registerAthleteTrainingTools(server, ctx) {
1293
1335
  }
1294
1336
  //#endregion
1295
1337
  //#region package.json
1296
- var version = "0.6.3";
1338
+ var version = "0.6.5";
1297
1339
  //#endregion
1298
1340
  //#region src/server.ts
1299
1341
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trainheroic-unofficial/athlete-mcp",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
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": "0.6.3",
25
- "@trainheroic-unofficial/js": "0.6.3"
24
+ "@trainheroic-unofficial/core": "0.6.5",
25
+ "@trainheroic-unofficial/js": "0.6.5"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^26.0.0",