@trainheroic-unofficial/athlete-mcp 1.4.0 → 1.5.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 +172 -66
- package/package.json +3 -3
package/dist/server.mjs
CHANGED
|
@@ -160,20 +160,41 @@ const loggedSetSchema = z.object({
|
|
|
160
160
|
param2: z.union([z.number(), z.string()]).optional()
|
|
161
161
|
});
|
|
162
162
|
/**
|
|
163
|
+
* A logged set that can name the prescribed position it fills. `slot` is the 1-based set
|
|
164
|
+
* index in the prescription (10 max) the result should land in; omit it to fill the next
|
|
165
|
+
* sequential position (the first entry is set 1, the second set 2, and so on). Targeting a
|
|
166
|
+
* slot lets a partial log land in the right positions of a multi-set prescription — e.g.
|
|
167
|
+
* three top singles into the 4th/5th/6th positions of an `8,5,3,1,1,1` ramp. Used by the
|
|
168
|
+
* by-set logging write (not the by-exercise session log, where each exercise's sets are
|
|
169
|
+
* always sequential).
|
|
170
|
+
*/
|
|
171
|
+
const loggedSetWithSlotSchema = loggedSetSchema.extend({ slot: z.number().int().min(1).max(10).optional() });
|
|
172
|
+
/**
|
|
163
173
|
* Args for the set-logging write. `date` (the workout's day) locates the saved
|
|
164
174
|
* workout via the range endpoint; `savedWorkoutSetId` picks the set to complete; `results`
|
|
165
|
-
* gives, per exercise in it, the entered value of each set
|
|
175
|
+
* gives, per exercise in it, the entered value of each set. Each set fills the next position by
|
|
176
|
+
* default, or names its `slot` (1-based) to place a partial log at specific positions. A partial
|
|
177
|
+
* log keeps positions already logged in an earlier call and leaves the positions it does not
|
|
178
|
+
* write empty.
|
|
166
179
|
*/
|
|
167
180
|
const logSetArgsSchema = z.object({
|
|
168
181
|
date: dateString,
|
|
169
182
|
savedWorkoutSetId: idArgSchema,
|
|
170
183
|
results: z.array(z.object({
|
|
171
184
|
savedWorkoutSetExerciseId: idArgSchema,
|
|
172
|
-
sets: z.array(
|
|
185
|
+
sets: z.array(loggedSetWithSlotSchema).min(1)
|
|
173
186
|
})).min(1)
|
|
174
187
|
});
|
|
175
188
|
logSetArgsSchema.extend({ athleteId: idArgSchema });
|
|
176
|
-
|
|
189
|
+
z.object({
|
|
190
|
+
date: dateString,
|
|
191
|
+
savedWorkoutSetId: idArgSchema,
|
|
192
|
+
athleteId: idArgSchema,
|
|
193
|
+
results: z.array(z.object({
|
|
194
|
+
savedWorkoutSetExerciseId: idArgSchema,
|
|
195
|
+
sets: z.array(loggedSetSchema).min(1)
|
|
196
|
+
})).min(1)
|
|
197
|
+
});
|
|
177
198
|
z.object({
|
|
178
199
|
savedWorkoutSetExerciseId: idArgSchema,
|
|
179
200
|
exerciseId: idArgSchema
|
|
@@ -635,6 +656,10 @@ function exerciseUnits(param1, param2) {
|
|
|
635
656
|
function isRecord(x) {
|
|
636
657
|
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
637
658
|
}
|
|
659
|
+
/** A non-empty string, or null. Narrows a loosely-typed API field to a usable string. */
|
|
660
|
+
function str(v) {
|
|
661
|
+
return typeof v === "string" && v !== "" ? v : null;
|
|
662
|
+
}
|
|
638
663
|
/**
|
|
639
664
|
* Rank candidate rows for a free-text query (FTS5 replacement). Higher is better:
|
|
640
665
|
* exact title, then prefix, then count of matched tokens, with shorter titles and
|
|
@@ -720,7 +745,6 @@ function fetchLeaderboard(client, workoutId, opts = {}) {
|
|
|
720
745
|
const query = qs.toString();
|
|
721
746
|
return getJson(client, `/3.0/athlete/leaderboard/${workoutId}${query ? `?${query}` : ""}`, "athlete leaderboard");
|
|
722
747
|
}
|
|
723
|
-
const SLOTS = 10;
|
|
724
748
|
function nonEmpty(value) {
|
|
725
749
|
return value !== void 0 && value !== null && String(value).trim() !== "";
|
|
726
750
|
}
|
|
@@ -731,7 +755,7 @@ function nonEmpty(value) {
|
|
|
731
755
|
*/
|
|
732
756
|
function prescribedSets(ex) {
|
|
733
757
|
const out = [];
|
|
734
|
-
for (let i = 1; i <=
|
|
758
|
+
for (let i = 1; i <= 10; i += 1) {
|
|
735
759
|
const p1 = ex[`param_1_data_${i}`];
|
|
736
760
|
const p2 = ex[`param_2_data_${i}`];
|
|
737
761
|
const has1 = nonEmpty(p1);
|
|
@@ -752,7 +776,7 @@ function prescribedSets(ex) {
|
|
|
752
776
|
*/
|
|
753
777
|
function performedSets(ex) {
|
|
754
778
|
const out = [];
|
|
755
|
-
for (let i = 1; i <=
|
|
779
|
+
for (let i = 1; i <= 10; i += 1) {
|
|
756
780
|
if (coerceInt(ex[`param_${i}_made`]) !== 1) continue;
|
|
757
781
|
const p1 = ex[`param_1_data_${i}`];
|
|
758
782
|
const p2 = ex[`param_2_data_${i}`];
|
|
@@ -786,9 +810,6 @@ function presentBlock(set, performedById) {
|
|
|
786
810
|
exercises: exercises.filter(isRecord).map((ex) => presentExercise(ex, performedById))
|
|
787
811
|
};
|
|
788
812
|
}
|
|
789
|
-
function str(v) {
|
|
790
|
-
return typeof v === "string" && v !== "" ? v : null;
|
|
791
|
-
}
|
|
792
813
|
/** Every logged set (programmed + athlete-added) in the saved copy, paired with its exercises. */
|
|
793
814
|
function savedSets(saved) {
|
|
794
815
|
const out = [];
|
|
@@ -918,7 +939,52 @@ function summarizeAthleteWorkouts(list) {
|
|
|
918
939
|
};
|
|
919
940
|
});
|
|
920
941
|
}
|
|
921
|
-
|
|
942
|
+
/** Flatten `/v5/exercises/{id}/history` into PRs + a session time-series. */
|
|
943
|
+
function presentExerciseHistory(detail) {
|
|
944
|
+
return {
|
|
945
|
+
liftPRs: (detail.liftPRs ?? []).map((p) => ({
|
|
946
|
+
description: p.description ?? null,
|
|
947
|
+
reps: p.reps ?? null,
|
|
948
|
+
weight: p.weight ?? null,
|
|
949
|
+
units: p.units ?? null,
|
|
950
|
+
date: p.dateCompleted ?? null
|
|
951
|
+
})),
|
|
952
|
+
sessions: (detail.history ?? []).map((h) => ({
|
|
953
|
+
date: h.dateCompleted,
|
|
954
|
+
abr: h.abr ?? null,
|
|
955
|
+
estimated1RM: h.bestEstimated1RM ?? null,
|
|
956
|
+
sets: (h.sets ?? []).map((s) => ({
|
|
957
|
+
setNumber: s.setNumber,
|
|
958
|
+
value: s.formattedValue ?? null
|
|
959
|
+
}))
|
|
960
|
+
}))
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
//#endregion
|
|
964
|
+
//#region ../js/src/athlete-set-write.ts
|
|
965
|
+
/**
|
|
966
|
+
* Coerce the loosely-typed `results` from a validated log/prescribe args object into the SDK's
|
|
967
|
+
* {@link SetResult}[]. The dto schemas validate ids as a number or a numeric string and leave
|
|
968
|
+
* empty param slots optional; this narrows the id to a number and copies only the present per-set
|
|
969
|
+
* values (so the per-set type stays free of `undefined` under exactOptionalPropertyTypes). Shared
|
|
970
|
+
* by the MCP tools and the CLI so the mapping lives in one place rather than once per surface.
|
|
971
|
+
*/
|
|
972
|
+
function toSetResults(results) {
|
|
973
|
+
return results.map((r) => {
|
|
974
|
+
const id = coerceInt(r.savedWorkoutSetExerciseId);
|
|
975
|
+
if (id === null) throw new Error(`Invalid savedWorkoutSetExerciseId: ${String(r.savedWorkoutSetExerciseId)}`);
|
|
976
|
+
return {
|
|
977
|
+
savedWorkoutSetExerciseId: id,
|
|
978
|
+
sets: r.sets.map((s) => {
|
|
979
|
+
const set = {};
|
|
980
|
+
if (s.param1 !== void 0) set.param1 = s.param1;
|
|
981
|
+
if (s.param2 !== void 0) set.param2 = s.param2;
|
|
982
|
+
if (s.slot !== void 0) set.slot = s.slot;
|
|
983
|
+
return set;
|
|
984
|
+
})
|
|
985
|
+
};
|
|
986
|
+
});
|
|
987
|
+
}
|
|
922
988
|
/**
|
|
923
989
|
* Build the body for `PUT /1.0/{role}/savedworkoutsetexercise/{id}`. The body uses snake_case
|
|
924
990
|
* keys matching the live API response shape. Each set slot (1-10) carries `param_1_data_N` /
|
|
@@ -926,36 +992,96 @@ const MAX_PARAM_SLOTS = 10;
|
|
|
926
992
|
*
|
|
927
993
|
* `mode` selects which write this is — the same endpoint serves both:
|
|
928
994
|
* - `"log"`: the values ARE a performed result, so `param_N_made` is 1 where the slot has data
|
|
929
|
-
* and the exercise `completed` flag is 1 when any set has data.
|
|
995
|
+
* and the exercise `completed` flag is 1 when any set has logged data.
|
|
930
996
|
* - `"prescribe"`: the values are prescribed targets, written with every `param_N_made` and
|
|
931
997
|
* `completed` left at 0 so the set is not marked done. This matches what the app sends when a
|
|
932
998
|
* coach edits an athlete's prescribed reps/weight.
|
|
933
999
|
*
|
|
1000
|
+
* Each set fills a 1-based slot: its explicit `slot`, or its sequential position in `results`
|
|
1001
|
+
* when `slot` is omitted. A `log` carrying the live exercise record in `existing` keeps the slots
|
|
1002
|
+
* it does not write that were ALREADY performed (`param_N_made === 1`), so logging a second part of
|
|
1003
|
+
* a set does not wipe the earlier-logged sets. A slot holding only un-logged prescription pre-fill
|
|
1004
|
+
* (`param_N_made === 0`) is left blank rather than carried over: marking the set completed makes
|
|
1005
|
+
* the server flag every data-bearing slot performed, so preserving that pre-fill would fabricate
|
|
1006
|
+
* sets the athlete never did. The prescription is unaffected (it lives in the separate `workout`
|
|
1007
|
+
* copy, not this saved copy). A `prescribe` ignores `existing` and replaces the whole prescription.
|
|
1008
|
+
*
|
|
934
1009
|
* Only `savedWorkoutSetExerciseId`, `savedWorkoutSetId`, and `workoutSetExerciseId` are
|
|
935
|
-
* required from the live exercise record; everything else is derived from `results
|
|
1010
|
+
* required from the live exercise record; everything else is derived from `results` and the
|
|
1011
|
+
* preserved slots of `existing`.
|
|
936
1012
|
*
|
|
937
1013
|
* Exported for unit testing — callers should use `logAthleteSet` / `prescribeForAthlete` instead.
|
|
938
1014
|
*/
|
|
939
|
-
function buildExerciseSetPayload(savedWorkoutSetExerciseId, savedWorkoutSetId, workoutSetExerciseId, results, mode) {
|
|
940
|
-
if (results.length >
|
|
1015
|
+
function buildExerciseSetPayload(savedWorkoutSetExerciseId, savedWorkoutSetId, workoutSetExerciseId, results, mode, existing) {
|
|
1016
|
+
if (results.length > 10) throw new Error(`At most 10 sets are supported per exercise; got ${results.length}.`);
|
|
1017
|
+
const bySlot = /* @__PURE__ */ new Map();
|
|
1018
|
+
results.forEach((set, i) => {
|
|
1019
|
+
const slot = set.slot ?? i + 1;
|
|
1020
|
+
if (slot < 1 || slot > 10) throw new Error(`Set slot ${slot} is out of range; slots are 1–10.`);
|
|
1021
|
+
if (bySlot.has(slot)) throw new Error(`Two sets target slot ${slot}; each slot can be written once.`);
|
|
1022
|
+
bySlot.set(slot, set);
|
|
1023
|
+
});
|
|
941
1024
|
const performed = mode === "log";
|
|
942
|
-
const
|
|
1025
|
+
const carryOver = performed && existing !== void 0;
|
|
943
1026
|
const body = {
|
|
944
1027
|
id: savedWorkoutSetExerciseId,
|
|
945
1028
|
saved_workout_set_id: savedWorkoutSetId,
|
|
946
|
-
workout_set_exercise_id: workoutSetExerciseId
|
|
947
|
-
completed: performed && hasData ? 1 : 0
|
|
1029
|
+
workout_set_exercise_id: workoutSetExerciseId
|
|
948
1030
|
};
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
1031
|
+
let anyMade = false;
|
|
1032
|
+
for (let i = 1; i <= 10; i += 1) {
|
|
1033
|
+
const target = bySlot.get(i);
|
|
1034
|
+
let p1;
|
|
1035
|
+
let p2;
|
|
1036
|
+
let made;
|
|
1037
|
+
if (target) {
|
|
1038
|
+
p1 = target.param1 !== void 0 ? String(target.param1) : "";
|
|
1039
|
+
p2 = target.param2 !== void 0 ? String(target.param2) : "";
|
|
1040
|
+
made = performed && (p1 !== "" || p2 !== "") ? 1 : 0;
|
|
1041
|
+
} else if (carryOver && coerceInt(existing?.[`param_${i}_made`]) === 1) {
|
|
1042
|
+
p1 = existingSlotData(existing, `param_1_data_${i}`);
|
|
1043
|
+
p2 = existingSlotData(existing, `param_2_data_${i}`);
|
|
1044
|
+
made = 1;
|
|
1045
|
+
} else {
|
|
1046
|
+
p1 = "";
|
|
1047
|
+
p2 = "";
|
|
1048
|
+
made = 0;
|
|
1049
|
+
}
|
|
1050
|
+
if (made === 1) anyMade = true;
|
|
1051
|
+
body[`param_${i}_made`] = made;
|
|
954
1052
|
body[`param_1_data_${i}`] = p1;
|
|
955
1053
|
body[`param_2_data_${i}`] = p2;
|
|
956
1054
|
}
|
|
1055
|
+
body.completed = performed && anyMade ? 1 : 0;
|
|
957
1056
|
return body;
|
|
958
1057
|
}
|
|
1058
|
+
/** Read a saved-copy slot value (`param_1_data_N` / `param_2_data_N`) as the string the body uses. */
|
|
1059
|
+
function existingSlotData(existing, key) {
|
|
1060
|
+
const v = existing?.[key];
|
|
1061
|
+
return v === void 0 || v === null ? "" : String(v);
|
|
1062
|
+
}
|
|
1063
|
+
/** True when a saved-copy exercise already carries a performed slot (any `param_N_made` === 1). */
|
|
1064
|
+
function exerciseHasLoggedData(ex) {
|
|
1065
|
+
for (let i = 1; i <= 10; i += 1) if (coerceInt(ex[`param_${i}_made`]) === 1) return true;
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
/** True when a result carries at least one non-empty param value (so it produces a performed slot). */
|
|
1069
|
+
function resultHasData(result) {
|
|
1070
|
+
return result.sets.some((s) => s.param1 !== void 0 && String(s.param1) !== "" || s.param2 !== void 0 && String(s.param2) !== "");
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Whether every exercise in a saved workout set now has logged data — either written with data in
|
|
1074
|
+
* this call (`loggedIds`) or already carrying a performed slot. Gates the set-completion PUT: a
|
|
1075
|
+
* superset/circuit stays open until the last exercise is logged, so completing it on a partial log
|
|
1076
|
+
* does not flip its still-empty siblings to "done". An exercise written with only empty values does
|
|
1077
|
+
* not count (it would not be marked performed), so an all-empty log never completes the set.
|
|
1078
|
+
*/
|
|
1079
|
+
function isSetFullyLogged(exercises, loggedIds) {
|
|
1080
|
+
return exercises.every((ex) => {
|
|
1081
|
+
const id = coerceInt(ex.id);
|
|
1082
|
+
return id !== null && loggedIds.has(id) || exerciseHasLoggedData(ex);
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
959
1085
|
/**
|
|
960
1086
|
* Locate the target saved workout set across all program workouts on the given day.
|
|
961
1087
|
* Returns the `savedWorkoutId`, the matching set's `workoutSetExercises` array so callers
|
|
@@ -1054,7 +1180,8 @@ async function logAthleteSet(client, args) {
|
|
|
1054
1180
|
const r = await writeSetResults(client, { role: "athlete" }, await fetchAthleteWorkouts(client, args.date, args.date), args.savedWorkoutSetId, args.results, "log");
|
|
1055
1181
|
return {
|
|
1056
1182
|
savedWorkoutSetId: r.savedWorkoutSetId,
|
|
1057
|
-
exercisesLogged: r.exercisesWritten
|
|
1183
|
+
exercisesLogged: r.exercisesWritten,
|
|
1184
|
+
setCompleted: r.setCompleted
|
|
1058
1185
|
};
|
|
1059
1186
|
}
|
|
1060
1187
|
/**
|
|
@@ -1075,7 +1202,8 @@ async function logForAthlete(client, args) {
|
|
|
1075
1202
|
}, workouts, args.savedWorkoutSetId, args.results, "log");
|
|
1076
1203
|
return {
|
|
1077
1204
|
savedWorkoutSetId: r.savedWorkoutSetId,
|
|
1078
|
-
exercisesLogged: r.exercisesWritten
|
|
1205
|
+
exercisesLogged: r.exercisesWritten,
|
|
1206
|
+
setCompleted: r.setCompleted
|
|
1079
1207
|
};
|
|
1080
1208
|
}
|
|
1081
1209
|
/**
|
|
@@ -1108,7 +1236,7 @@ async function writeSetResults(client, target, workouts, savedWorkoutSetId, resu
|
|
|
1108
1236
|
const workoutSetExerciseId = coerceInt(ex.workout_set_exercise_id);
|
|
1109
1237
|
if (!workoutSetExerciseId) throw new Error(`savedWorkoutSetExercise ${result.savedWorkoutSetExerciseId} is missing its workout_set_exercise_id (the prescription-template pointer the write needs). This is the savedWorkoutSetExerciseId, not an exercise_id — re-read the ids from athlete_saved_workouts.`);
|
|
1110
1238
|
const body = {
|
|
1111
|
-
...buildExerciseSetPayload(result.savedWorkoutSetExerciseId, savedWorkoutSetId, workoutSetExerciseId, result.sets, mode),
|
|
1239
|
+
...buildExerciseSetPayload(result.savedWorkoutSetExerciseId, savedWorkoutSetId, workoutSetExerciseId, result.sets, mode, ex),
|
|
1112
1240
|
...extra
|
|
1113
1241
|
};
|
|
1114
1242
|
const res = await client.request("PUT", `/1.0/${target.role}/savedworkoutsetexercise/${result.savedWorkoutSetExerciseId}${suffix}`, { body });
|
|
@@ -1118,17 +1246,22 @@ async function writeSetResults(client, target, workouts, savedWorkoutSetId, resu
|
|
|
1118
1246
|
}
|
|
1119
1247
|
exercisesWritten += 1;
|
|
1120
1248
|
}
|
|
1249
|
+
let setCompleted = false;
|
|
1121
1250
|
if (mode === "log") {
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1251
|
+
if (isSetFullyLogged(exercises, new Set(results.filter(resultHasData).map((r) => r.savedWorkoutSetExerciseId)))) {
|
|
1252
|
+
const setBody = {
|
|
1253
|
+
...buildSetCompletePayload(rawSet, exercises.map((e) => coerceInt(e.id)).filter((n) => n !== null), true),
|
|
1254
|
+
...extra
|
|
1255
|
+
};
|
|
1256
|
+
const setRes = await client.request("PUT", `/1.0/${target.role}/savedworkoutset/${savedWorkoutSetId}${suffix}`, { body: setBody });
|
|
1257
|
+
if (!setRes.ok) throw new Error(`Failed to mark workout set ${savedWorkoutSetId} completed (HTTP ${setRes.status}).`);
|
|
1258
|
+
setCompleted = true;
|
|
1259
|
+
}
|
|
1128
1260
|
}
|
|
1129
1261
|
return {
|
|
1130
1262
|
savedWorkoutSetId,
|
|
1131
|
-
exercisesWritten
|
|
1263
|
+
exercisesWritten,
|
|
1264
|
+
setCompleted
|
|
1132
1265
|
};
|
|
1133
1266
|
}
|
|
1134
1267
|
/**
|
|
@@ -1235,7 +1368,10 @@ async function logResolvedExercises(client, target, date, resolved) {
|
|
|
1235
1368
|
savedWorkoutSetId,
|
|
1236
1369
|
results
|
|
1237
1370
|
});
|
|
1238
|
-
out.push(
|
|
1371
|
+
out.push({
|
|
1372
|
+
savedWorkoutSetId: written.savedWorkoutSetId,
|
|
1373
|
+
exercisesLogged: written.exercisesLogged
|
|
1374
|
+
});
|
|
1239
1375
|
}
|
|
1240
1376
|
return out;
|
|
1241
1377
|
}
|
|
@@ -1278,27 +1414,6 @@ async function logAdHocSession(client, args) {
|
|
|
1278
1414
|
sets
|
|
1279
1415
|
};
|
|
1280
1416
|
}
|
|
1281
|
-
/** Flatten `/v5/exercises/{id}/history` into PRs + a session time-series. */
|
|
1282
|
-
function presentExerciseHistory(detail) {
|
|
1283
|
-
return {
|
|
1284
|
-
liftPRs: (detail.liftPRs ?? []).map((p) => ({
|
|
1285
|
-
description: p.description ?? null,
|
|
1286
|
-
reps: p.reps ?? null,
|
|
1287
|
-
weight: p.weight ?? null,
|
|
1288
|
-
units: p.units ?? null,
|
|
1289
|
-
date: p.dateCompleted ?? null
|
|
1290
|
-
})),
|
|
1291
|
-
sessions: (detail.history ?? []).map((h) => ({
|
|
1292
|
-
date: h.dateCompleted,
|
|
1293
|
-
abr: h.abr ?? null,
|
|
1294
|
-
estimated1RM: h.bestEstimated1RM ?? null,
|
|
1295
|
-
sets: (h.sets ?? []).map((s) => ({
|
|
1296
|
-
setNumber: s.setNumber,
|
|
1297
|
-
value: s.formattedValue ?? null
|
|
1298
|
-
}))
|
|
1299
|
-
}))
|
|
1300
|
-
};
|
|
1301
|
-
}
|
|
1302
1417
|
//#endregion
|
|
1303
1418
|
//#region ../core/src/history.ts
|
|
1304
1419
|
/**
|
|
@@ -1523,7 +1638,7 @@ function registerLogTool(server, ctx) {
|
|
|
1523
1638
|
}));
|
|
1524
1639
|
server.registerTool("athlete_log_set", {
|
|
1525
1640
|
title: "Log completed set results",
|
|
1526
|
-
description: "Athlete-facing write: record entered results (reps/weight per set) for a saved workout set on a given day
|
|
1641
|
+
description: "Athlete-facing write: record entered results (reps/weight per set) for a saved workout set on a given day. Writes to the athlete's (coach-visible) training log and shows in exercise history. Each set fills the next position in order; give a set a 1-based `slot` to place it at a specific position — e.g. log three top singles into positions 4-6 of an 8,5,3,1,1,1 ramp. A partial log records only the positions you send (plus any logged in an earlier call) and leaves the rest unlogged, so it does not mark untouched sets as performed. In a superset/circuit the block is marked done only once every exercise in it has logged results, so logging one exercise leaves its siblings untouched (the response's `setCompleted` reports whether the block was completed). Get savedWorkoutSetId + savedWorkoutSetExerciseId from athlete_workouts (raw:true). Requires confirmation (elicitation or confirm:true).",
|
|
1527
1642
|
inputSchema: {
|
|
1528
1643
|
...logSetArgsSchema.shape,
|
|
1529
1644
|
confirm: z.boolean().optional()
|
|
@@ -1531,19 +1646,10 @@ function registerLogTool(server, ctx) {
|
|
|
1531
1646
|
annotations: DESTRUCTIVE
|
|
1532
1647
|
}, ({ date, savedWorkoutSetId, results, confirm }, extra) => attempt(async () => {
|
|
1533
1648
|
if (!await confirmGate(server, extra.requestId, `Log results to saved workout set ${toId(savedWorkoutSetId)} on ${date}? This writes to your coach-visible training log.`, confirm)) return errorResult(NOT_CONFIRMED);
|
|
1534
|
-
const mapped = results.map((r) => ({
|
|
1535
|
-
savedWorkoutSetExerciseId: toId(r.savedWorkoutSetExerciseId),
|
|
1536
|
-
sets: r.sets.map((s) => {
|
|
1537
|
-
const slot = {};
|
|
1538
|
-
if (s.param1 !== void 0) slot.param1 = s.param1;
|
|
1539
|
-
if (s.param2 !== void 0) slot.param2 = s.param2;
|
|
1540
|
-
return slot;
|
|
1541
|
-
})
|
|
1542
|
-
}));
|
|
1543
1649
|
return jsonResult(await logAthleteSet(ctx.client, {
|
|
1544
1650
|
date,
|
|
1545
1651
|
savedWorkoutSetId: toId(savedWorkoutSetId),
|
|
1546
|
-
results:
|
|
1652
|
+
results: toSetResults(results)
|
|
1547
1653
|
}));
|
|
1548
1654
|
}));
|
|
1549
1655
|
}
|
|
@@ -1574,7 +1680,7 @@ function registerAthleteTrainingTools(server, ctx) {
|
|
|
1574
1680
|
}
|
|
1575
1681
|
//#endregion
|
|
1576
1682
|
//#region package.json
|
|
1577
|
-
var version = "1.
|
|
1683
|
+
var version = "1.5.0";
|
|
1578
1684
|
//#endregion
|
|
1579
1685
|
//#region src/server.ts
|
|
1580
1686
|
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.5.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.5.0",
|
|
25
|
+
"@trainheroic-unofficial/js": "1.5.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^26.0.0",
|