@trainheroic-unofficial/athlete-mcp 3.2.0 → 3.3.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.
Files changed (2) hide show
  1. package/dist/server.mjs +473 -79
  2. package/package.json +4 -4
package/dist/server.mjs CHANGED
@@ -24,12 +24,14 @@ const numLikeOrNull = z.union([
24
24
  z.string(),
25
25
  z.null()
26
26
  ]);
27
- z.looseObject({
27
+ /** `/user/simple` — the identity + tenant key (numeric `id`) for any logged-in account. */
28
+ const userSimpleSchema = z.looseObject({
28
29
  id: intLike$1,
29
30
  roles: z.array(z.string()).optional(),
30
31
  org_id: intLikeOrNull$1.optional()
31
32
  });
32
- z.looseObject({
33
+ /** `/v5/athleteProfile/summary` — lifetime training totals. Needs `use_metric` in the query. */
34
+ const athleteProfileSummarySchema = z.looseObject({
33
35
  reps_sum: z.number().optional(),
34
36
  volume_sum: z.number().optional(),
35
37
  sessions_count: z.number().optional(),
@@ -37,7 +39,8 @@ z.looseObject({
37
39
  last_logged_date: z.string().optional(),
38
40
  duration_hours: z.number().optional()
39
41
  });
40
- z.looseObject({
42
+ /** `/v5/users/{id}` — the detailed athlete profile (only the fields we surface). */
43
+ const athleteUserSchema = z.looseObject({
41
44
  id: intLike$1,
42
45
  email: z.string().optional(),
43
46
  name_first: z.string().optional(),
@@ -47,7 +50,8 @@ z.looseObject({
47
50
  date_of_birth: z.string().optional(),
48
51
  use_metric: z.boolean().optional()
49
52
  });
50
- z.looseObject({ id: intLike$1 });
53
+ /** `/1.0/athlete/prefs` notification + display preference flags. */
54
+ const athletePrefsSchema = z.looseObject({ id: intLike$1 });
51
55
  /** One item of `/2.0/athlete/workingMax` — the athlete's working max for an exercise. */
52
56
  const athleteWorkingMaxSchema = z.looseObject({
53
57
  exercise_id: intLike$1,
@@ -57,7 +61,7 @@ const athleteWorkingMaxSchema = z.looseObject({
57
61
  type_suffix: z.string().optional(),
58
62
  working_max_id: intLikeOrNull$1.optional()
59
63
  });
60
- z.array(athleteWorkingMaxSchema);
64
+ const athleteWorkingMaxListSchema = z.array(athleteWorkingMaxSchema);
61
65
  /** One item of `/v5/users/exercises/history` — an exercise the athlete has logged. */
62
66
  const exerciseHistoryListItemSchema = z.looseObject({
63
67
  id: intLike$1,
@@ -107,7 +111,8 @@ const liftPRSchema = z.looseObject({
107
111
  isMetric: z.boolean().optional(),
108
112
  description: z.string().optional()
109
113
  });
110
- z.looseObject({
114
+ /** `/v5/exercises/{id}/history` — the per-exercise PRs + session history. */
115
+ const exerciseHistoryDetailSchema = z.looseObject({
111
116
  liftPRs: z.array(liftPRSchema).optional(),
112
117
  singleParamPRs: z.array(z.unknown()).optional(),
113
118
  history: z.array(historyEntrySchema).optional()
@@ -123,8 +128,9 @@ const personalRecordSchema = z.looseObject({
123
128
  units: z.string().optional(),
124
129
  isMetric: z.boolean().optional()
125
130
  });
126
- z.array(personalRecordSchema);
127
- z.looseObject({
131
+ const personalRecordListSchema = z.array(personalRecordSchema);
132
+ /** `/v5/exercises/{id}/stats` — last performance + PR for an exercise. Needs `date` in the query. */
133
+ const exerciseStatsSchema = z.looseObject({
128
134
  isLift: z.boolean().optional(),
129
135
  lastPerformance: z.unknown().optional(),
130
136
  personalRecord: z.unknown().optional()
@@ -144,7 +150,7 @@ const programWorkoutSchema = z.looseObject({
144
150
  team_title: z.string().optional(),
145
151
  summarizedSavedWorkout: z.unknown().optional()
146
152
  });
147
- z.array(programWorkoutSchema);
153
+ const programWorkoutListSchema = z.array(programWorkoutSchema);
148
154
  /** A `YYYY-MM-DD` date argument. The single definition reused across athlete tool inputs. */
149
155
  const dateString = z.string().regex(/^\d{4}-\d{2}-\d{2}$/u, "expected YYYY-MM-DD");
150
156
  z.object({
@@ -251,6 +257,163 @@ const athleteSessionRemoveArgsSchema = z.object({
251
257
  programWorkoutId: idArgSchema,
252
258
  date: dateString
253
259
  });
260
+ const presentedNullNumber = z.number().nullable();
261
+ const presentedNullString = z.string().nullable();
262
+ /** Flattened exercise within a presented workout: prescriptions, logged results, units. */
263
+ const athleteWorkoutExerciseSchema = z.object({
264
+ exerciseId: presentedNullNumber,
265
+ title: z.string(),
266
+ instruction: presentedNullString,
267
+ units: z.array(presentedNullString),
268
+ prescribed: z.array(z.string()),
269
+ performed: z.array(z.string())
270
+ });
271
+ const athleteWorkoutBlockSchema = z.object({
272
+ order: z.number(),
273
+ title: presentedNullString,
274
+ instruction: presentedNullString,
275
+ isTest: z.boolean(),
276
+ exercises: z.array(athleteWorkoutExerciseSchema)
277
+ });
278
+ const athleteWorkoutViewSchema = z.object({
279
+ id: presentedNullNumber,
280
+ date: z.string(),
281
+ title: z.string(),
282
+ program: presentedNullString,
283
+ team: presentedNullString,
284
+ instruction: presentedNullString,
285
+ logged: z.boolean(),
286
+ personal: z.boolean(),
287
+ blocks: z.array(athleteWorkoutBlockSchema)
288
+ });
289
+ const athleteWorkoutSummarySchema = z.object({
290
+ id: presentedNullNumber,
291
+ date: z.string(),
292
+ title: z.string(),
293
+ program: presentedNullString,
294
+ team: presentedNullString,
295
+ logged: z.boolean(),
296
+ personal: z.boolean(),
297
+ exerciseCount: z.number().int().nonnegative(),
298
+ performedCount: z.number().int().nonnegative()
299
+ });
300
+ /** Presented views first so a summary/detail row cannot be swallowed by the loose raw list. */
301
+ const athleteWorkoutsOutputSchema = z.union([
302
+ z.array(athleteWorkoutSummarySchema),
303
+ z.array(athleteWorkoutViewSchema),
304
+ programWorkoutListSchema
305
+ ]);
306
+ const athleteProfileOutputSchema = z.looseObject({
307
+ summary: athleteProfileSummarySchema,
308
+ user: athleteUserSchema
309
+ });
310
+ const athleteExerciseCatalogOutputSchema = z.array(z.object({
311
+ id: z.union([z.number(), z.string()]),
312
+ title: z.string(),
313
+ isCircuit: z.boolean(),
314
+ units: z.array(presentedNullString)
315
+ }));
316
+ const coachAthleteExerciseSchema = z.object({
317
+ exerciseId: presentedNullNumber,
318
+ title: z.string(),
319
+ summary: presentedNullString,
320
+ completed: z.boolean()
321
+ });
322
+ const coachAthleteSessionSchema = z.object({
323
+ workoutId: presentedNullNumber,
324
+ savedWorkoutId: presentedNullNumber,
325
+ title: z.string(),
326
+ logged: z.boolean(),
327
+ completed: z.boolean(),
328
+ rpe: presentedNullNumber,
329
+ durationMin: presentedNullNumber,
330
+ notes: presentedNullString,
331
+ exercises: z.array(coachAthleteExerciseSchema)
332
+ });
333
+ z.object({
334
+ athleteId: presentedNullNumber,
335
+ athleteName: presentedNullString,
336
+ year: z.number().int(),
337
+ month: z.number().int(),
338
+ sessions: z.array(coachAthleteSessionSchema)
339
+ });
340
+ const rosterActivityRowSchema = z.object({
341
+ athleteId: z.number(),
342
+ sessionsCount: presentedNullNumber,
343
+ firstLoggedDate: presentedNullString,
344
+ lastLoggedDate: presentedNullString,
345
+ totalReps: presentedNullNumber,
346
+ totalVolume: presentedNullNumber
347
+ });
348
+ const teamVolumeAthleteSchema = z.object({
349
+ athleteId: z.number(),
350
+ name: presentedNullString,
351
+ sessions: z.number(),
352
+ reps: z.number(),
353
+ volume: z.number(),
354
+ firstLoggedDate: presentedNullString,
355
+ lastLoggedDate: presentedNullString
356
+ });
357
+ z.object({
358
+ window: z.object({
359
+ start: z.string(),
360
+ end: z.string()
361
+ }),
362
+ athletes: z.array(teamVolumeAthleteSchema),
363
+ totals: z.object({
364
+ athletes: z.number(),
365
+ sessions: z.number(),
366
+ reps: z.number(),
367
+ volume: z.number()
368
+ })
369
+ });
370
+ z.array(rosterActivityRowSchema);
371
+ const presentedExerciseSessionSchema = z.object({
372
+ date: z.string(),
373
+ abr: presentedNullString,
374
+ estimated1RM: presentedNullNumber,
375
+ sets: z.array(z.object({
376
+ setNumber: z.number(),
377
+ value: presentedNullString
378
+ }))
379
+ });
380
+ const presentedExerciseHistorySchema = z.object({
381
+ liftPRs: z.array(z.object({
382
+ description: presentedNullString,
383
+ reps: presentedNullNumber,
384
+ weight: presentedNullNumber,
385
+ units: presentedNullString,
386
+ date: presentedNullString
387
+ })),
388
+ sessions: z.array(presentedExerciseSessionSchema)
389
+ });
390
+ /** Presented history first so the loose raw detail schema cannot swallow it. */
391
+ const exerciseHistoryOutputSchema = z.union([presentedExerciseHistorySchema, exerciseHistoryDetailSchema]);
392
+ //#endregion
393
+ //#region ../dto/src/exercise.ts
394
+ /**
395
+ * A row presented for display. The raw param-type codes are dropped and the fixed
396
+ * measurement units are surfaced positionally in `units`, ordered by entry slot
397
+ * (param 1, then param 2). Positional, not semantic: param 2 is not always the load
398
+ * — some exercises reverse the slots — so the units are not labelled by role.
399
+ */
400
+ const exerciseViewSchema = z.object({
401
+ id: z.number(),
402
+ title: z.string(),
403
+ can_edit: z.number(),
404
+ user_id: z.number().nullable(),
405
+ use_count: z.number(),
406
+ units: z.array(z.string().nullable())
407
+ });
408
+ z.looseObject({
409
+ id: idSchema,
410
+ title: z.string(),
411
+ units: z.array(z.string().nullable())
412
+ });
413
+ z.object({
414
+ match: exerciseViewSchema.nullable(),
415
+ candidates: z.array(exerciseViewSchema)
416
+ });
254
417
  z.looseObject({
255
418
  title: z.string().min(1),
256
419
  param_1_type: z.number().optional(),
@@ -305,6 +468,198 @@ z.object({
305
468
  pub_timezone: z.string().optional()
306
469
  }).refine((v) => v.pub_enabled !== void 0 || v.pub_days !== void 0 || v.pub_time !== void 0 || v.pub_timezone !== void 0, { message: "Provide at least one pub_* field" });
307
470
  //#endregion
471
+ //#region ../dto/src/tool-output.ts
472
+ const nullableNumber = z.number().nullable();
473
+ const nullableString = z.string().nullable();
474
+ const truncationMarkerSchema = z.looseObject({
475
+ field: z.string().optional(),
476
+ returned: z.number().int().nonnegative().optional(),
477
+ total: z.number().int().nonnegative(),
478
+ omitted: z.number().int().nonnegative(),
479
+ hint: z.string()
480
+ });
481
+ /** Dedicated budget-fallback envelope. Never the natural tool shape. */
482
+ const truncatedOutputSchema = z.looseObject({
483
+ preview: z.string().optional(),
484
+ items: z.array(z.json()).optional(),
485
+ __truncated: truncationMarkerSchema
486
+ });
487
+ /**
488
+ * Add the shared size-budget fallback to a tool's natural result shape.
489
+ * Truncation is a dedicated `{ items|preview, __truncated }` envelope, so it cannot
490
+ * validate as the natural object and drop the marker.
491
+ */
492
+ function toolOutputSchema(schema) {
493
+ return z.union([truncatedOutputSchema, schema]);
494
+ }
495
+ /** Explicit passthrough for tools whose live API payload is not yet contracted. */
496
+ const opaqueOutputSchema = toolOutputSchema(z.json());
497
+ const logSetTargetOutputSchema = z.object({
498
+ date: z.string(),
499
+ workoutTitle: z.string(),
500
+ program: nullableString,
501
+ programId: nullableNumber,
502
+ team: nullableString,
503
+ teamId: nullableNumber,
504
+ savedWorkoutSetId: z.number(),
505
+ setTitle: nullableString,
506
+ exercises: z.array(z.object({
507
+ savedWorkoutSetExerciseId: z.number(),
508
+ title: z.string(),
509
+ units: z.array(nullableString),
510
+ prescribed: z.array(z.string()),
511
+ performed: z.array(z.string())
512
+ }))
513
+ });
514
+ const logTargetsOutputSchema = z.union([z.array(logSetTargetOutputSchema), programWorkoutListSchema]);
515
+ z.object({
516
+ draft: z.literal(true),
517
+ note: z.string(),
518
+ would_POST: z.string(),
519
+ payload: z.json()
520
+ });
521
+ z.looseObject({
522
+ status: z.enum(["sent", "logged"]),
523
+ reference: z.string().optional(),
524
+ note: z.string()
525
+ });
526
+ z.looseObject({ removed: z.union([z.boolean(), z.number()]) });
527
+ const athleteSessionRemovedOutputSchema = z.object({
528
+ removed: z.literal(true),
529
+ programWorkoutId: z.number(),
530
+ date: z.string()
531
+ });
532
+ z.object({
533
+ invited: z.literal(true),
534
+ teamId: z.number(),
535
+ result: z.json().optional()
536
+ });
537
+ const setLogOutputSchema = z.object({
538
+ savedWorkoutSetId: z.number(),
539
+ exercisesLogged: z.number().int().nonnegative(),
540
+ setCompleted: z.boolean()
541
+ });
542
+ const setPrescriptionOutputSchema = z.object({
543
+ savedWorkoutSetId: z.number(),
544
+ exercisesPrescribed: z.number().int().nonnegative()
545
+ });
546
+ const exerciseSwapOutputSchema = z.object({
547
+ savedWorkoutSetExerciseId: z.number(),
548
+ athleteId: nullableNumber,
549
+ newExerciseId: z.number(),
550
+ newExerciseTitle: nullableString,
551
+ originalTeamExerciseId: nullableNumber
552
+ });
553
+ const sessionLogSetOutputSchema = z.object({
554
+ savedWorkoutSetId: z.number(),
555
+ exercisesLogged: z.number().int().nonnegative()
556
+ });
557
+ const sessionLogOutputSchema = z.object({
558
+ date: z.string(),
559
+ created: z.boolean(),
560
+ sets: z.array(sessionLogSetOutputSchema),
561
+ scheduledAlternatives: z.array(z.object({
562
+ exerciseId: z.number(),
563
+ title: z.string(),
564
+ program: nullableString,
565
+ workoutTitle: z.string(),
566
+ savedWorkoutSetId: z.number(),
567
+ savedWorkoutSetExerciseId: z.number()
568
+ })).optional()
569
+ });
570
+ const personalWorkoutCreatedOutputSchema = z.object({
571
+ programWorkoutId: z.number(),
572
+ workoutId: z.number(),
573
+ savedWorkoutId: z.number(),
574
+ groupId: z.number(),
575
+ date: z.string()
576
+ });
577
+ const workoutReadExerciseOutputSchema = z.object({
578
+ order: z.number(),
579
+ title: z.string(),
580
+ reps: z.array(z.string()),
581
+ primaryUnit: nullableString,
582
+ load: z.array(z.string()),
583
+ loadUnit: nullableString,
584
+ instruction: z.string()
585
+ });
586
+ const workoutReadBlockOutputSchema = z.object({
587
+ order: z.number(),
588
+ title: z.string(),
589
+ instruction: z.string(),
590
+ leaderboard: nullableString,
591
+ exercises: z.array(workoutReadExerciseOutputSchema)
592
+ });
593
+ const workoutReadOutputSchema = z.object({
594
+ pwId: z.number(),
595
+ date: z.string(),
596
+ published: z.json().optional(),
597
+ instruction: z.string(),
598
+ blocks: z.array(workoutReadBlockOutputSchema)
599
+ });
600
+ z.object({
601
+ pwId: z.number(),
602
+ workoutId: z.number(),
603
+ programId: z.number(),
604
+ published: z.literal(false),
605
+ advisories: z.object({
606
+ notes: z.array(z.string()),
607
+ warnings: z.array(z.string())
608
+ }),
609
+ readback: workoutReadOutputSchema.nullable(),
610
+ note: z.string()
611
+ });
612
+ z.object({
613
+ published: z.number(),
614
+ readback: workoutReadOutputSchema,
615
+ note: z.string()
616
+ });
617
+ z.object({
618
+ sent: z.literal(true),
619
+ comment: z.json()
620
+ });
621
+ z.object({
622
+ deleted: z.literal(true),
623
+ response: z.json().optional()
624
+ });
625
+ z.object({ deleted: z.number() });
626
+ z.object({ forgotten: z.number() });
627
+ z.looseObject({ id: z.union([z.number(), z.string()]) });
628
+ const mainLiftPrOutputSchema = z.object({
629
+ family: z.enum([
630
+ "cleanjerk",
631
+ "snatch",
632
+ "deadlift",
633
+ "squat",
634
+ "bench",
635
+ "overhead"
636
+ ]),
637
+ label: z.string(),
638
+ exerciseId: nullableNumber,
639
+ title: nullableString,
640
+ weight: nullableNumber,
641
+ reps: nullableNumber,
642
+ units: nullableString,
643
+ date: nullableString
644
+ });
645
+ z.object({
646
+ athleteId: z.number(),
647
+ athleteName: nullableString,
648
+ prs: z.array(mainLiftPrOutputSchema)
649
+ });
650
+ z.object({
651
+ containerId: z.number(),
652
+ programId: z.number(),
653
+ title: z.string(),
654
+ kind: z.enum(["calendar", "fixed"]),
655
+ requestedName: z.string(),
656
+ nameApplied: z.boolean()
657
+ });
658
+ z.object({
659
+ programId: z.number(),
660
+ containerId: nullableNumber
661
+ });
662
+ //#endregion
308
663
  //#region ../dto/src/workout.ts
309
664
  /** A single exercise prescription inside a block. */
310
665
  const exerciseSpecSchema = z.object({
@@ -369,6 +724,12 @@ function toId(value) {
369
724
  }
370
725
  const READ = {
371
726
  readOnlyHint: true,
727
+ destructiveHint: false,
728
+ openWorldHint: true
729
+ };
730
+ const ADDITIVE = {
731
+ readOnlyHint: false,
732
+ destructiveHint: false,
372
733
  openWorldHint: true
373
734
  };
374
735
  const DESTRUCTIVE = {
@@ -390,32 +751,36 @@ async function attempt(fn) {
390
751
  }
391
752
  /** Conservative per-result character cap, below the smallest host cap. */
392
753
  const DEFAULT_RESULT_BUDGET = 6e4;
754
+ /** Smallest cap that can still carry a useful structured truncation envelope. */
755
+ const MIN_RESULT_BUDGET = 256;
393
756
  /** Reserve for the `__truncated` marker so wrapping cannot push back over budget. */
394
757
  const MARKER_RESERVE = 300;
395
758
  const DEFAULT_ARRAY_HINT = "Result was truncated to fit the size budget. Narrow it with a filter/search argument or paginate to see the rest.";
396
759
  const DEFAULT_OBJECT_HINT = "Result was truncated to fit the size budget. Request a more specific id or sub-resource.";
397
760
  /**
398
761
  * Clip an array to its first `keep` items and attach the `__truncated` marker describing what was
399
- * dropped. The marker is model-facing (tools instruct the model to key off `__truncated`), so its
400
- * shape has exactly one definition here: the size-budget path (`boundedSerialize`) and any tool
401
- * that deliberately caps a list (e.g. `athlete_exercises`) emit the same thing.
762
+ * dropped. Every budget fallback that trims a list uses this envelope (`{ items, __truncated }`),
763
+ * including in-place object-array truncation (the sliced array becomes `items` and `field` names
764
+ * the original key). `athlete_exercises` uses the same helper for a client-side catalog cap.
402
765
  */
403
- function clipArray(items, keep, hint) {
766
+ function clipArray(items, keep, hint, field) {
767
+ const marker = {
768
+ returned: keep,
769
+ total: items.length,
770
+ omitted: items.length - keep,
771
+ hint: hint ?? DEFAULT_ARRAY_HINT
772
+ };
773
+ if (field !== void 0) marker.field = field;
404
774
  return {
405
775
  items: items.slice(0, keep),
406
- __truncated: {
407
- returned: keep,
408
- total: items.length,
409
- omitted: items.length - keep,
410
- hint: hint ?? DEFAULT_ARRAY_HINT
411
- }
776
+ __truncated: marker
412
777
  };
413
778
  }
414
779
  /** Active budget. Overridable via TH_MCP_RESULT_BUDGET on Node; the default on workerd. */
415
780
  function resultBudget() {
416
781
  const raw = (globalThis.process?.env)?.TH_MCP_RESULT_BUDGET;
417
782
  const n = raw ? Number(raw) : NaN;
418
- return Number.isFinite(n) && n > 0 ? n : DEFAULT_RESULT_BUDGET;
783
+ return Number.isFinite(n) && n >= MIN_RESULT_BUDGET ? n : DEFAULT_RESULT_BUDGET;
419
784
  }
420
785
  function isPlainObject(value) {
421
786
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -432,13 +797,6 @@ function largestPrefixCount(pieces, charBudget) {
432
797
  }
433
798
  return k;
434
799
  }
435
- /** Last resort: cap a string at the budget and label it as truncated, non-JSON output. */
436
- function hardCap(text, budget, hint) {
437
- if (text.length <= budget) return text;
438
- const note = `\n\n[TRUNCATED: output exceeded ${budget} chars and is NOT valid JSON. ${hint ?? "Narrow the query (filter, paginate, or fetch a specific id)."}]`;
439
- const keep = Math.max(0, budget - note.length);
440
- return text.slice(0, keep) + note;
441
- }
442
800
  function largestArrayValuedKey(obj) {
443
801
  let best = null;
444
802
  let bestLen = -1;
@@ -452,56 +810,79 @@ function largestArrayValuedKey(obj) {
452
810
  }
453
811
  return best;
454
812
  }
455
- /**
456
- * Serialize `data` as JSON within `budget` characters. Small results are pretty-printed.
457
- * Oversized results degrade in order: trim a top-level array (wrapping it as
458
- * `{ items, __truncated }`), then a top-level object's largest array property (annotated
459
- * with `__truncated`), then a last-resort hard character cap. Pure and side-effect free.
460
- */
461
- function boundedSerialize(data, budget, hint) {
462
- if (typeof data === "string") return hardCap(data, budget, hint);
463
- const compact = JSON.stringify(data) ?? "null";
813
+ function jsonValue(data) {
814
+ return JSON.parse(JSON.stringify(data) ?? "null");
815
+ }
816
+ function previewEnvelope(source, budget, hint) {
817
+ const total = source.length;
818
+ const makeValue = (preview, markerHint) => ({
819
+ preview,
820
+ __truncated: {
821
+ total,
822
+ omitted: total - preview.length,
823
+ hint: markerHint
824
+ }
825
+ });
826
+ let markerHint = hint ?? "Narrow the query (filter, paginate, or fetch a specific id).";
827
+ while (JSON.stringify(makeValue("", markerHint)).length > budget && markerHint.length > 0) markerHint = markerHint.slice(0, -1);
828
+ if (JSON.stringify(makeValue("", markerHint)).length > budget) throw new RangeError("Result budget is too small for a structured truncation envelope.");
829
+ let keep = Math.max(0, budget - JSON.stringify(makeValue("", markerHint)).length);
830
+ let value = makeValue(source.slice(0, keep), markerHint);
831
+ while (JSON.stringify(value).length > budget && keep > 0) {
832
+ keep = Math.max(0, keep - (JSON.stringify(value).length - budget));
833
+ value = makeValue(source.slice(0, keep), markerHint);
834
+ }
835
+ return value;
836
+ }
837
+ function boundedResult(data, budget, hint) {
838
+ if (typeof data === "string" && data.length <= budget) return {
839
+ text: data,
840
+ value: data
841
+ };
842
+ const value = jsonValue(data);
843
+ const compact = JSON.stringify(value);
464
844
  if (compact.length <= budget) {
465
- const pretty = JSON.stringify(data, null, 2) ?? "null";
466
- return pretty.length <= budget ? pretty : compact;
845
+ const pretty = JSON.stringify(value, null, 2);
846
+ return {
847
+ text: pretty.length <= budget ? pretty : compact,
848
+ value
849
+ };
467
850
  }
468
- if (Array.isArray(data)) {
469
- const k = largestPrefixCount(data.map((el) => JSON.stringify(el) ?? "null"), budget - MARKER_RESERVE);
470
- const out = JSON.stringify(clipArray(data, k, hint));
471
- if (out.length <= budget) return out;
472
- } else if (isPlainObject(data)) {
473
- const key = largestArrayValuedKey(data);
851
+ if (Array.isArray(value)) {
852
+ const truncated = clipArray(value, largestPrefixCount(value.map((element) => JSON.stringify(element)), budget - MARKER_RESERVE), hint);
853
+ const text = JSON.stringify(truncated);
854
+ if (text.length <= budget) return {
855
+ text,
856
+ value: truncated
857
+ };
858
+ } else if (isPlainObject(value)) {
859
+ const key = largestArrayValuedKey(value);
474
860
  if (key !== null) {
475
- const arr = data[key];
476
- const pieces = arr.map((el) => JSON.stringify(el) ?? "null");
477
- const restLen = (JSON.stringify({
478
- ...data,
479
- [key]: []
480
- }) ?? "{}").length;
481
- const k = largestPrefixCount(pieces, Math.max(0, budget - MARKER_RESERVE - restLen));
482
- const clone = {
483
- ...data,
484
- [key]: arr.slice(0, k),
485
- __truncated: {
486
- field: key,
487
- returned: k,
488
- total: arr.length,
489
- omitted: arr.length - k,
490
- hint: hint ?? DEFAULT_OBJECT_HINT
491
- }
861
+ const array = value[key];
862
+ const truncated = clipArray(array, largestPrefixCount(array.map((element) => JSON.stringify(element)), budget - MARKER_RESERVE), hint ?? DEFAULT_OBJECT_HINT, key);
863
+ const text = JSON.stringify(truncated);
864
+ if (text.length <= budget) return {
865
+ text,
866
+ value: truncated
492
867
  };
493
- const out = JSON.stringify(clone);
494
- if (out.length <= budget) return out;
495
868
  }
496
869
  }
497
- return hardCap(compact, budget, hint);
870
+ const truncated = previewEnvelope(typeof data === "string" ? data : compact, budget, hint);
871
+ return {
872
+ text: JSON.stringify(truncated),
873
+ value: truncated
874
+ };
498
875
  }
499
876
  /** A successful tool result carrying JSON (or text) for the model, size-bounded. */
500
877
  function jsonResult(data, opts) {
501
- return { content: [{
502
- type: "text",
503
- text: boundedSerialize(data, resultBudget(), opts?.hint)
504
- }] };
878
+ const { text, value } = boundedResult(data, resultBudget(), opts?.hint);
879
+ return {
880
+ content: [{
881
+ type: "text",
882
+ text
883
+ }],
884
+ structuredContent: value
885
+ };
505
886
  }
506
887
  /** A tool-level error: returned in-band (isError) so the model can self-correct. */
507
888
  function errorResult(message) {
@@ -1830,12 +2211,14 @@ function registerProfileTools(server, ctx, whoami, userId) {
1830
2211
  title: "Who am I (athlete)",
1831
2212
  description: "The logged-in account's identity (id, name, roles) from /user/simple.",
1832
2213
  inputSchema: {},
2214
+ outputSchema: toolOutputSchema(userSimpleSchema),
1833
2215
  annotations: READ
1834
2216
  }, () => attempt(async () => jsonResult(await whoami())));
1835
2217
  server.registerTool("athlete_profile", {
1836
2218
  title: "Athlete profile + lifetime totals",
1837
2219
  description: "Lifetime training totals in one call — all-time session count (summary.sessions_count), total reps and volume, first/last logged date — plus the profile (name, units, dob). Use this for any 'how many sessions all-time / total volume ever' question rather than summing athlete_workouts windows. Set useMetric for kg/metric totals.",
1838
2220
  inputSchema: { useMetric: z.boolean().optional() },
2221
+ outputSchema: toolOutputSchema(athleteProfileOutputSchema),
1839
2222
  annotations: READ
1840
2223
  }, ({ useMetric }) => attempt(async () => {
1841
2224
  const id = await userId();
@@ -1849,12 +2232,14 @@ function registerProfileTools(server, ctx, whoami, userId) {
1849
2232
  title: "Athlete preferences",
1850
2233
  description: "Notification and display preference flags for the athlete account.",
1851
2234
  inputSchema: {},
2235
+ outputSchema: toolOutputSchema(athletePrefsSchema),
1852
2236
  annotations: READ
1853
2237
  }, () => attempt(async () => jsonResult(await fetchAthletePrefs(ctx.client))));
1854
2238
  server.registerTool("athlete_working_maxes", {
1855
2239
  title: "Working maxes",
1856
2240
  description: "The athlete's working max per exercise (drives % prescriptions). An entry can carry a null value: the exercise has a working-max slot but no number has been set yet, which means there is effectively no working max for it.",
1857
2241
  inputSchema: {},
2242
+ outputSchema: toolOutputSchema(athleteWorkingMaxListSchema),
1858
2243
  annotations: READ
1859
2244
  }, () => attempt(async () => jsonResult(await fetchWorkingMaxes(ctx.client))));
1860
2245
  server.registerTool("athlete_leaderboard", {
@@ -1866,6 +2251,7 @@ function registerProfileTools(server, ctx, whoami, userId) {
1866
2251
  pageSize: z.number().int().positive().max(200).optional(),
1867
2252
  gender: z.number().int().optional()
1868
2253
  },
2254
+ outputSchema: opaqueOutputSchema,
1869
2255
  annotations: READ
1870
2256
  }, ({ workoutId, page, pageSize, gender }) => attempt(async () => {
1871
2257
  const opts = {};
@@ -1922,6 +2308,7 @@ function registerExerciseTools(server, ctx, userId) {
1922
2308
  limit: z.number().int().positive().max(200).optional(),
1923
2309
  summary: z.boolean().optional()
1924
2310
  },
2311
+ outputSchema: toolOutputSchema(athleteWorkoutsOutputSchema),
1925
2312
  annotations: READ
1926
2313
  }, (args) => runAthleteWorkouts(ctx, args));
1927
2314
  server.registerTool("athlete_exercises", {
@@ -1931,6 +2318,7 @@ function registerExerciseTools(server, ctx, userId) {
1931
2318
  q: z.string().optional(),
1932
2319
  limit: z.number().int().positive().optional()
1933
2320
  },
2321
+ outputSchema: toolOutputSchema(athleteExerciseCatalogOutputSchema),
1934
2322
  annotations: READ
1935
2323
  }, (args) => runAthleteExercises(ctx, args));
1936
2324
  server.registerTool("athlete_exercise_history", {
@@ -1942,6 +2330,7 @@ function registerExerciseTools(server, ctx, userId) {
1942
2330
  since: dateString.optional(),
1943
2331
  until: dateString.optional()
1944
2332
  },
2333
+ outputSchema: toolOutputSchema(exerciseHistoryOutputSchema),
1945
2334
  annotations: READ
1946
2335
  }, ({ exerciseId, raw, since, until }) => attempt(async () => {
1947
2336
  const detail = await fetchExerciseHistoryDetail(ctx.client, toId(exerciseId), await userId());
@@ -1952,6 +2341,7 @@ function registerExerciseTools(server, ctx, userId) {
1952
2341
  title: "Exercise personal records",
1953
2342
  description: "The all-time PR board for an exercise (reps/weight per rep-max, strength-standard filters). Get the exercise id from athlete_exercises. A lift is often logged under several name variants, each with its own board, so if a PR looks missing check the other variants. For a point-in-time snapshot use athlete_exercise_stats; for the dated session trend use athlete_exercise_history.",
1954
2343
  inputSchema: { exerciseId: idParam },
2344
+ outputSchema: toolOutputSchema(personalRecordListSchema),
1955
2345
  annotations: READ
1956
2346
  }, ({ exerciseId }) => attempt(async () => jsonResult(await fetchPersonalRecords(ctx.client, toId(exerciseId)))));
1957
2347
  server.registerTool("athlete_exercise_stats", {
@@ -1961,6 +2351,7 @@ function registerExerciseTools(server, ctx, userId) {
1961
2351
  exerciseId: idParam,
1962
2352
  date: dateString
1963
2353
  },
2354
+ outputSchema: toolOutputSchema(exerciseStatsSchema),
1964
2355
  annotations: READ
1965
2356
  }, ({ exerciseId, date }) => attempt(async () => jsonResult(await fetchExerciseStats(ctx.client, toId(exerciseId), await userId(), date))));
1966
2357
  }
@@ -1977,6 +2368,7 @@ function registerLogTargetsTool(server, ctx) {
1977
2368
  teamId: idParam.optional(),
1978
2369
  raw: z.boolean().optional()
1979
2370
  },
2371
+ outputSchema: toolOutputSchema(logTargetsOutputSchema),
1980
2372
  annotations: READ
1981
2373
  }, ({ startDate, endDate, program, programId, teamId, raw }) => attempt(async () => {
1982
2374
  const workouts = selectWorkoutsByProgram(await fetchAthleteWorkouts(ctx.client, startDate, endDate), definedProps({
@@ -1994,11 +2386,8 @@ function registerSessionTools(server, ctx) {
1994
2386
  title: "Create personal workout session",
1995
2387
  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.",
1996
2388
  inputSchema: { date: dateString },
1997
- annotations: {
1998
- readOnlyHint: false,
1999
- destructiveHint: false,
2000
- openWorldHint: true
2001
- }
2389
+ outputSchema: toolOutputSchema(personalWorkoutCreatedOutputSchema),
2390
+ annotations: ADDITIVE
2002
2391
  }, ({ date }) => attempt(async () => jsonResult(await createPersonalWorkout(ctx.client, date))));
2003
2392
  server.registerTool("athlete_session_add_exercises", {
2004
2393
  title: "Add exercises to a personal workout session",
@@ -2010,11 +2399,8 @@ function registerSessionTools(server, ctx) {
2010
2399
  order: z.number().int().positive()
2011
2400
  })).min(1)
2012
2401
  },
2013
- annotations: {
2014
- readOnlyHint: false,
2015
- destructiveHint: false,
2016
- openWorldHint: true
2017
- }
2402
+ outputSchema: opaqueOutputSchema,
2403
+ annotations: ADDITIVE
2018
2404
  }, ({ workoutId, exercises }) => attempt(async () => {
2019
2405
  const mapped = exercises.map((e) => ({
2020
2406
  exerciseId: toId(e.exerciseId),
@@ -2029,6 +2415,7 @@ function registerSessionTools(server, ctx) {
2029
2415
  ...athleteSessionRemoveArgsSchema.shape,
2030
2416
  confirm: z.boolean().optional()
2031
2417
  },
2418
+ outputSchema: toolOutputSchema(athleteSessionRemovedOutputSchema),
2032
2419
  annotations: DESTRUCTIVE
2033
2420
  }, ({ programWorkoutId, date, confirm }, extra) => attempt(async () => {
2034
2421
  const id = toId(programWorkoutId);
@@ -2071,6 +2458,7 @@ function registerLogTool(server, ctx) {
2071
2458
  ...logSessionArgsSchema.shape,
2072
2459
  confirm: z.boolean().optional()
2073
2460
  },
2461
+ outputSchema: toolOutputSchema(sessionLogOutputSchema),
2074
2462
  annotations: DESTRUCTIVE
2075
2463
  }, ({ date, exercises, confirm }, extra) => attempt(async () => {
2076
2464
  const blocked = confirmGate(extra, `Log a session of ${exercises.length} exercise(s) on ${date}? This writes to your coach-visible training log.`, confirm);
@@ -2089,6 +2477,7 @@ function registerLogTool(server, ctx) {
2089
2477
  ...logSetArgsSchema.shape,
2090
2478
  confirm: z.boolean().optional()
2091
2479
  },
2480
+ outputSchema: toolOutputSchema(setLogOutputSchema),
2092
2481
  annotations: DESTRUCTIVE
2093
2482
  }, ({ date, savedWorkoutSetId, results, confirm }, extra) => attempt(async () => {
2094
2483
  const blocked = confirmGate(extra, `Log results to saved workout set ${toId(savedWorkoutSetId)} on ${date}? This writes to your coach-visible training log.`, confirm);
@@ -2106,6 +2495,7 @@ function registerLogTool(server, ctx) {
2106
2495
  ...athletePrescribeSetArgsSchema.shape,
2107
2496
  confirm: z.boolean().optional()
2108
2497
  },
2498
+ outputSchema: toolOutputSchema(setPrescriptionOutputSchema),
2109
2499
  annotations: DESTRUCTIVE
2110
2500
  }, ({ date, savedWorkoutSetId, results, confirm }, extra) => attempt(async () => {
2111
2501
  const id = toId(savedWorkoutSetId);
@@ -2132,6 +2522,7 @@ function registerSwapTool(server, ctx) {
2132
2522
  ...swapAthleteExerciseArgsSchema.shape,
2133
2523
  confirm: z.boolean().optional()
2134
2524
  },
2525
+ outputSchema: toolOutputSchema(exerciseSwapOutputSchema),
2135
2526
  annotations: DESTRUCTIVE
2136
2527
  }, ({ savedWorkoutSetExerciseId, exerciseId, confirm }, extra) => attempt(async () => {
2137
2528
  const sweId = toId(savedWorkoutSetExerciseId);
@@ -2149,18 +2540,21 @@ function registerCatalogReads(server, ctx) {
2149
2540
  title: "Circuit history",
2150
2541
  description: "Named circuit history for the logged-in athlete (GET /v5/users/circuits/{recent|history}). kind defaults to recent. Empty when the athlete has no saved circuits. Distinct from circuit *blocks* in workout_build (type 1).",
2151
2542
  inputSchema: { kind: z.enum(["recent", "history"]).optional() },
2543
+ outputSchema: opaqueOutputSchema,
2152
2544
  annotations: READ
2153
2545
  }, ({ kind }) => attempt(async () => jsonResult(await fetchAthleteCircuits(ctx.client, kind ?? "recent"))));
2154
2546
  server.registerTool("athlete_programming_programs", {
2155
2547
  title: "Subscribed programs",
2156
2548
  description: "Programs the athlete is subscribed to (GET /1.0/athlete/programming/programs). Not the coach list_programs surface. Empty when the athlete has no subscriptions.",
2157
2549
  inputSchema: {},
2550
+ outputSchema: opaqueOutputSchema,
2158
2551
  annotations: READ
2159
2552
  }, () => attempt(async () => jsonResult(await fetchAthleteProgrammingPrograms(ctx.client))));
2160
2553
  server.registerTool("athlete_recent_exercises", {
2161
2554
  title: "Recent exercises",
2162
2555
  description: "Recently used exercises (GET /v5/users/exercises/recent). Distinct from athlete_exercises (full logged catalog) and athlete_exercise_history (one lift).",
2163
2556
  inputSchema: {},
2557
+ outputSchema: opaqueOutputSchema,
2164
2558
  annotations: READ
2165
2559
  }, () => attempt(async () => jsonResult(await fetchRecentExercises(ctx.client))));
2166
2560
  }
@@ -2194,7 +2588,7 @@ function registerAthleteTrainingTools(server, ctx) {
2194
2588
  }
2195
2589
  //#endregion
2196
2590
  //#region package.json
2197
- var version = "3.2.0";
2591
+ var version = "3.3.0";
2198
2592
  //#endregion
2199
2593
  //#region src/server.ts
2200
2594
  function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trainheroic-unofficial/athlete-mcp",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
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/core": "3.2.0",
25
- "@trainheroic-unofficial/js": "3.2.0"
24
+ "@trainheroic-unofficial/core": "3.3.0",
25
+ "@trainheroic-unofficial/js": "3.3.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^26.2.0",
29
29
  "tsdown": "^0.22.14",
30
- "tsx": "^4.23.11",
30
+ "tsx": "^4.23.12",
31
31
  "typescript": "^7.0.2",
32
32
  "vitest": "^4.1.10"
33
33
  },