@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.
Files changed (2) hide show
  1. package/dist/server.mjs +91 -2
  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: "0.1.0"
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.0",
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.0",
25
- "@trainheroic-unofficial/js": "0.4.0"
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",