@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.
- package/dist/server.mjs +60 -31
- 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/
|
|
895
|
-
*
|
|
896
|
-
* `
|
|
897
|
-
*
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1038
|
-
* `target` selects the surface: `athlete` writes `/1.0/athlete/...`;
|
|
1039
|
-
* `/1.0/coach/...{athleteId}` and stamps `athleteId` into each body.
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
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
|
|
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
|
-
...
|
|
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
|
|
1068
|
-
throw new Error(`Failed to
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
25
|
-
"@trainheroic-unofficial/js": "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",
|