@trainheroic-unofficial/athlete-mcp 1.6.0 → 1.7.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 +204 -86
- package/package.json +4 -4
package/dist/server.mjs
CHANGED
|
@@ -195,7 +195,14 @@ z.object({
|
|
|
195
195
|
sets: z.array(loggedSetSchema).min(1)
|
|
196
196
|
})).min(1)
|
|
197
197
|
});
|
|
198
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Args for the coach per-athlete exercise swap: replace the exercise prescribed in one of a
|
|
200
|
+
* roster athlete's saved-workout slots with a different exercise, the API equivalent of the
|
|
201
|
+
* app's per-athlete "swap exercise". `savedWorkoutSetExerciseId` is that athlete's own slot id
|
|
202
|
+
* (the same id `coachLogSetArgsSchema` uses, read off athlete_saved_workouts raw);
|
|
203
|
+
* `exerciseId` is the replacement exercise. The team/program prescription is left untouched.
|
|
204
|
+
*/
|
|
205
|
+
const swapAthleteExerciseArgsSchema = z.object({
|
|
199
206
|
savedWorkoutSetExerciseId: idArgSchema,
|
|
200
207
|
exerciseId: idArgSchema
|
|
201
208
|
});
|
|
@@ -775,66 +782,40 @@ function nonEmpty(value) {
|
|
|
775
782
|
return value !== void 0 && value !== null && String(value).trim() !== "";
|
|
776
783
|
}
|
|
777
784
|
/**
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
785
|
+
* Slot-indexed values for one exercise. With `requireMade`, only slots the athlete logged
|
|
786
|
+
* (`param_i_made === 1`) — the performed reader; otherwise every slot carrying data — the
|
|
787
|
+
* prescription reader. Values stay raw; the projectors format or parse them.
|
|
781
788
|
*/
|
|
782
|
-
function
|
|
783
|
-
const
|
|
789
|
+
function slotValues(ex, requireMade) {
|
|
790
|
+
const map = /* @__PURE__ */ new Map();
|
|
784
791
|
for (let i = 1; i <= 10; i += 1) {
|
|
792
|
+
if (requireMade && coerceInt(ex[`param_${i}_made`]) !== 1) continue;
|
|
785
793
|
const p1 = ex[`param_1_data_${i}`];
|
|
786
794
|
const p2 = ex[`param_2_data_${i}`];
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
else out.push(`@ ${p2}`);
|
|
795
|
+
if (!nonEmpty(p1) && !nonEmpty(p2)) continue;
|
|
796
|
+
map.set(i, {
|
|
797
|
+
p1: nonEmpty(p1) ? p1 : null,
|
|
798
|
+
p2: nonEmpty(p2) ? p2 : null
|
|
799
|
+
});
|
|
793
800
|
}
|
|
794
|
-
return
|
|
801
|
+
return map;
|
|
795
802
|
}
|
|
796
|
-
/**
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const out = [];
|
|
805
|
-
for (let i = 1; i <= 10; i += 1) {
|
|
806
|
-
if (coerceInt(ex[`param_${i}_made`]) !== 1) continue;
|
|
807
|
-
const p1 = ex[`param_1_data_${i}`];
|
|
808
|
-
const p2 = ex[`param_2_data_${i}`];
|
|
809
|
-
const has1 = nonEmpty(p1);
|
|
810
|
-
const has2 = nonEmpty(p2);
|
|
811
|
-
if (has1 && has2) out.push(`${p1} @ ${p2}`);
|
|
812
|
-
else if (has1) out.push(String(p1));
|
|
813
|
-
else if (has2) out.push(`@ ${p2}`);
|
|
814
|
-
}
|
|
815
|
-
return out;
|
|
803
|
+
/** The joined display for a slot, kept verbatim (`"5 @ 225"`, `"AMRAP"`, `"@ 225"`). */
|
|
804
|
+
function fmtSlot(slot) {
|
|
805
|
+
const has1 = slot.p1 !== null;
|
|
806
|
+
const has2 = slot.p2 !== null;
|
|
807
|
+
if (has1 && has2) return `${slot.p1} @ ${slot.p2}`;
|
|
808
|
+
if (has1) return String(slot.p1);
|
|
809
|
+
if (has2) return `@ ${slot.p2}`;
|
|
810
|
+
return "";
|
|
816
811
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
return {
|
|
821
|
-
exerciseId: coerceInt(ex.exercise_id),
|
|
822
|
-
title: typeof ex.title === "string" ? ex.title : "",
|
|
823
|
-
instruction,
|
|
824
|
-
units: exerciseUnits(ex.param_1_type, ex.param_2_type),
|
|
825
|
-
prescribed: prescribedSets(ex),
|
|
826
|
-
performed: (id !== null ? performedById.get(id) : void 0) ?? []
|
|
827
|
-
};
|
|
812
|
+
/** Every slot carrying data, joined for display (the prescription reader): `["5 @ 225", "AMRAP"]`. */
|
|
813
|
+
function prescribedStrings(ex) {
|
|
814
|
+
return [...slotValues(ex, false).values()].map(fmtSlot);
|
|
828
815
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
return
|
|
832
|
-
order: coerceInt(set.order) ?? 0,
|
|
833
|
-
title: typeof set.title === "string" && set.title !== "" ? set.title : null,
|
|
834
|
-
instruction: typeof set.instruction === "string" && set.instruction !== "" ? set.instruction : null,
|
|
835
|
-
isTest: coerceInt(set.is_test) === 1,
|
|
836
|
-
exercises: exercises.filter(isRecord).map((ex) => presentExercise(ex, performedById))
|
|
837
|
-
};
|
|
816
|
+
/** The slots the athlete logged (`param_i_made === 1`), joined for display (the performed reader). */
|
|
817
|
+
function performedStrings(ex) {
|
|
818
|
+
return [...slotValues(ex, true).values()].map(fmtSlot);
|
|
838
819
|
}
|
|
839
820
|
/** Every logged set (programmed + athlete-added) in the saved copy, paired with its exercises. */
|
|
840
821
|
function savedSets(saved) {
|
|
@@ -853,54 +834,82 @@ function savedSets(saved) {
|
|
|
853
834
|
return out;
|
|
854
835
|
}
|
|
855
836
|
/**
|
|
856
|
-
* Map each prescription exercise id to the
|
|
857
|
-
* copy
|
|
858
|
-
* entered values live in the same `param_N_data` slots as a prescription
|
|
859
|
-
* prescription reader works on them unchanged.
|
|
837
|
+
* Map each prescription exercise id to the slot-indexed values the athlete logged against it. In
|
|
838
|
+
* the saved copy `workout_set_exercise_id` points back at the prescription exercise's `id`, and the
|
|
839
|
+
* entered values live in the same `param_N_data` slots as a prescription.
|
|
860
840
|
*/
|
|
861
|
-
function
|
|
841
|
+
function performedSlotsByExerciseId(sets) {
|
|
862
842
|
const map = /* @__PURE__ */ new Map();
|
|
863
843
|
for (const { exercises } of sets) for (const ex of exercises) {
|
|
864
844
|
const id = coerceInt(ex.workout_set_exercise_id);
|
|
865
845
|
if (id === null) continue;
|
|
866
|
-
const
|
|
867
|
-
if (
|
|
846
|
+
const slots = slotValues(ex, true);
|
|
847
|
+
if (slots.size > 0) map.set(id, slots);
|
|
868
848
|
}
|
|
869
849
|
return map;
|
|
870
850
|
}
|
|
871
|
-
/**
|
|
872
|
-
function
|
|
851
|
+
/** Align an exercise's prescribed slots with the slots the athlete logged, per set index. */
|
|
852
|
+
function mergeExercise(prescribed, performed, meta) {
|
|
853
|
+
const indices = [.../* @__PURE__ */ new Set([...prescribed.keys(), ...performed.keys()])].sort((a, b) => a - b);
|
|
854
|
+
return {
|
|
855
|
+
...meta,
|
|
856
|
+
sets: indices.map((i) => ({
|
|
857
|
+
set: i,
|
|
858
|
+
prescribed: prescribed.get(i) ?? null,
|
|
859
|
+
performed: performed.get(i) ?? null
|
|
860
|
+
}))
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
/** A prescription block, each exercise aligned with the slots the athlete logged against it. */
|
|
864
|
+
function mergePrescriptionBlock(set, performedById) {
|
|
865
|
+
const exercises = Array.isArray(set.workoutSetExercises) ? set.workoutSetExercises : [];
|
|
873
866
|
return {
|
|
874
867
|
order: coerceInt(set.order) ?? 0,
|
|
875
|
-
title:
|
|
876
|
-
instruction:
|
|
868
|
+
title: str(set.title),
|
|
869
|
+
instruction: str(set.instruction),
|
|
877
870
|
isTest: coerceInt(set.is_test) === 1,
|
|
878
|
-
exercises: exercises.map((ex) =>
|
|
871
|
+
exercises: exercises.filter(isRecord).map((ex) => {
|
|
872
|
+
const id = coerceInt(ex.id);
|
|
873
|
+
const performed = (id !== null ? performedById.get(id) : void 0) ?? /* @__PURE__ */ new Map();
|
|
874
|
+
return mergeExercise(slotValues(ex, false), performed, {
|
|
875
|
+
exerciseId: coerceInt(ex.exercise_id),
|
|
876
|
+
title: typeof ex.title === "string" ? ex.title : "",
|
|
877
|
+
instruction: str(ex.instruction),
|
|
878
|
+
units: exerciseUnits(ex.param_1_type, ex.param_2_type)
|
|
879
|
+
});
|
|
880
|
+
})
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
/** A logged block straight from the saved copy (athlete-added or personal work; no prescription). */
|
|
884
|
+
function mergeSavedBlock(set, exercises) {
|
|
885
|
+
return {
|
|
886
|
+
order: coerceInt(set.order) ?? 0,
|
|
887
|
+
title: str(set.title),
|
|
888
|
+
instruction: str(set.instruction),
|
|
889
|
+
isTest: coerceInt(set.is_test) === 1,
|
|
890
|
+
exercises: exercises.map((ex) => mergeExercise(/* @__PURE__ */ new Map(), slotValues(ex, true), {
|
|
879
891
|
exerciseId: coerceInt(ex.exercise_id),
|
|
880
892
|
title: typeof ex.exercise_title === "string" ? ex.exercise_title : "",
|
|
881
|
-
instruction:
|
|
882
|
-
units: exerciseUnits(ex.param_1_type, ex.param_2_type)
|
|
883
|
-
prescribed: [],
|
|
884
|
-
performed: performedSets(ex)
|
|
893
|
+
instruction: str(ex.instruction),
|
|
894
|
+
units: exerciseUnits(ex.param_1_type, ex.param_2_type)
|
|
885
895
|
}))
|
|
886
896
|
};
|
|
887
897
|
}
|
|
888
898
|
/**
|
|
889
|
-
* Flatten one `/3.0/athlete/programworkout/range` item into
|
|
899
|
+
* Flatten one `/3.0/athlete/programworkout/range` item into the canonical merged form, joining the
|
|
890
900
|
* prescription (`summarizedSavedWorkout.workout`) with what the athlete logged
|
|
891
|
-
* (`summarizedSavedWorkout.saved_workout`).
|
|
892
|
-
*
|
|
893
|
-
* own blocks. No `raw` is needed to see logged results.
|
|
901
|
+
* (`summarizedSavedWorkout.saved_workout`). Prescription blocks are enriched with logged slots;
|
|
902
|
+
* athlete-added / personal work with no prescription is appended as its own block.
|
|
894
903
|
*/
|
|
895
|
-
function
|
|
904
|
+
function mergeAthleteWorkout(raw) {
|
|
896
905
|
const rec = raw;
|
|
897
906
|
const ssw = isRecord(rec.summarizedSavedWorkout) ? rec.summarizedSavedWorkout : {};
|
|
898
907
|
const workout = isRecord(ssw.workout) ? ssw.workout : {};
|
|
899
908
|
const saved = isRecord(ssw.saved_workout) ? ssw.saved_workout : {};
|
|
900
909
|
const prescriptionSets = (Array.isArray(workout.workoutSets) ? workout.workoutSets : []).filter(isRecord);
|
|
901
910
|
const logged = savedSets(saved);
|
|
902
|
-
const performedById =
|
|
903
|
-
const blocks = prescriptionSets.map((s) =>
|
|
911
|
+
const performedById = performedSlotsByExerciseId(logged);
|
|
912
|
+
const blocks = prescriptionSets.map((s) => mergePrescriptionBlock(s, performedById)).sort((a, b) => a.order - b.order);
|
|
904
913
|
const prescribedIds = /* @__PURE__ */ new Set();
|
|
905
914
|
for (const s of prescriptionSets) {
|
|
906
915
|
const exs = Array.isArray(s.workoutSetExercises) ? s.workoutSetExercises : [];
|
|
@@ -912,11 +921,11 @@ function presentAthleteWorkout(raw) {
|
|
|
912
921
|
}
|
|
913
922
|
for (const { set, exercises } of logged) {
|
|
914
923
|
const extra = exercises.filter((ex) => {
|
|
915
|
-
if (
|
|
924
|
+
if (slotValues(ex, true).size === 0) return false;
|
|
916
925
|
const id = coerceInt(ex.workout_set_exercise_id);
|
|
917
926
|
return id === null || !prescribedIds.has(id);
|
|
918
927
|
});
|
|
919
|
-
if (extra.length > 0) blocks.push(
|
|
928
|
+
if (extra.length > 0) blocks.push(mergeSavedBlock(set, extra));
|
|
920
929
|
}
|
|
921
930
|
return {
|
|
922
931
|
id: coerceInt(rec.id),
|
|
@@ -925,11 +934,58 @@ function presentAthleteWorkout(raw) {
|
|
|
925
934
|
program: str(rec.program_title),
|
|
926
935
|
team: str(rec.team_title),
|
|
927
936
|
instruction: str(workout.instruction),
|
|
928
|
-
logged: blocks.some((b) => b.exercises.some((e) => e.
|
|
937
|
+
logged: blocks.some((b) => b.exercises.some((e) => e.sets.some((s) => s.performed !== null))),
|
|
929
938
|
personal: isPersonalSession(rec),
|
|
930
939
|
blocks
|
|
931
940
|
};
|
|
932
941
|
}
|
|
942
|
+
/** The per-set displays for one side (prescribed or performed), in slot order. */
|
|
943
|
+
function sideStrings(sets, pick) {
|
|
944
|
+
const out = [];
|
|
945
|
+
for (const s of sets) {
|
|
946
|
+
const slot = pick(s);
|
|
947
|
+
if (slot) out.push(fmtSlot(slot));
|
|
948
|
+
}
|
|
949
|
+
return out;
|
|
950
|
+
}
|
|
951
|
+
function toStringExercise(ex) {
|
|
952
|
+
return {
|
|
953
|
+
exerciseId: ex.exerciseId,
|
|
954
|
+
title: ex.title,
|
|
955
|
+
instruction: ex.instruction,
|
|
956
|
+
units: ex.units,
|
|
957
|
+
prescribed: sideStrings(ex.sets, (s) => s.prescribed),
|
|
958
|
+
performed: sideStrings(ex.sets, (s) => s.performed)
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function toStringBlock(b) {
|
|
962
|
+
return {
|
|
963
|
+
order: b.order,
|
|
964
|
+
title: b.title,
|
|
965
|
+
instruction: b.instruction,
|
|
966
|
+
isTest: b.isTest,
|
|
967
|
+
exercises: b.exercises.map(toStringExercise)
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Flatten one range item into a readable workout: each exercise carries its `prescribed` and
|
|
972
|
+
* `performed` sets as joined `"5 @ 225"` strings; athlete-added/personal work that has no
|
|
973
|
+
* prescription is appended as its own blocks. No `raw` is needed to see logged results.
|
|
974
|
+
*/
|
|
975
|
+
function presentAthleteWorkout(raw) {
|
|
976
|
+
const w = mergeAthleteWorkout(raw);
|
|
977
|
+
return {
|
|
978
|
+
id: w.id,
|
|
979
|
+
date: w.date,
|
|
980
|
+
title: w.title,
|
|
981
|
+
program: w.program,
|
|
982
|
+
team: w.team,
|
|
983
|
+
instruction: w.instruction,
|
|
984
|
+
logged: w.logged,
|
|
985
|
+
personal: w.personal,
|
|
986
|
+
blocks: w.blocks.map(toStringBlock)
|
|
987
|
+
};
|
|
988
|
+
}
|
|
933
989
|
function presentAthleteWorkouts(list) {
|
|
934
990
|
return list.map(presentAthleteWorkout);
|
|
935
991
|
}
|
|
@@ -989,8 +1045,8 @@ function presentLogTargets(list) {
|
|
|
989
1045
|
savedWorkoutSetExerciseId: id,
|
|
990
1046
|
title: exerciseTitle(ex),
|
|
991
1047
|
units: exerciseUnits(ex.param_1_type, ex.param_2_type),
|
|
992
|
-
prescribed:
|
|
993
|
-
performed:
|
|
1048
|
+
prescribed: prescribedStrings(ex),
|
|
1049
|
+
performed: performedStrings(ex)
|
|
994
1050
|
};
|
|
995
1051
|
}).filter((e) => e !== null);
|
|
996
1052
|
targets.push({
|
|
@@ -1009,10 +1065,11 @@ function presentLogTargets(list) {
|
|
|
1009
1065
|
return targets;
|
|
1010
1066
|
}
|
|
1011
1067
|
/**
|
|
1012
|
-
* Narrow a
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1068
|
+
* Narrow a workout list for the common "what did I actually do" reads. `loggedOnly` keeps only
|
|
1069
|
+
* workouts the athlete logged a set on (the reliable signal, not the API's completion flag).
|
|
1070
|
+
* `limit` keeps the most recent N by date (newest first). Both are pure post-filters; the raw API
|
|
1071
|
+
* path is left untouched. Generic over the presented (`AthleteWorkoutView`) and exported
|
|
1072
|
+
* (`WorkoutHistoryExport`) shapes, which both carry `date` and `logged`, so one rule serves both.
|
|
1016
1073
|
*/
|
|
1017
1074
|
function selectWorkouts(list, opts = {}) {
|
|
1018
1075
|
let out = opts.loggedOnly === true ? list.filter((w) => w.logged) : [...list];
|
|
@@ -1309,6 +1366,41 @@ async function logForAthlete(client, args) {
|
|
|
1309
1366
|
};
|
|
1310
1367
|
}
|
|
1311
1368
|
/**
|
|
1369
|
+
* Swap the exercise prescribed in one of an athlete's saved-workout slots for a different
|
|
1370
|
+
* exercise — the API equivalent of the app's per-athlete "swap exercise":
|
|
1371
|
+
* `PUT /v5/savedWorkoutSetExercises/{savedWorkoutSetExerciseId}?exerciseId={exerciseId}` with an
|
|
1372
|
+
* empty body. The new exercise rides in the query string, not the body.
|
|
1373
|
+
*
|
|
1374
|
+
* This overrides only this athlete's copy of the slot; the underlying team/program prescription
|
|
1375
|
+
* (`workout_set_exercise.exercise_id`) is untouched, so other athletes on the same program keep
|
|
1376
|
+
* the original exercise. A coach's session token may write another user's row because the row
|
|
1377
|
+
* already carries its owner's `user_id`.
|
|
1378
|
+
*
|
|
1379
|
+
* `savedWorkoutSetExerciseId` is the same id {@link logForAthlete}/{@link logAthleteSet} use,
|
|
1380
|
+
* read off the athlete's saved workouts (the raw view). `exerciseId` is any exercise id the org
|
|
1381
|
+
* can use (resolve one via the exercise index).
|
|
1382
|
+
*
|
|
1383
|
+
* NOTE: as with logging, TrainHeroic's seeded *demo* athletes are read-only and return 401/403;
|
|
1384
|
+
* real (invited) athletes accept the swap.
|
|
1385
|
+
*/
|
|
1386
|
+
async function swapAthleteExercise(client, args) {
|
|
1387
|
+
const res = await client.request("PUT", `/v5/savedWorkoutSetExercises/${args.savedWorkoutSetExerciseId}?exerciseId=${args.exerciseId}`);
|
|
1388
|
+
if (!res.ok) {
|
|
1389
|
+
const readOnly = res.status === 401 || res.status === 403 ? ` Athlete may be read-only for changes — TrainHeroic's seeded demo/sample athletes return ${res.status} here; swaps only persist for real (invited) athletes.` : "";
|
|
1390
|
+
throw new Error(`Swap failed (HTTP ${res.status}) for savedWorkoutSetExercise ${args.savedWorkoutSetExerciseId}.${readOnly}`);
|
|
1391
|
+
}
|
|
1392
|
+
const row = isRecord(res.data) ? res.data : {};
|
|
1393
|
+
const template = isRecord(row.workout_set_exercise) ? row.workout_set_exercise : {};
|
|
1394
|
+
const exercise = isRecord(row.exercise) ? row.exercise : {};
|
|
1395
|
+
return {
|
|
1396
|
+
savedWorkoutSetExerciseId: args.savedWorkoutSetExerciseId,
|
|
1397
|
+
athleteId: coerceInt(row.user_id),
|
|
1398
|
+
newExerciseId: coerceInt(row.exercise_id) ?? args.exerciseId,
|
|
1399
|
+
newExerciseTitle: str(exercise.title),
|
|
1400
|
+
originalTeamExerciseId: coerceInt(template.exercise_id)
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1312
1404
|
* Shared set-write behind {@link logAthleteSet}, {@link logForAthlete}, and
|
|
1313
1405
|
* {@link prescribeForAthlete}. `target` selects the surface: `athlete` writes `/1.0/athlete/...`;
|
|
1314
1406
|
* `coach` writes `/1.0/coach/...{athleteId}` and stamps `athleteId` into each body.
|
|
@@ -1889,6 +1981,31 @@ function registerLogTool(server, ctx) {
|
|
|
1889
1981
|
}));
|
|
1890
1982
|
}
|
|
1891
1983
|
/**
|
|
1984
|
+
* The athlete's own per-exercise swap, in its own function so registerLogTool/registerSessionTools
|
|
1985
|
+
* stay under the oxlint max-lines-per-function cap. Lets an athlete substitute a prescribed
|
|
1986
|
+
* exercise in a coach-scheduled workout for a different one — the self-service equivalent of the
|
|
1987
|
+
* app's "swap exercise", overriding only the athlete's own copy of that slot.
|
|
1988
|
+
*/
|
|
1989
|
+
function registerSwapTool(server, ctx) {
|
|
1990
|
+
server.registerTool("athlete_swap_exercise", {
|
|
1991
|
+
title: "Swap one exercise in your scheduled workout",
|
|
1992
|
+
description: "Athlete-facing write: substitute one prescribed exercise in your scheduled workout for a different one — the API equivalent of the app's \"swap exercise\". This is the way to change which movement a slot is BEFORE logging: session_add/remove only touch personal sessions, and athlete_log_set records results into a slot without changing its exercise, so use this when a COACH-SCHEDULED slot should be a different lift. It overrides only your copy of the slot; the team/program prescription is untouched. Give savedWorkoutSetExerciseId (the slot to change, from athlete_log_targets — each row lists it per exercise) and exerciseId (the replacement, from athlete_exercises). After swapping, log against the slot as usual with athlete_log_set. Requires confirmation (elicitation or confirm:true).",
|
|
1993
|
+
inputSchema: {
|
|
1994
|
+
...swapAthleteExerciseArgsSchema.shape,
|
|
1995
|
+
confirm: z.boolean().optional()
|
|
1996
|
+
},
|
|
1997
|
+
annotations: DESTRUCTIVE
|
|
1998
|
+
}, ({ savedWorkoutSetExerciseId, exerciseId, confirm }, extra) => attempt(async () => {
|
|
1999
|
+
const sweId = toId(savedWorkoutSetExerciseId);
|
|
2000
|
+
const exId = toId(exerciseId);
|
|
2001
|
+
if (!await confirmGate(server, extra.requestId, `Swap the exercise in your saved workout slot ${sweId} to exercise ${exId}? This changes what that slot is prescribed (your copy only).`, confirm)) return errorResult(NOT_CONFIRMED);
|
|
2002
|
+
return jsonResult(await swapAthleteExercise(ctx.client, {
|
|
2003
|
+
savedWorkoutSetExerciseId: sweId,
|
|
2004
|
+
exerciseId: exId
|
|
2005
|
+
}));
|
|
2006
|
+
}));
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
1892
2009
|
* Live tools over the logged-in user's own training (history, scheduled/completed workouts,
|
|
1893
2010
|
* PRs, working maxes), plus a gated set-logging write. The athlete user id is
|
|
1894
2011
|
* resolved once from /user/simple and reused across tools.
|
|
@@ -1913,10 +2030,11 @@ function registerAthleteTrainingTools(server, ctx) {
|
|
|
1913
2030
|
registerLogTargetsTool(server, ctx);
|
|
1914
2031
|
registerSessionTools(server, ctx);
|
|
1915
2032
|
registerLogTool(server, ctx);
|
|
2033
|
+
registerSwapTool(server, ctx);
|
|
1916
2034
|
}
|
|
1917
2035
|
//#endregion
|
|
1918
2036
|
//#region package.json
|
|
1919
|
-
var version = "1.
|
|
2037
|
+
var version = "1.7.0";
|
|
1920
2038
|
//#endregion
|
|
1921
2039
|
//#region src/server.ts
|
|
1922
2040
|
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.7.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
23
23
|
"zod": "^4.4.3",
|
|
24
|
-
"@trainheroic-unofficial/
|
|
25
|
-
"@trainheroic-unofficial/
|
|
24
|
+
"@trainheroic-unofficial/js": "1.7.0",
|
|
25
|
+
"@trainheroic-unofficial/core": "1.7.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^26.0.
|
|
28
|
+
"@types/node": "^26.0.1",
|
|
29
29
|
"tsdown": "^0.22.3",
|
|
30
30
|
"tsx": "^4.22.4",
|
|
31
31
|
"typescript": "^6.0.3",
|