@trainheroic-unofficial/athlete-mcp 2.0.2 → 2.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/dist/server.mjs +61 -19
- package/package.json +5 -5
package/dist/server.mjs
CHANGED
|
@@ -545,6 +545,34 @@ function confirmGate(ctx, message, confirmArg) {
|
|
|
545
545
|
*/
|
|
546
546
|
const SERVER_INSTRUCTIONS = "Speak to the user in plain, everyday language about their training. Describe what you are doing in the TrainHeroic app's own terms (for example, say you are creating a workout rather than naming a tool). Do not surface internal tool names (the snake_case identifiers such as athlete_session_create), raw parameter names, or numeric ids in your replies unless the user explicitly asks for them; they are implementation details. The tool descriptions cross-reference each other by name only so you can chain them correctly. Keep that wiring to yourself.";
|
|
547
547
|
//#endregion
|
|
548
|
+
//#region ../js/src/http-error.ts
|
|
549
|
+
/**
|
|
550
|
+
* A final non-2xx response from TrainHeroic. The error deliberately carries only request
|
|
551
|
+
* metadata that is safe to send to telemetry: never the path, query string, request body,
|
|
552
|
+
* response body, credentials, or session token.
|
|
553
|
+
*/
|
|
554
|
+
var TrainHeroicHttpError = class extends Error {
|
|
555
|
+
name = "TrainHeroicHttpError";
|
|
556
|
+
method;
|
|
557
|
+
status;
|
|
558
|
+
host;
|
|
559
|
+
constructor(method, url, status) {
|
|
560
|
+
const parsed = new URL(url);
|
|
561
|
+
const normalizedMethod = method.toUpperCase();
|
|
562
|
+
super(`TrainHeroic ${normalizedMethod} request failed with HTTP ${status}`);
|
|
563
|
+
this.method = normalizedMethod;
|
|
564
|
+
this.status = status;
|
|
565
|
+
this.host = parsed.host;
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
/** Call observability hooks without allowing them to change SDK behavior. */
|
|
569
|
+
function notifyHttpError(handler, method, url, status) {
|
|
570
|
+
if (!handler) return;
|
|
571
|
+
try {
|
|
572
|
+
Promise.resolve(handler(new TrainHeroicHttpError(method, url, status))).catch(() => {});
|
|
573
|
+
} catch {}
|
|
574
|
+
}
|
|
575
|
+
//#endregion
|
|
548
576
|
//#region ../js/src/auth.ts
|
|
549
577
|
const DEFAULT_AUTH_URL = "https://apis.trainheroic.com/auth";
|
|
550
578
|
/**
|
|
@@ -566,8 +594,9 @@ function authUrl() {
|
|
|
566
594
|
* the Phase 0 spike: no refresh_token, no api_token, no TTL). The 48-char session_id
|
|
567
595
|
* is sent as the `session-token` header and works against both API hosts.
|
|
568
596
|
*/
|
|
569
|
-
async function loginTrainHeroic(email, password) {
|
|
570
|
-
const
|
|
597
|
+
async function loginTrainHeroic(email, password, options = {}) {
|
|
598
|
+
const url = authUrl();
|
|
599
|
+
const res = await fetch(url, {
|
|
571
600
|
method: "POST",
|
|
572
601
|
headers: {
|
|
573
602
|
"content-type": "application/x-www-form-urlencoded",
|
|
@@ -578,7 +607,10 @@ async function loginTrainHeroic(email, password) {
|
|
|
578
607
|
password
|
|
579
608
|
}).toString()
|
|
580
609
|
});
|
|
581
|
-
if (!res.ok)
|
|
610
|
+
if (!res.ok) {
|
|
611
|
+
notifyHttpError(options.onHttpError, "POST", url, res.status);
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
582
614
|
const data = await res.json().catch(() => null);
|
|
583
615
|
if (!data || typeof data.id !== "number" || !data.session_id) return null;
|
|
584
616
|
return {
|
|
@@ -621,6 +653,7 @@ var TrainHeroicClient = class {
|
|
|
621
653
|
#email;
|
|
622
654
|
#password;
|
|
623
655
|
#onSession;
|
|
656
|
+
#onHttpError;
|
|
624
657
|
#sessionId;
|
|
625
658
|
#loginInFlight = null;
|
|
626
659
|
constructor(email, password, sessionId = null, options = {}) {
|
|
@@ -628,6 +661,7 @@ var TrainHeroicClient = class {
|
|
|
628
661
|
this.#password = password;
|
|
629
662
|
this.#sessionId = sessionId;
|
|
630
663
|
this.#onSession = options.onSession;
|
|
664
|
+
this.#onHttpError = options.onHttpError;
|
|
631
665
|
}
|
|
632
666
|
get sessionId() {
|
|
633
667
|
return this.#sessionId;
|
|
@@ -642,7 +676,7 @@ var TrainHeroicClient = class {
|
|
|
642
676
|
}
|
|
643
677
|
}
|
|
644
678
|
async #login() {
|
|
645
|
-
const session = await loginTrainHeroic(this.#email, this.#password);
|
|
679
|
+
const session = await loginTrainHeroic(this.#email, this.#password, this.#onHttpError ? { onHttpError: this.#onHttpError } : {});
|
|
646
680
|
if (!session) throw new TrainHeroicAuthError("TrainHeroic login failed");
|
|
647
681
|
this.#sessionId = session.sessionId;
|
|
648
682
|
try {
|
|
@@ -659,6 +693,7 @@ var TrainHeroicClient = class {
|
|
|
659
693
|
session = await this.#ensureSession();
|
|
660
694
|
res = await this.#send(method, url, session, options.body);
|
|
661
695
|
}
|
|
696
|
+
if (!res.ok) notifyHttpError(this.#onHttpError, method, url, res.status);
|
|
662
697
|
const text = await res.text();
|
|
663
698
|
let data = text;
|
|
664
699
|
if (text.length > 0) try {
|
|
@@ -787,7 +822,7 @@ async function getJson(client, path, label) {
|
|
|
787
822
|
if (!res.ok) throw new Error(`${label} failed (HTTP ${res.status}).`);
|
|
788
823
|
return res.data;
|
|
789
824
|
}
|
|
790
|
-
async function getArray(client, path, label) {
|
|
825
|
+
async function getArray$1(client, path, label) {
|
|
791
826
|
const res = await client.request("GET", path);
|
|
792
827
|
if (!res.ok || !Array.isArray(res.data)) throw new Error(`${label} failed (HTTP ${res.status}).`);
|
|
793
828
|
return res.data;
|
|
@@ -803,10 +838,10 @@ function fetchAthletePrefs(client) {
|
|
|
803
838
|
return getJson(client, "/1.0/athlete/prefs", "athlete prefs");
|
|
804
839
|
}
|
|
805
840
|
function fetchWorkingMaxes(client) {
|
|
806
|
-
return getArray(client, "/2.0/athlete/workingMax", "athlete working maxes");
|
|
841
|
+
return getArray$1(client, "/2.0/athlete/workingMax", "athlete working maxes");
|
|
807
842
|
}
|
|
808
843
|
function fetchExerciseHistoryList(client) {
|
|
809
|
-
return getArray(client, "/v5/users/exercises/history", "athlete exercise history list");
|
|
844
|
+
return getArray$1(client, "/v5/users/exercises/history", "athlete exercise history list");
|
|
810
845
|
}
|
|
811
846
|
/** Free-text search over the athlete's logged exercises (FTS replacement via rankSearch). */
|
|
812
847
|
async function searchExerciseHistory(client, query, limit = 20) {
|
|
@@ -816,7 +851,7 @@ function fetchExerciseHistoryDetail(client, exerciseId, userId) {
|
|
|
816
851
|
return getJson(client, `/v5/exercises/${exerciseId}/history?userId=${userId}`, "athlete exercise history");
|
|
817
852
|
}
|
|
818
853
|
function fetchPersonalRecords(client, exerciseId) {
|
|
819
|
-
return getArray(client, `/v5/exercises/${exerciseId}/personalRecords`, "athlete personal records");
|
|
854
|
+
return getArray$1(client, `/v5/exercises/${exerciseId}/personalRecords`, "athlete personal records");
|
|
820
855
|
}
|
|
821
856
|
/** Last performance + PR for an exercise. `date` (YYYY-MM-DD) is required by the API. */
|
|
822
857
|
function fetchExerciseStats(client, exerciseId, userId, date) {
|
|
@@ -824,16 +859,7 @@ function fetchExerciseStats(client, exerciseId, userId, date) {
|
|
|
824
859
|
}
|
|
825
860
|
/** Scheduled + completed workouts in an inclusive YYYY-MM-DD window. */
|
|
826
861
|
function fetchAthleteWorkouts(client, startDate, endDate) {
|
|
827
|
-
return getArray(client, `/3.0/athlete/programworkout/range?startDate=${startDate}&endDate=${endDate}`, "athlete workouts");
|
|
828
|
-
}
|
|
829
|
-
/**
|
|
830
|
-
* A coach's view of a roster athlete's scheduled + completed workouts in an inclusive
|
|
831
|
-
* YYYY-MM-DD window (`/3.0/coach/athlete/programworkout/range/{athleteId}`). Returns the same
|
|
832
|
-
* `ProgramWorkout[]` shape as `fetchAthleteWorkouts`, so the same presenters and
|
|
833
|
-
* `findSavedWorkoutSet` apply — it just reads another athlete's data through the coach surface.
|
|
834
|
-
*/
|
|
835
|
-
function fetchCoachAthleteWorkouts(client, athleteId, startDate, endDate) {
|
|
836
|
-
return getArray(client, `/3.0/coach/athlete/programworkout/range/${athleteId}?startDate=${startDate}&endDate=${endDate}`, "coach athlete workouts");
|
|
862
|
+
return getArray$1(client, `/3.0/athlete/programworkout/range?startDate=${startDate}&endDate=${endDate}`, "athlete workouts");
|
|
837
863
|
}
|
|
838
864
|
function fetchLeaderboard(client, workoutId, opts = {}) {
|
|
839
865
|
const qs = new URLSearchParams();
|
|
@@ -1186,6 +1212,22 @@ function presentExerciseHistory(detail) {
|
|
|
1186
1212
|
};
|
|
1187
1213
|
}
|
|
1188
1214
|
//#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
|
|
1189
1231
|
//#region ../js/src/exercise-set-payload.ts
|
|
1190
1232
|
function slotData(exercise, key) {
|
|
1191
1233
|
const value = exercise?.[key];
|
|
@@ -2108,7 +2150,7 @@ function registerAthleteTrainingTools(server, ctx) {
|
|
|
2108
2150
|
}
|
|
2109
2151
|
//#endregion
|
|
2110
2152
|
//#region package.json
|
|
2111
|
-
var version = "2.
|
|
2153
|
+
var version = "2.1.1";
|
|
2112
2154
|
//#endregion
|
|
2113
2155
|
//#region src/server.ts
|
|
2114
2156
|
function main() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trainheroic-unofficial/athlete-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/server": "2.0.0",
|
|
23
23
|
"zod": "^4.4.3",
|
|
24
|
-
"@trainheroic-unofficial/
|
|
25
|
-
"@trainheroic-unofficial/
|
|
24
|
+
"@trainheroic-unofficial/js": "2.1.1",
|
|
25
|
+
"@trainheroic-unofficial/core": "2.1.1"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^26.
|
|
28
|
+
"@types/node": "^26.2.0",
|
|
29
29
|
"tsdown": "^0.22.14",
|
|
30
|
-
"tsx": "^4.23.
|
|
30
|
+
"tsx": "^4.23.11",
|
|
31
31
|
"typescript": "^7.0.2",
|
|
32
32
|
"vitest": "^4.1.10"
|
|
33
33
|
},
|