@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 +6 -3
- package/dist/server.mjs +48 -55
- package/package.json +3 -3
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,
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
847
|
+
return getArray(client, "/2.0/athlete/workingMax", "athlete working maxes");
|
|
842
848
|
}
|
|
843
849
|
function fetchExerciseHistoryList(client) {
|
|
844
|
-
return getArray
|
|
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
|
|
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
|
|
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
|
-
*
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
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
|
-
*
|
|
1453
|
-
*
|
|
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
|
|
1456
|
-
const
|
|
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
|
-
|
|
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
|
|
1504
|
-
* {@link prescribeForAthlete}. `target` selects the surface: `athlete`
|
|
1505
|
-
* `coach` writes `/1.0/coach/...{athleteId}` and stamps `athleteId` into
|
|
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,
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
25
|
-
"@trainheroic-unofficial/js": "3.
|
|
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",
|