@trainheroic-unofficial/athlete-mcp 3.0.0 → 3.1.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/README.md CHANGED
@@ -4,7 +4,7 @@ Local single-user [MCP](https://modelcontextprotocol.io) server for a TrainHeroi
4
4
 
5
5
  It reads two required environment variables, `TRAINHEROIC_EMAIL` and `TRAINHEROIC_PASSWORD` (your existing TrainHeroic login). These are real credentials in plaintext; the config below puts them in a file or shell history, so treat them as secrets.
6
6
 
7
- It exposes the logged-in athlete's own training: scheduled and completed workouts, per-exercise history, PRs (personal records), working maxes, and lifetime totals, plus one confirmation-gated write that logs a completed set. Coaching capabilities (rosters, teams, programs, messaging) are available through [`@trainheroic-unofficial/coach-mcp`](../coach-mcp). A hosted version that holds credentials server-side behind OAuth and gives a coach login both the athlete and coaching tools is described in the [root README](../../README.md).
7
+ It exposes the logged-in athlete's own training: scheduled and completed workouts, per-exercise history, PRs (personal records), working maxes, lifetime totals, and confirmation-gated training writes. Coaching capabilities (rosters, teams, programs, messaging) are available through [`@trainheroic-unofficial/coach-mcp`](../coach-mcp). A hosted version that holds credentials server-side behind OAuth and gives a coach login both the athlete and coaching tools is described in the [root README](../../README.md).
8
8
 
9
9
  ---
10
10
 
@@ -48,8 +48,8 @@ A coach account works here too: a TrainHeroic coach account also has its own ath
48
48
 
49
49
  ## Tools
50
50
 
51
- All read-only except `athlete_log_set`. The assistant fills in each tool's parameters (dates
52
- are `YYYY-MM-DD`); your MCP client shows the full schema for each.
51
+ The assistant fills in each tool's parameters (dates are `YYYY-MM-DD`); your MCP client shows the
52
+ full schema for each. Writes are confirmation-gated.
53
53
 
54
54
  - `athlete_whoami`: identity (id, name, roles)
55
55
  - `athlete_profile`: lifetime totals + profile
@@ -67,6 +67,9 @@ are `YYYY-MM-DD`); your MCP client shows the full schema for each.
67
67
  - `athlete_log_set`: logs completed set results to your training log. This is a real write
68
68
  that your coach can see. It confirms before running: the server asks the client to confirm,
69
69
  falling back to an explicit `confirm: true` argument when the client can't prompt.
70
+ - `athlete_prescribe_set`: sets your planned reps and/or weight without recording the set as
71
+ completed. It replaces the selected exercise's whole prescription, so every planned set and
72
+ value to keep must be included.
70
73
 
71
74
  ---
72
75
 
package/dist/server.mjs CHANGED
@@ -203,12 +203,18 @@ const logSetArgsSchema = z.object({
203
203
  results: loggedExerciseResultsSchema
204
204
  });
205
205
  logSetArgsSchema.extend({ athleteId: idArgSchema });
