@trainheroic-unofficial/athlete-mcp 0.6.2 → 0.6.3

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 +42 -14
  2. package/package.json +3 -3
package/dist/server.mjs CHANGED
@@ -837,6 +837,28 @@ function selectWorkouts(list, opts = {}) {
837
837
  if (opts.limit !== void 0) out = [...out].sort((a, b) => a.date < b.date ? 1 : a.date > b.date ? -1 : 0).slice(0, opts.limit);
838
838
  return out;
839
839
  }
840
+ /**
841
+ * Project presented workouts to compact headers (date/title/program + logged flag + exercise
842
+ * counts), dropping the per-set block detail. A dense multi-program week of full views can be
843
+ * tens of KB; this keeps an overview question to one small row per session, with a drill-in
844
+ * (narrow the date range) when the detail is actually needed.
845
+ */
846
+ function summarizeAthleteWorkouts(list) {
847
+ return list.map((w) => {
848
+ const exerciseCount = w.blocks.reduce((n, b) => n + b.exercises.length, 0);
849
+ const performedCount = w.blocks.reduce((n, b) => n + b.exercises.filter((e) => e.performed.length > 0).length, 0);
850
+ return {
851
+ id: w.id,
852
+ date: w.date,
853
+ title: w.title,
854
+ program: w.program,
855
+ team: w.team,
856
+ logged: w.logged,
857
+ exerciseCount,
858
+ performedCount
859
+ };
860
+ });
861
+ }
840
862
  const MAX_PARAM_SLOTS = 10;
841
863
  /**
842
864
  * Build the body for `PUT /1.0/athlete/savedworkoutsetexercise/{id}`. The body uses
@@ -1103,9 +1125,21 @@ function registerProfileTools(server, ctx, whoami, userId) {
1103
1125
  return jsonResult(await fetchLeaderboard(ctx.client, toId(workoutId), opts));
1104
1126
  }));
1105
1127
  }
1106
- const ATHLETE_WORKOUTS_DESC = "Workouts in an inclusive YYYY-MM-DD window, flattened to blocks/exercises. Each exercise carries both its `prescribed` sets (what the program called for) and `performed` sets (what the athlete actually logged); each workout has a top-level `logged` flag. Use `performed`/`logged` to tell what was recorded or done. That is the reliable signal: a session can hold logged sets while the API's own completion flags stay 0, and an empty `performed` means nothing was logged for that exercise. For 'did I record anything / what did I do', set loggedOnly:true to return only sessions with logged sets (it keeps whole sessions that have any logged set, so individual exercises inside can still show empty `performed`; it also shrinks a large result); limit returns the most recent N workouts (newest first). For 'what's my next workout', query a forward window from today; if it comes back empty, the most recent session is likely yesterday's still-unlogged one, so widen the window backward a day or two. Both filters apply to the presented view, not raw. raw:true returns the untouched API objects. This is a date-windowed fetch, NOT an aggregate: for lifetime totals (all-time session count, total volume, first/last logged date) call athlete_profile instead of summing windows — a multi-year range here can time out. Narrow the window if the result is truncated.";
1107
- const ATHLETE_EXERCISE_HISTORY_DESC = "Per-exercise PRs and the dated session time-series (sets performed, estimated 1RM). The returned `sessions` run all-time, newest first; pass since/until (YYYY-MM-DD, inclusive) to keep only sessions in that window — use it for 'last 3 months' / 'this year' questions so the result stays small. `liftPRs` are always all-time and ignore since/until. Set raw:true for the untouched API object. Get the exercise id from athlete_exercises.";
1108
- const ATHLETE_EXERCISES_DESC = "The exercises the athlete has logged (id + title + positional units). Pass q to free-text search by name; use the returned id with athlete_exercise_history, athlete_personal_records, or athlete_exercise_stats (all of which require an exercise id).";
1128
+ const ATHLETE_WORKOUTS_DESC = "Workouts in an inclusive YYYY-MM-DD window, flattened to blocks/exercises. Each exercise carries both its `prescribed` sets (what the program called for) and `performed` sets (what the athlete actually logged); each workout has a top-level `logged` flag. Use `performed`/`logged` to tell what was recorded or done. That is the reliable signal: a session can hold logged sets while the API's own completion flags stay 0, and an empty `performed` means nothing was logged for that exercise. For 'did I record anything / what did I do', set loggedOnly:true to return only sessions with logged sets (it keeps whole sessions that have any logged set, so individual exercises inside can still show empty `performed`; it also shrinks a large result); limit returns the most recent N workouts (newest first). For a high-level overview ('what's on my schedule this week', 'what have I been training lately') set summary:true to get one compact row per session (date, program, title, logged flag, and exerciseCount/performedCount — read performedCount against exerciseCount, e.g. 1 of 12 logged) instead of every set — a multi-program week of full detail is large, so prefer summary first, then re-query a single day without it to drill into that day's sets. For 'what's my next workout', query a forward window from today; if it comes back empty, the most recent session is likely yesterday's still-unlogged one, so widen the window backward a day or two. Both filters apply to the presented view, not raw. raw:true returns the untouched API objects. This is a date-windowed fetch, NOT an aggregate: for lifetime totals (all-time session count, total volume, first/last logged date) call athlete_profile instead of summing windows — a multi-year range here can time out. Narrow the window if the result is truncated.";
1129
+ const ATHLETE_EXERCISE_HISTORY_DESC = "Per-exercise PRs and the dated session time-series (sets performed, estimated 1RM). The returned `sessions` run all-time, newest first; pass since/until (YYYY-MM-DD, inclusive) to keep only sessions in that window — use it for 'last 3 months' / 'this year' questions so the result stays small. estimated1RM is a formula off the session's best set and reads high when a session mixes a heavy single with high-rep backdown work, so treat it as approximate, not a logged single. `liftPRs` are always all-time and ignore since/until, but each carries the date it was set, so filter those dates yourself to answer 'PRs set this year'. Set raw:true for the untouched API object. Get the exercise id from athlete_exercises.";
1130
+ const ATHLETE_EXERCISES_DESC = "The exercises the athlete has logged (id + title + positional units). Pass q to free-text search by name; use the returned id with athlete_exercise_history, athlete_personal_records, or athlete_exercise_stats (all of which require an exercise id). A common name returns several variants (e.g. plain 'Bench Press' id 1162 vs 'BARBELL BENCH PRESS'); each variant keeps its own separate history and PR board, so prefer the plain canonical entry, and if a PR or trend looks incomplete, the work may be split across variants — check the others.";
1131
+ function runAthleteWorkouts(ctx, args) {
1132
+ return attempt(async () => {
1133
+ const workouts = await fetchAthleteWorkouts(ctx.client, args.startDate, args.endDate);
1134
+ if (args.raw === true) return jsonResult(workouts, { hint: "Narrow startDate/endDate to shrink this result." });
1135
+ const opts = {};
1136
+ if (args.loggedOnly !== void 0) opts.loggedOnly = args.loggedOnly;
1137
+ if (args.limit !== void 0) opts.limit = args.limit;
1138
+ const selected = selectWorkouts(presentAthleteWorkouts(workouts), opts);
1139
+ if (args.summary === true) return jsonResult(summarizeAthleteWorkouts(selected), { hint: "Compact per-session overview. Re-query a narrow startDate/endDate without summary for a day's full prescribed/performed sets." });
1140
+ return jsonResult(selected, { hint: "Large? Set summary:true for one row per session, loggedOnly:true, pass limit, or narrow the dates." });
1141
+ });
1142
+ }
1109
1143
  /** Workouts, exercise catalog, per-exercise history/PRs/stats. */
