@trainheroic-unofficial/athlete-mcp 0.4.0 → 0.4.2
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 +91 -2
- package/package.json +3 -3
package/dist/server.mjs
CHANGED
|
@@ -415,6 +415,17 @@ async function confirmGate(server, requestId, message, confirmArg) {
|
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
//#endregion
|
|
418
|
+
//#region ../core/src/instructions.ts
|
|
419
|
+
/**
|
|
420
|
+
* Server-level guidance passed as the MCP `instructions` string. The host model receives it
|
|
421
|
+
* in the initialize result and treats it like a system hint. Its job is to stop tool-layer
|
|
422
|
+
* implementation details from leaking into user-facing replies: by default a model narrates
|
|
423
|
+
* capability using the raw snake_case tool name (e.g. `athlete_session_create`), and the tool
|
|
424
|
+
* descriptions here reference each other by name to drive correct chaining, which primes that
|
|
425
|
+
* leak further. This tells the host to keep that wiring internal and speak in app terms.
|
|
426
|
+
*/
|
|
427
|
+
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.";
|
|
428
|
+
//#endregion
|
|
418
429
|
//#region ../js/src/auth.ts
|
|
419
430
|
const AUTH_URL = "https://apis.trainheroic.com/auth";
|
|
420
431
|
/**
|
|
@@ -848,6 +859,45 @@ async function logAthleteSet(client, args) {
|
|
|
848
859
|
exercisesLogged
|
|
849
860
|
};
|
|
850
861
|
}
|
|
862
|
+
/**
|
|
863
|
+
* POST /v5/programWorkouts/personal — create a personal workout session for a given date.
|
|
864
|
+
* Returns the key ids: workoutId (needed for addExercisesToWorkout), programWorkoutId,
|
|
865
|
+
* savedWorkoutId, and groupId.
|
|
866
|
+
*/
|
|
867
|
+
async function createPersonalWorkout(client, date) {
|
|
868
|
+
const res = await client.request("POST", "/v5/programWorkouts/personal", { body: { date } });
|
|
869
|
+
if (!res.ok) throw new Error(`Create personal workout failed (HTTP ${res.status}).`);
|
|
870
|
+
if (!isRecord(res.data)) throw new Error("Unexpected response from /v5/programWorkouts/personal.");
|
|
871
|
+
const pw = isRecord(res.data.programWorkout) ? res.data.programWorkout : null;
|
|
872
|
+
const sw = isRecord(res.data.savedWorkout) ? res.data.savedWorkout : null;
|
|
873
|
+
if (!pw || !sw) throw new Error("Missing programWorkout or savedWorkout in response.");
|
|
874
|
+
const programWorkoutId = coerceInt(pw.id);
|
|
875
|
+
const workoutId = coerceInt(pw.workoutId);
|
|
876
|
+
const savedWorkoutId = coerceInt(sw.id);
|
|
877
|
+
const groupId = coerceInt(sw.group_id);
|
|
878
|
+
if (!programWorkoutId || !workoutId || !savedWorkoutId || !groupId) throw new Error("Could not parse required ids from personal workout response.");
|
|
879
|
+
return {
|
|
880
|
+
programWorkoutId,
|
|
881
|
+
workoutId,
|
|
882
|
+
savedWorkoutId,
|
|
883
|
+
groupId,
|
|
884
|
+
date: typeof pw.date === "string" ? pw.date : date
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* PUT /v5/personalCalendar/workouts/{workoutId}/addExercises — add exercises to a personal
|
|
889
|
+
* workout. Returns saved workout set objects: each top-level `id` is a savedWorkoutSetId
|
|
890
|
+
* and `savedWorkoutSetExercises[].id` is a savedWorkoutSetExerciseId, both needed by
|
|
891
|
+
* logAthleteSet.
|
|
892
|
+
*/
|
|
893
|
+
async function addExercisesToWorkout(client, workoutId, exercises) {
|
|
894
|
+
const res = await client.request("PUT", `/v5/personalCalendar/workouts/${workoutId}/addExercises`, { body: {
|
|
895
|
+
exercises,
|
|
896
|
+
circuits: []
|
|
897
|
+
} });
|
|
898
|
+
if (!res.ok) throw new Error(`Add exercises to workout failed (HTTP ${res.status}).`);
|
|
899
|
+
return res.data;
|
|
900
|
+
}
|
|
851
901
|
/** Flatten `/v5/exercises/{id}/history` into PRs + a session time-series. */
|
|
852
902
|
function presentExerciseHistory(detail) {
|
|
853
903
|
return {
|
|
@@ -980,6 +1030,41 @@ function registerExerciseTools(server, ctx, userId) {
|
|
|
980
1030
|
annotations: READ
|
|
981
1031
|
}, ({ exerciseId, date }) => attempt(async () => jsonResult(await fetchExerciseStats(ctx.client, toId(exerciseId), await userId(), date))));
|
|
982
1032
|
}
|
|
1033
|
+
/** Create a personal workout session and add exercises to it. */
|
|
1034
|
+
function registerSessionTools(server, ctx) {
|
|
1035
|
+
server.registerTool("athlete_session_create", {
|
|
1036
|
+
title: "Create personal workout session",
|
|
1037
|
+
description: "Create a new personal workout session for a given YYYY-MM-DD date on the athlete's personal calendar. Returns programWorkoutId, workoutId (pass to athlete_session_add_exercises), savedWorkoutId, groupId, and date.",
|
|
1038
|
+
inputSchema: { date: dateString },
|
|
1039
|
+
annotations: {
|
|
1040
|
+
readOnlyHint: false,
|
|
1041
|
+
destructiveHint: false,
|
|
1042
|
+
openWorldHint: true
|
|
1043
|
+
}
|
|
1044
|
+
}, ({ date }) => attempt(async () => jsonResult(await createPersonalWorkout(ctx.client, date))));
|
|
1045
|
+
server.registerTool("athlete_session_add_exercises", {
|
|
1046
|
+
title: "Add exercises to a personal workout session",
|
|
1047
|
+
description: "Add one or more exercises to a personal workout session. Get workoutId from athlete_session_create. Each item needs an exerciseId (from athlete_exercises) and a 1-based order. Returns saved workout set objects: each top-level id is a savedWorkoutSetId and savedWorkoutSetExercises[].id is a savedWorkoutSetExerciseId — both needed for athlete_log_set.",
|
|
1048
|
+
inputSchema: {
|
|
1049
|
+
workoutId: idParam,
|
|
1050
|
+
exercises: z.array(z.object({
|
|
1051
|
+
exerciseId: idParam,
|
|
1052
|
+
order: z.number().int().positive()
|
|
1053
|
+
})).min(1)
|
|
1054
|
+
},
|
|
1055
|
+
annotations: {
|
|
1056
|
+
readOnlyHint: false,
|
|
1057
|
+
destructiveHint: false,
|
|
1058
|
+
openWorldHint: true
|
|
1059
|
+
}
|
|
1060
|
+
}, ({ workoutId, exercises }) => attempt(async () => {
|
|
1061
|
+
const mapped = exercises.map((e) => ({
|
|
1062
|
+
exerciseId: toId(e.exerciseId),
|
|
1063
|
+
order: e.order
|
|
1064
|
+
}));
|
|
1065
|
+
return jsonResult(await addExercisesToWorkout(ctx.client, toId(workoutId), mapped), { hint: "Each top-level id is a savedWorkoutSetId; savedWorkoutSetExercises[].id is the savedWorkoutSetExerciseId for athlete_log_set." });
|
|
1066
|
+
}));
|
|
1067
|
+
}
|
|
983
1068
|
/** The gated set-logging write. */
|
|
984
1069
|
function registerLogTool(server, ctx) {
|
|
985
1070
|
server.registerTool("athlete_log_set", {
|
|
@@ -1030,9 +1115,13 @@ function registerAthleteTrainingTools(server, ctx) {
|
|
|
1030
1115
|
};
|
|
1031
1116
|
registerProfileTools(server, ctx, whoami, userId);
|
|
1032
1117
|
registerExerciseTools(server, ctx, userId);
|
|
1118
|
+
registerSessionTools(server, ctx);
|
|
1033
1119
|
registerLogTool(server, ctx);
|
|
1034
1120
|
}
|
|
1035
1121
|
//#endregion
|
|
1122
|
+
//#region package.json
|
|
1123
|
+
var version = "0.4.1";
|
|
1124
|
+
//#endregion
|
|
1036
1125
|
//#region src/server.ts
|
|
1037
1126
|
async function main() {
|
|
1038
1127
|
const email = process.env.TRAINHEROIC_EMAIL;
|
|
@@ -1044,8 +1133,8 @@ async function main() {
|
|
|
1044
1133
|
const client = new TrainHeroicClient(email, password);
|
|
1045
1134
|
const server = new McpServer({
|
|
1046
1135
|
name: "trainheroic-athlete",
|
|
1047
|
-
version
|
|
1048
|
-
});
|
|
1136
|
+
version
|
|
1137
|
+
}, { instructions: SERVER_INSTRUCTIONS });
|
|
1049
1138
|
registerAthleteTrainingTools(server, { client });
|
|
1050
1139
|
await server.connect(new StdioServerTransport());
|
|
1051
1140
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trainheroic-unofficial/athlete-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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.4.
|
|
25
|
-
"@trainheroic-unofficial/js": "0.4.
|
|
24
|
+
"@trainheroic-unofficial/core": "0.4.2",
|
|
25
|
+
"@trainheroic-unofficial/js": "0.4.2"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^26.0.0",
|