206
- z.object({
206
+ /**
207
+ * Args for changing the logged-in athlete's prescribed targets without marking the set performed.
208
+ * The write replaces the exercise's whole prescription, so its sets are positional and sequential
209
+ * by definition: it deliberately omits the log path's `slot` field because a sparse prescription
210
+ * has no meaning here.
211
+ */
212
+ const athletePrescribeSetArgsSchema = z.object({
207
213
  date: dateString,
208
214
  savedWorkoutSetId: idArgSchema,
209
- athleteId: idArgSchema,
210
215
  results: prescribedExerciseResultsSchema
211
216
  });
217
+ athletePrescribeSetArgsSchema.extend({ athleteId: idArgSchema });
212
218
  /**
213
219
  * Args for the coach per-athlete exercise swap: replace the exercise prescribed in one of a
214
220
  * roster athlete's saved-workout slots with a different exercise, the API equivalent of the
@@ -822,7 +828,7 @@ async function getJson(client, path, label) {
822
828
  if (!res.ok) throw new Error(`${label} failed (HTTP ${res.status}).`);
823
829
  return res.data;
824
830
  }
825
- async function getArray$1(client, path, label) {
831
+ async function getArray(client, path, label) {
826
832
  const res = await client.request("GET", path);
827
833
  if (!res.ok || !Array.isArray(res.data)) throw new Error(`${label} failed (HTTP ${res.status}).`);
828
834
  return res.data;
@@ -838,10 +844,10 @@ function fetchAthletePrefs(client) {
838
844
  return getJson(client, "/1.0/athlete/prefs", "athlete prefs");
839
845
  }
840
846
  function fetchWorkingMaxes(client) {
841
- return getArray$1(client, "/2.0/athlete/workingMax", "athlete working maxes");
847
+ return getArray(client, "/2.0/athlete/workingMax", "athlete working maxes");
842
848
  }
843
849
  function fetchExerciseHistoryList(client) {
844
- return getArray$1(client, "/v5/users/exercises/history", "athlete exercise history list");
850
+ return getArray(client, "/v5/users/exercises/history", "athlete exercise history list");
845
851
  }
846
852
  /** Free-text search over the athlete's logged exercises (FTS replacement via rankSearch). */
847
853
  async function searchExerciseHistory(client, query, limit = 20) {
@@ -851,7 +857,7 @@ function fetchExerciseHistoryDetail(client, exerciseId, userId) {
851
857
  return getJson(client, `/v5/exercises/${exerciseId}/history?userId=${userId}`, "athlete exercise history");
852
858
  }
853
859
  function fetchPersonalRecords(client, exerciseId) {
854
- return getArray$1(client, `/v5/exercises/${exerciseId}/personalRecords`, "athlete personal records");
860
+ return getArray(client, `/v5/exercises/${exerciseId}/personalRecords`, "athlete personal records");
855
861
  }
856
862
  /** Last performance + PR for an exercise. `date` (YYYY-MM-DD) is required by the API. */
857
863
  function fetchExerciseStats(client, exerciseId, userId, date) {
@@ -859,7 +865,7 @@ function fetchExerciseStats(client, exerciseId, userId, date) {
859
865
  }
860
866
  /** Scheduled + completed workouts in an inclusive YYYY-MM-DD window. */
861
867
  function fetchAthleteWorkouts(client, startDate, endDate) {
862
- return getArray$1(client, `/3.0/athlete/programworkout/range?startDate=${startDate}&endDate=${endDate}`, "athlete workouts");
868
+ return getArray(client, `/3.0/athlete/programworkout/range?startDate=${startDate}&endDate=${endDate}`, "athlete workouts");
863
869
  }
864
870
  function fetchLeaderboard(client, workoutId, opts = {}) {
865
871
  const qs = new URLSearchParams();
@@ -1212,22 +1218,6 @@ function presentExerciseHistory(detail) {
1212
1218
  };
1213
1219
  }
1214
1220
  //#endregion
1215
- //#region ../js/src/coach-athlete-calendar.ts
1216
- async function getArray(client, path, label) {
1217
- const res = await client.request("GET", path);
1218
- if (!res.ok || !Array.isArray(res.data)) throw new Error(`${label} failed (HTTP ${res.status}).`);
1219
- return res.data;
1220
- }
1221
- /**
1222
- * A coach's view of a roster athlete's scheduled + completed workouts in an inclusive
1223
- * YYYY-MM-DD window (`/3.0/coach/athlete/programworkout/range/{athleteId}`). Returns the same
1224
- * `ProgramWorkout[]` shape as `fetchAthleteWorkouts`, so the same presenters and
1225
- * `findSavedWorkoutSet` apply — it just reads another athlete's data through the coach surface.
1226
- */
1227
- function fetchCoachAthleteWorkouts(client, athleteId, startDate, endDate) {
1228
- return getArray(client, `/3.0/coach/athlete/programworkout/range/${athleteId}?startDate=${startDate}&endDate=${endDate}`, "coach athlete workouts");
1229
- }
1230
- //#endregion
1231
1221
  //#region ../js/src/exercise-set-payload.ts
1232
1222
  function slotData(exercise, key) {
1233
1223
  const value = exercise?.[key];
@@ -1443,25 +1433,18 @@ async function logAthleteSet(client, args) {
1443
1433
  };
1444
1434
  }
1445
1435
  /**
1446
- * Coach "Log for Athlete": record set results for a roster athlete on their behalf, via the
1447
- * coach surface `PUT /1.0/coach/savedworkoutsetexercise/{id}/{athleteId}` (the data write)
1448
- * then `PUT /1.0/coach/savedworkoutset/{id}/{athleteId}` (mark complete). Same two-step
1449
- * contract as {@link logAthleteSet}; the bodies are identical except each is stamped with
1450
- * `athleteId`. The day is located through the coach range endpoint for that athlete.
1436
+ * Set the logged-in athlete's prescribed reps/weight for one saved workout set WITHOUT marking it
1437
+ * performed. This is the athlete-side equivalent of editing target values in the app: it writes
1438
+ * `param_N_data_M` with every made/completed flag cleared and skips the set-completion PUT.
1451
1439
  *
1452
- * NOTE: TrainHeroic's seeded *demo* athletes are read-only for results and return 401 on the
1453
- * data-write step; real (invited) athletes accept it.
1440
+ * The write replaces the selected exercise's whole prescription, so callers must pass every set
1441
+ * they want to keep. An omitted param is cleared rather than preserved.
1454
1442
  */
1455
- async function logForAthlete(client, args) {
1456
- const workouts = await fetchCoachAthleteWorkouts(client, args.athleteId, args.date, args.date);
1457
- const r = await writeSetResults(client, {
1458
- role: "coach",
1459
- athleteId: args.athleteId
1460
- }, workouts, args.savedWorkoutSetId, args.results, "log");
1443
+ async function prescribeAthleteSet(client, args) {
1444
+ const r = await writeSetResults(client, { role: "athlete" }, await fetchAthleteWorkouts(client, args.date, args.date), args.savedWorkoutSetId, args.results, "prescribe");
1461
1445
  return {
1462
1446
  savedWorkoutSetId: r.savedWorkoutSetId,
1463
- exercisesLogged: r.exercisesWritten,
1464
- setCompleted: r.setCompleted
1447
+ exercisesPrescribed: r.exercisesWritten
1465
1448
  };
1466
1449
  }
1467
1450
  /**
@@ -1500,9 +1483,10 @@ async function swapAthleteExercise(client, args) {
1500
1483
  };
1501
1484
  }
1502
1485
  /**
1503
- * Shared set-write behind {@link logAthleteSet}, {@link logForAthlete}, and
1504
- * {@link prescribeForAthlete}. `target` selects the surface: `athlete` writes `/1.0/athlete/...`;
1505
- * `coach` writes `/1.0/coach/...{athleteId}` and stamps `athleteId` into each body.
1486
+ * Shared set-write behind {@link logAthleteSet}, {@link prescribeAthleteSet},
1487
+ * {@link logForAthlete}, and {@link prescribeForAthlete}. `target` selects the surface: `athlete`
1488
+ * writes `/1.0/athlete/...`; `coach` writes `/1.0/coach/...{athleteId}` and stamps `athleteId` into
1489
+ * each body.
1506
1490
  *
1507
1491
  * Step 1 PUTs each exercise's per-set values to its own endpoint (the only path that actually
1508
1492
  * stores reps and weight). `mode` decides what those values mean: `"log"` records them as a
@@ -1707,7 +1691,7 @@ function findScheduledMatches(workouts, exerciseIds) {
1707
1691
  return out;
1708
1692
  }
1709
1693
  /** Group resolved exercises by saved set and write each set via the given log target. */
1710
- async function logResolvedExercises(client, target, date, resolved) {
1694
+ async function logResolvedExercises(client, target, workouts, resolved) {
1711
1695
  const bySet = /* @__PURE__ */ new Map();
1712
1696
  for (const r of resolved) {
1713
1697
  const list = bySet.get(r.savedWorkoutSetId) ?? [];
@@ -1719,19 +1703,10 @@ async function logResolvedExercises(client, target, date, resolved) {
1719
1703
  }
1720
1704
  const out = [];
1721
1705
  for (const [savedWorkoutSetId, results] of bySet) {
1722
- const written = target.role === "coach" ? await logForAthlete(client, {
1723
- athleteId: target.athleteId,
1724
- date,
1725
- savedWorkoutSetId,
1726
- results
1727
- }) : await logAthleteSet(client, {
1728
- date,
1729
- savedWorkoutSetId,
1730
- results
1731
- });
1706
+ const written = await writeSetResults(client, target, workouts, savedWorkoutSetId, results, "log");
1732
1707
  out.push({
1733
1708
  savedWorkoutSetId: written.savedWorkoutSetId,
1734
- exercisesLogged: written.exercisesLogged
1709
+ exercisesLogged: written.exercisesWritten
1735
1710
  });
1736
1711
  }
1737
1712
  return out;
@@ -1769,7 +1744,7 @@ async function logAdHocSession(client, args) {
1769
1744
  sets: e.sets
1770
1745
  };
1771
1746
  });
1772
- const sets = await logResolvedExercises(client, { role: "athlete" }, args.date, resolved);
1747
+ const sets = await logResolvedExercises(client, { role: "athlete" }, await fetchAthleteWorkouts(client, args.date, args.date), resolved);
1773
1748
  const result = {
1774
1749
  date: args.date,
1775
1750
  created,
@@ -2094,6 +2069,24 @@ function registerLogTool(server, ctx) {
2094
2069
  results: toSetResults(results)
2095
2070
  }));
2096
2071
  }));
2072
+ server.registerTool("athlete_prescribe_set", {
2073
+ title: "Set your prescribed reps/weight without logging",
2074
+ description: "Athlete-facing write: set the planned reps and/or weight for one of your scheduled workout sets WITHOUT recording it as performed. Use this when you know the target load before training but have not completed the set yet. The set stays open and nothing is added to exercise history. param1 is reps and param2 is weight; pass one sets[] entry per planned set. This write REPLACES the selected exercise's whole prescription, so include every planned set and every value you want to keep; an omitted param is cleared. Get savedWorkoutSetId + savedWorkoutSetExerciseId from athlete_log_targets. To record actual completed results, use athlete_log_set instead. Requires confirmation (elicitation or confirm:true).",
2075
+ inputSchema: {
2076
+ ...athletePrescribeSetArgsSchema.shape,
2077
+ confirm: z.boolean().optional()
2078
+ },
2079
+ annotations: DESTRUCTIVE
2080
+ }, ({ date, savedWorkoutSetId, results, confirm }, extra) => attempt(async () => {
2081
+ const id = toId(savedWorkoutSetId);
2082
+ const blocked = confirmGate(extra, `Set prescribed values on your saved workout set ${id} on ${date}? This changes your planned reps/weight but does not log the set as completed.`, confirm);
2083
+ if (blocked) return blocked;
2084
+ return jsonResult(await prescribeAthleteSet(ctx.client, {
2085
+ date,
2086
+ savedWorkoutSetId: id,
2087
+ results: toSetResults(results)
2088
+ }));
2089
+ }));
2097
2090
  }
2098
2091
  /**
2099
2092
  * The athlete's own per-exercise swap, in its own function so registerLogTool/registerSessionTools
@@ -2150,7 +2143,7 @@ function registerAthleteTrainingTools(server, ctx) {
2150
2143
  }
2151
2144
  //#endregion
2152
2145
  //#region package.json
2153
- var version = "3.0.0";
2146
+ var version = "3.1.0";
2154
2147
  //#endregion
2155
2148
  //#region src/server.ts
2156
2149
  function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trainheroic-unofficial/athlete-mcp",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,8 +21,8 @@
21
21
  "dependencies": {
22
22
  "@modelcontextprotocol/server": "2.0.0",
23
23
  "zod": "^4.4.3",
24
- "@trainheroic-unofficial/core": "3.0.0",
25
- "@trainheroic-unofficial/js": "3.0.0"
24
+ "@trainheroic-unofficial/core": "3.1.0",
25
+ "@trainheroic-unofficial/js": "3.1.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^26.2.0",