1110
1144
  function registerExerciseTools(server, ctx, userId) {
1111
1145
  server.registerTool("athlete_workouts", {
@@ -1116,17 +1150,11 @@ function registerExerciseTools(server, ctx, userId) {
1116
1150
  endDate: dateString,
1117
1151
  raw: z.boolean().optional(),
1118
1152
  loggedOnly: z.boolean().optional(),
1119
- limit: z.number().int().positive().max(200).optional()
1153
+ limit: z.number().int().positive().max(200).optional(),
1154
+ summary: z.boolean().optional()
1120
1155
  },
1121
1156
  annotations: READ
1122
- }, ({ startDate, endDate, raw, loggedOnly, limit }) => attempt(async () => {
1123
- const workouts = await fetchAthleteWorkouts(ctx.client, startDate, endDate);
1124
- if (raw === true) return jsonResult(workouts, { hint: "Narrow startDate/endDate to shrink this result." });
1125
- const opts = {};
1126
- if (loggedOnly !== void 0) opts.loggedOnly = loggedOnly;
1127
- if (limit !== void 0) opts.limit = limit;
1128
- return jsonResult(selectWorkouts(presentAthleteWorkouts(workouts), opts), { hint: "Large? Set loggedOnly:true, pass limit, or narrow startDate/endDate." });
1129
- }));
1157
+ }, (args) => runAthleteWorkouts(ctx, args));
1130
1158
  server.registerTool("athlete_exercises", {
1131
1159
  title: "Search logged exercises",
1132
1160
  description: ATHLETE_EXERCISES_DESC,
@@ -1161,7 +1189,7 @@ function registerExerciseTools(server, ctx, userId) {
1161
1189
  }));
1162
1190
  server.registerTool("athlete_personal_records", {
1163
1191
  title: "Exercise personal records",
1164
- description: "The all-time PR board for an exercise (reps/weight per rep-max, strength-standard filters). Get the exercise id from athlete_exercises. For a point-in-time snapshot use athlete_exercise_stats; for the dated session trend use athlete_exercise_history.",
1192
+ description: "The all-time PR board for an exercise (reps/weight per rep-max, strength-standard filters). Get the exercise id from athlete_exercises. A lift is often logged under several name variants, each with its own board, so if a PR looks missing check the other variants. For a point-in-time snapshot use athlete_exercise_stats; for the dated session trend use athlete_exercise_history.",
1165
1193
  inputSchema: { exerciseId: idParam },
1166
1194
  annotations: READ
1167
1195
  }, ({ exerciseId }) => attempt(async () => jsonResult(await fetchPersonalRecords(ctx.client, toId(exerciseId)))));
@@ -1265,7 +1293,7 @@ function registerAthleteTrainingTools(server, ctx) {
1265
1293
  }
1266
1294
  //#endregion
1267
1295
  //#region package.json
1268
- var version = "0.6.2";
1296
+ var version = "0.6.3";
1269
1297
  //#endregion
1270
1298
  //#region src/server.ts
1271
1299
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trainheroic-unofficial/athlete-mcp",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
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.2",
25
- "@trainheroic-unofficial/js": "0.6.2"
24
+ "@trainheroic-unofficial/core": "0.6.3",
25
+ "@trainheroic-unofficial/js": "0.6.3"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^26.0.0",