@trainheroic-unofficial/athlete-mcp 3.0.1 → 3.1.1

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
@@ -281,6 +287,17 @@ const programWorkoutResponseSchema = z.looseObject({
281
287
  sets: z.record(z.string(), z.unknown()).optional()
282
288
  });
283
289
  z.looseObject({ programWorkouts: z.array(programWorkoutResponseSchema).optional() });
290
+ z.looseObject({
291
+ id: intLike,
292
+ title: z.string(),
293
+ programId: intLike,
294
+ group_program: intLike,
295
+ type: intLike,
296
+ program: z.looseObject({
297
+ id: intLike,
298
+ program_type: intLike
299
+ }).optional()
300
+ });
284
301
  //#endregion
285
302
  //#region ../dto/src/workout.ts
286
303
  /** A single exercise prescription inside a block. */
@@ -693,7 +710,7 @@ var TrainHeroicClient = class {
693
710
  session = await this.#ensureSession();
694
711
  res = await this.#send(method, url, session, options.body);
695
712
  }
696
- if (!res.ok) notifyHttpError(this.#onHttpError, method, url, res.status);
713
+ if (!res.ok && !options.expectedStatuses?.includes(res.status)) notifyHttpError(this.#onHttpError, method, url, res.status);
697
714
  const text = await res.text();
698
715
  let data = text;
699
716
  if (text.length > 0) try {
@@ -1427,6 +1444,21 @@ async function logAthleteSet(client, args) {
1427
1444
  };
1428
1445
  }
1429
1446
  /**
1447
+ * Set the logged-in athlete's prescribed reps/weight for one saved workout set WITHOUT marking it
1448
+ * performed. This is the athlete-side equivalent of editing target values in the app: it writes
1449
+ * `param_N_data_M` with every made/completed flag cleared and skips the set-completion PUT.
1450
+ *
1451
+ * The write replaces the selected exercise's whole prescription, so callers must pass every set
1452
+ * they want to keep. An omitted param is cleared rather than preserved.
1453
+ */
1454
+ async function prescribeAthleteSet(client, args) {
1455
+ const r = await writeSetResults(client, { role: "athlete" }, await fetchAthleteWorkouts(client, args.date, args.date), args.savedWorkoutSetId, args.results, "prescribe");
1456
+ return {
1457
+ savedWorkoutSetId: r.savedWorkoutSetId,
1458
+ exercisesPrescribed: r.exercisesWritten
1459
+ };
1460
+ }
1461
+ /**
1430
1462
  * Swap the exercise prescribed in one of an athlete's saved-workout slots for a different
1431
1463
  * exercise — the API equivalent of the app's per-athlete "swap exercise":
1432
1464
  * `PUT /v5/savedWorkoutSetExercises/{savedWorkoutSetExerciseId}?exerciseId={exerciseId}` with an
@@ -1462,9 +1494,10 @@ async function swapAthleteExercise(client, args) {
1462
1494
  };
1463
1495
  }
1464
1496
  /**
1465
- * Shared set-write behind {@link logAthleteSet}, {@link logForAthlete}, and
1466
- * {@link prescribeForAthlete}. `target` selects the surface: `athlete` writes `/1.0/athlete/...`;
1467
- * `coach` writes `/1.0/coach/...{athleteId}` and stamps `athleteId` into each body.
1497
+ * Shared set-write behind {@link logAthleteSet}, {@link prescribeAthleteSet},
1498
+ * {@link logForAthlete}, and {@link prescribeForAthlete}. `target` selects the surface: `athlete`
1499
+ * writes `/1.0/athlete/...`; `coach` writes `/1.0/coach/...{athleteId}` and stamps `athleteId` into
1500
+ * each body.
1468
1501
  *
1469
1502
  * Step 1 PUTs each exercise's per-set values to its own endpoint (the only path that actually
1470
1503
  * stores reps and weight). `mode` decides what those values mean: `"log"` records them as a
@@ -2047,6 +2080,24 @@ function registerLogTool(server, ctx) {
2047
2080
  results: toSetResults(results)
2048
2081
  }));
2049
2082
  }));
2083
+ server.registerTool("athlete_prescribe_set", {
2084
+ title: "Set your prescribed reps/weight without logging",
2085
+ 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).",
2086
+ inputSchema: {
2087
+ ...athletePrescribeSetArgsSchema.shape,
2088
+ confirm: z.boolean().optional()
2089
+ },
2090
+ annotations: DESTRUCTIVE
2091
+ }, ({ date, savedWorkoutSetId, results, confirm }, extra) => attempt(async () => {
2092
+ const id = toId(savedWorkoutSetId);
2093
+ 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);
2094
+ if (blocked) return blocked;
2095
+ return jsonResult(await prescribeAthleteSet(ctx.client, {
2096
+ date,
2097
+ savedWorkoutSetId: id,
2098
+ results: toSetResults(results)
2099
+ }));
2100
+ }));
2050
2101
  }
2051
2102
  /**
2052
2103
  * The athlete's own per-exercise swap, in its own function so registerLogTool/registerSessionTools
@@ -2103,7 +2154,7 @@ function registerAthleteTrainingTools(server, ctx) {
2103
2154
  }
2104
2155
  //#endregion
2105
2156
  //#region package.json
2106
- var version = "3.0.1";
2157
+ var version = "3.1.1";
2107
2158
  //#endregion
2108
2159
  //#region src/server.ts
2109
2160
  function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trainheroic-unofficial/athlete-mcp",
3
- "version": "3.0.1",
3
+ "version": "3.1.1",
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.1",
25
- "@trainheroic-unofficial/js": "3.0.1"
24
+ "@trainheroic-unofficial/core": "3.1.1",
25
+ "@trainheroic-unofficial/js": "3.1.1"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^26.2.0",