@trainheroic-unofficial/athlete-mcp 3.1.1 → 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.
- package/dist/server.mjs +513 -79
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(),
|
|
@@ -298,6 +461,204 @@ z.looseObject({
|
|
|
298
461
|
program_type: intLike
|
|
299
462
|
}).optional()
|
|
300
463
|
});
|
|
464
|
+
z.object({
|
|
465
|
+
pub_enabled: z.union([z.number(), z.boolean()]).optional(),
|
|
466
|
+
pub_days: z.unknown().optional(),
|
|
467
|
+
pub_time: z.unknown().optional(),
|
|
468
|
+
pub_timezone: z.string().optional()
|
|
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" });
|
|
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
|
+
});
|
|
301
662
|
//#endregion
|
|
302
663
|
//#region ../dto/src/workout.ts
|
|
303
664
|
/** A single exercise prescription inside a block. */
|
|
@@ -350,6 +711,10 @@ z.object({
|
|
|
350
711
|
blocks: z.array(blockSpecSchema),
|
|
351
712
|
instruction: z.string().optional()
|
|
352
713
|
});
|
|
714
|
+
z.object({
|
|
715
|
+
title: z.string().min(1),
|
|
716
|
+
instruction: z.string().optional()
|
|
717
|
+
});
|
|
353
718
|
//#endregion
|
|
354
719
|
//#region ../core/src/context.ts
|
|
355
720
|
/** A tool argument that accepts a numeric id as a number or a string of digits. */
|
|
@@ -359,6 +724,12 @@ function toId(value) {
|
|
|
359
724
|
}
|
|
360
725
|
const READ = {
|
|
361
726
|
readOnlyHint: true,
|
|
727
|
+
destructiveHint: false,
|
|
728
|
+
openWorldHint: true
|
|
729
|
+
};
|
|
730
|
+
const ADDITIVE = {
|
|
731
|
+
readOnlyHint: false,
|
|
732
|
+
destructiveHint: false,
|
|
362
733
|
openWorldHint: true
|
|
363
734
|
};
|
|
364
735
|
const DESTRUCTIVE = {
|
|
@@ -380,32 +751,36 @@ async function attempt(fn) {
|
|
|
380
751
|
}
|
|
381
752
|
/** Conservative per-result character cap, below the smallest host cap. */
|
|
382
753
|
const DEFAULT_RESULT_BUDGET = 6e4;
|
|
754
|
+
/** Smallest cap that can still carry a useful structured truncation envelope. */
|
|
755
|
+
const MIN_RESULT_BUDGET = 256;
|
|
383
756
|
/** Reserve for the `__truncated` marker so wrapping cannot push back over budget. */
|
|
384
757
|
const MARKER_RESERVE = 300;
|
|
385
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.";
|
|
386
759
|
const DEFAULT_OBJECT_HINT = "Result was truncated to fit the size budget. Request a more specific id or sub-resource.";
|
|
387
760
|
/**
|
|
388
761
|
* Clip an array to its first `keep` items and attach the `__truncated` marker describing what was
|
|
389
|
-
* dropped.
|
|
390
|
-
*
|
|
391
|
-
*
|
|
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.
|
|
392
765
|
*/
|
|
393
|
-
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;
|
|
394
774
|
return {
|
|
395
775
|
items: items.slice(0, keep),
|
|
396
|
-
__truncated:
|
|
397
|
-
returned: keep,
|
|
398
|
-
total: items.length,
|
|
399
|
-
omitted: items.length - keep,
|
|
400
|
-
hint: hint ?? DEFAULT_ARRAY_HINT
|
|
401
|
-
}
|
|
776
|
+
__truncated: marker
|
|
402
777
|
};
|
|
403
778
|
}
|
|
404
779
|
/** Active budget. Overridable via TH_MCP_RESULT_BUDGET on Node; the default on workerd. */
|
|
405
780
|
function resultBudget() {
|
|
406
781
|
const raw = (globalThis.process?.env)?.TH_MCP_RESULT_BUDGET;
|
|
407
782
|
const n = raw ? Number(raw) : NaN;
|
|
408
|
-
return Number.isFinite(n) && n
|
|
783
|
+
return Number.isFinite(n) && n >= MIN_RESULT_BUDGET ? n : DEFAULT_RESULT_BUDGET;
|
|
409
784
|
}
|
|
410
785
|
function isPlainObject(value) {
|
|
411
786
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -422,13 +797,6 @@ function largestPrefixCount(pieces, charBudget) {
|
|
|
422
797
|
}
|
|
423
798
|
return k;
|
|
424
799
|
}
|
|
425
|
-
/** Last resort: cap a string at the budget and label it as truncated, non-JSON output. */
|
|
426
|
-
function hardCap(text, budget, hint) {
|
|
427
|
-
if (text.length <= budget) return text;
|
|
428
|
-
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)."}]`;
|
|
429
|
-
const keep = Math.max(0, budget - note.length);
|
|
430
|
-
return text.slice(0, keep) + note;
|
|
431
|
-
}
|
|
432
800
|
function largestArrayValuedKey(obj) {
|
|
433
801
|
let best = null;
|
|
434
802
|
let bestLen = -1;
|
|
@@ -442,56 +810,79 @@ function largestArrayValuedKey(obj) {
|
|
|
442
810
|
}
|
|
443
811
|
return best;
|
|
444
812
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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);
|
|
454
844
|
if (compact.length <= budget) {
|
|
455
|
-
const pretty = JSON.stringify(
|
|
456
|
-
return
|
|
845
|
+
const pretty = JSON.stringify(value, null, 2);
|
|
846
|
+
return {
|
|
847
|
+
text: pretty.length <= budget ? pretty : compact,
|
|
848
|
+
value
|
|
849
|
+
};
|
|
457
850
|
}
|
|
458
|
-
if (Array.isArray(
|
|
459
|
-
const
|
|
460
|
-
const
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
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);
|
|
464
860
|
if (key !== null) {
|
|
465
|
-
const
|
|
466
|
-
const
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const k = largestPrefixCount(pieces, Math.max(0, budget - MARKER_RESERVE - restLen));
|
|
472
|
-
const clone = {
|
|
473
|
-
...data,
|
|
474
|
-
[key]: arr.slice(0, k),
|
|
475
|
-
__truncated: {
|
|
476
|
-
field: key,
|
|
477
|
-
returned: k,
|
|
478
|
-
total: arr.length,
|
|
479
|
-
omitted: arr.length - k,
|
|
480
|
-
hint: hint ?? DEFAULT_OBJECT_HINT
|
|
481
|
-
}
|
|
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
|
|
482
867
|
};
|
|
483
|
-
const out = JSON.stringify(clone);
|
|
484
|
-
if (out.length <= budget) return out;
|
|
485
868
|
}
|
|
486
869
|
}
|
|
487
|
-
|
|
870
|
+
const truncated = previewEnvelope(typeof data === "string" ? data : compact, budget, hint);
|
|
871
|
+
return {
|
|
872
|
+
text: JSON.stringify(truncated),
|
|
873
|
+
value: truncated
|
|
874
|
+
};
|
|
488
875
|
}
|
|
489
876
|
/** A successful tool result carrying JSON (or text) for the model, size-bounded. */
|
|
490
877
|
function jsonResult(data, opts) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
878
|
+
const { text, value } = boundedResult(data, resultBudget(), opts?.hint);
|
|
879
|
+
return {
|
|
880
|
+
content: [{
|
|
881
|
+
type: "text",
|
|
882
|
+
text
|
|
883
|
+
}],
|
|
884
|
+
structuredContent: value
|
|
885
|
+
};
|
|
495
886
|
}
|
|
496
887
|
/** A tool-level error: returned in-band (isError) so the model can self-correct. */
|
|
497
888
|
function errorResult(message) {
|
|
@@ -860,6 +1251,15 @@ function fetchWorkingMaxes(client) {
|
|
|
860
1251
|
function fetchExerciseHistoryList(client) {
|
|
861
1252
|
return getArray(client, "/v5/users/exercises/history", "athlete exercise history list");
|
|
862
1253
|
}
|
|
1254
|
+
function fetchRecentExercises(client) {
|
|
1255
|
+
return getArray(client, "/v5/users/exercises/recent", "athlete recent exercises");
|
|
1256
|
+
}
|
|
1257
|
+
function fetchAthleteCircuits(client, kind = "recent") {
|
|
1258
|
+
return getArray(client, `/v5/users/circuits/${kind}`, `athlete ${kind} circuits`);
|
|
1259
|
+
}
|
|
1260
|
+
function fetchAthleteProgrammingPrograms(client) {
|
|
1261
|
+
return getArray(client, "/1.0/athlete/programming/programs", "athlete programming programs");
|
|
1262
|
+
}
|
|
863
1263
|
/** Free-text search over the athlete's logged exercises (FTS replacement via rankSearch). */
|
|
864
1264
|
async function searchExerciseHistory(client, query, limit = 20) {
|
|
865
1265
|
return rankSearch(await fetchExerciseHistoryList(client), query, limit);
|
|
@@ -1811,12 +2211,14 @@ function registerProfileTools(server, ctx, whoami, userId) {
|
|
|
1811
2211
|
title: "Who am I (athlete)",
|
|
1812
2212
|
description: "The logged-in account's identity (id, name, roles) from /user/simple.",
|
|
1813
2213
|
inputSchema: {},
|
|
2214
|
+
outputSchema: toolOutputSchema(userSimpleSchema),
|
|
1814
2215
|
annotations: READ
|
|
1815
2216
|
}, () => attempt(async () => jsonResult(await whoami())));
|
|
1816
2217
|
server.registerTool("athlete_profile", {
|
|
1817
2218
|
title: "Athlete profile + lifetime totals",
|
|
1818
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.",
|
|
1819
2220
|
inputSchema: { useMetric: z.boolean().optional() },
|
|
2221
|
+
outputSchema: toolOutputSchema(athleteProfileOutputSchema),
|
|
1820
2222
|
annotations: READ
|
|
1821
2223
|
}, ({ useMetric }) => attempt(async () => {
|
|
1822
2224
|
const id = await userId();
|
|
@@ -1830,12 +2232,14 @@ function registerProfileTools(server, ctx, whoami, userId) {
|
|
|
1830
2232
|
title: "Athlete preferences",
|
|
1831
2233
|
description: "Notification and display preference flags for the athlete account.",
|
|
1832
2234
|
inputSchema: {},
|
|
2235
|
+
outputSchema: toolOutputSchema(athletePrefsSchema),
|
|
1833
2236
|
annotations: READ
|
|
1834
2237
|
}, () => attempt(async () => jsonResult(await fetchAthletePrefs(ctx.client))));
|
|
1835
2238
|
server.registerTool("athlete_working_maxes", {
|
|
1836
2239
|
title: "Working maxes",
|
|
1837
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.",
|
|
1838
2241
|
inputSchema: {},
|
|
2242
|
+
outputSchema: toolOutputSchema(athleteWorkingMaxListSchema),
|
|
1839
2243
|
annotations: READ
|
|
1840
2244
|
}, () => attempt(async () => jsonResult(await fetchWorkingMaxes(ctx.client))));
|
|
1841
2245
|
server.registerTool("athlete_leaderboard", {
|
|
@@ -1847,6 +2251,7 @@ function registerProfileTools(server, ctx, whoami, userId) {
|
|
|
1847
2251
|
pageSize: z.number().int().positive().max(200).optional(),
|
|
1848
2252
|
gender: z.number().int().optional()
|
|
1849
2253
|
},
|
|
2254
|
+
outputSchema: opaqueOutputSchema,
|
|
1850
2255
|
annotations: READ
|
|
1851
2256
|
}, ({ workoutId, page, pageSize, gender }) => attempt(async () => {
|
|
1852
2257
|
const opts = {};
|
|
@@ -1903,6 +2308,7 @@ function registerExerciseTools(server, ctx, userId) {
|
|
|
1903
2308
|
limit: z.number().int().positive().max(200).optional(),
|
|
1904
2309
|
summary: z.boolean().optional()
|
|
1905
2310
|
},
|
|
2311
|
+
outputSchema: toolOutputSchema(athleteWorkoutsOutputSchema),
|
|
1906
2312
|
annotations: READ
|
|
1907
2313
|
}, (args) => runAthleteWorkouts(ctx, args));
|
|
1908
2314
|
server.registerTool("athlete_exercises", {
|
|
@@ -1912,6 +2318,7 @@ function registerExerciseTools(server, ctx, userId) {
|
|
|
1912
2318
|
q: z.string().optional(),
|
|
1913
2319
|
limit: z.number().int().positive().optional()
|
|
1914
2320
|
},
|
|
2321
|
+
outputSchema: toolOutputSchema(athleteExerciseCatalogOutputSchema),
|
|
1915
2322
|
annotations: READ
|
|
1916
2323
|
}, (args) => runAthleteExercises(ctx, args));
|
|
1917
2324
|
server.registerTool("athlete_exercise_history", {
|
|
@@ -1923,6 +2330,7 @@ function registerExerciseTools(server, ctx, userId) {
|
|
|
1923
2330
|
since: dateString.optional(),
|
|
1924
2331
|
until: dateString.optional()
|
|
1925
2332
|
},
|
|
2333
|
+
outputSchema: toolOutputSchema(exerciseHistoryOutputSchema),
|
|
1926
2334
|
annotations: READ
|
|
1927
2335
|
}, ({ exerciseId, raw, since, until }) => attempt(async () => {
|
|
1928
2336
|
const detail = await fetchExerciseHistoryDetail(ctx.client, toId(exerciseId), await userId());
|
|
@@ -1933,6 +2341,7 @@ function registerExerciseTools(server, ctx, userId) {
|
|
|
1933
2341
|
title: "Exercise personal records",
|
|
1934
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.",
|
|
1935
2343
|
inputSchema: { exerciseId: idParam },
|
|
2344
|
+
outputSchema: toolOutputSchema(personalRecordListSchema),
|
|
1936
2345
|
annotations: READ
|
|
1937
2346
|
}, ({ exerciseId }) => attempt(async () => jsonResult(await fetchPersonalRecords(ctx.client, toId(exerciseId)))));
|
|
1938
2347
|
server.registerTool("athlete_exercise_stats", {
|
|
@@ -1942,6 +2351,7 @@ function registerExerciseTools(server, ctx, userId) {
|
|
|
1942
2351
|
exerciseId: idParam,
|
|
1943
2352
|
date: dateString
|
|
1944
2353
|
},
|
|
2354
|
+
outputSchema: toolOutputSchema(exerciseStatsSchema),
|
|
1945
2355
|
annotations: READ
|
|
1946
2356
|
}, ({ exerciseId, date }) => attempt(async () => jsonResult(await fetchExerciseStats(ctx.client, toId(exerciseId), await userId(), date))));
|
|
1947
2357
|
}
|
|
@@ -1958,6 +2368,7 @@ function registerLogTargetsTool(server, ctx) {
|
|
|
1958
2368
|
teamId: idParam.optional(),
|
|
1959
2369
|
raw: z.boolean().optional()
|
|
1960
2370
|
},
|
|
2371
|
+
outputSchema: toolOutputSchema(logTargetsOutputSchema),
|
|
1961
2372
|
annotations: READ
|
|
1962
2373
|
}, ({ startDate, endDate, program, programId, teamId, raw }) => attempt(async () => {
|
|
1963
2374
|
const workouts = selectWorkoutsByProgram(await fetchAthleteWorkouts(ctx.client, startDate, endDate), definedProps({
|
|
@@ -1975,11 +2386,8 @@ function registerSessionTools(server, ctx) {
|
|
|
1975
2386
|
title: "Create personal workout session",
|
|
1976
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.",
|
|
1977
2388
|
inputSchema: { date: dateString },
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
destructiveHint: false,
|
|
1981
|
-
openWorldHint: true
|
|
1982
|
-
}
|
|
2389
|
+
outputSchema: toolOutputSchema(personalWorkoutCreatedOutputSchema),
|
|
2390
|
+
annotations: ADDITIVE
|
|
1983
2391
|
}, ({ date }) => attempt(async () => jsonResult(await createPersonalWorkout(ctx.client, date))));
|
|
1984
2392
|
server.registerTool("athlete_session_add_exercises", {
|
|
1985
2393
|
title: "Add exercises to a personal workout session",
|
|
@@ -1991,11 +2399,8 @@ function registerSessionTools(server, ctx) {
|
|
|
1991
2399
|
order: z.number().int().positive()
|
|
1992
2400
|
})).min(1)
|
|
1993
2401
|
},
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
destructiveHint: false,
|
|
1997
|
-
openWorldHint: true
|
|
1998
|
-
}
|
|
2402
|
+
outputSchema: opaqueOutputSchema,
|
|
2403
|
+
annotations: ADDITIVE
|
|
1999
2404
|
}, ({ workoutId, exercises }) => attempt(async () => {
|
|
2000
2405
|
const mapped = exercises.map((e) => ({
|
|
2001
2406
|
exerciseId: toId(e.exerciseId),
|
|
@@ -2010,6 +2415,7 @@ function registerSessionTools(server, ctx) {
|
|
|
2010
2415
|
...athleteSessionRemoveArgsSchema.shape,
|
|
2011
2416
|
confirm: z.boolean().optional()
|
|
2012
2417
|
},
|
|
2418
|
+
outputSchema: toolOutputSchema(athleteSessionRemovedOutputSchema),
|
|
2013
2419
|
annotations: DESTRUCTIVE
|
|
2014
2420
|
}, ({ programWorkoutId, date, confirm }, extra) => attempt(async () => {
|
|
2015
2421
|
const id = toId(programWorkoutId);
|
|
@@ -2052,6 +2458,7 @@ function registerLogTool(server, ctx) {
|
|
|
2052
2458
|
...logSessionArgsSchema.shape,
|
|
2053
2459
|
confirm: z.boolean().optional()
|
|
2054
2460
|
},
|
|
2461
|
+
outputSchema: toolOutputSchema(sessionLogOutputSchema),
|
|
2055
2462
|
annotations: DESTRUCTIVE
|
|
2056
2463
|
}, ({ date, exercises, confirm }, extra) => attempt(async () => {
|
|
2057
2464
|
const blocked = confirmGate(extra, `Log a session of ${exercises.length} exercise(s) on ${date}? This writes to your coach-visible training log.`, confirm);
|
|
@@ -2070,6 +2477,7 @@ function registerLogTool(server, ctx) {
|
|
|
2070
2477
|
...logSetArgsSchema.shape,
|
|
2071
2478
|
confirm: z.boolean().optional()
|
|
2072
2479
|
},
|
|
2480
|
+
outputSchema: toolOutputSchema(setLogOutputSchema),
|
|
2073
2481
|
annotations: DESTRUCTIVE
|
|
2074
2482
|
}, ({ date, savedWorkoutSetId, results, confirm }, extra) => attempt(async () => {
|
|
2075
2483
|
const blocked = confirmGate(extra, `Log results to saved workout set ${toId(savedWorkoutSetId)} on ${date}? This writes to your coach-visible training log.`, confirm);
|
|
@@ -2087,6 +2495,7 @@ function registerLogTool(server, ctx) {
|
|
|
2087
2495
|
...athletePrescribeSetArgsSchema.shape,
|
|
2088
2496
|
confirm: z.boolean().optional()
|
|
2089
2497
|
},
|
|
2498
|
+
outputSchema: toolOutputSchema(setPrescriptionOutputSchema),
|
|
2090
2499
|
annotations: DESTRUCTIVE
|
|
2091
2500
|
}, ({ date, savedWorkoutSetId, results, confirm }, extra) => attempt(async () => {
|
|
2092
2501
|
const id = toId(savedWorkoutSetId);
|
|
@@ -2113,6 +2522,7 @@ function registerSwapTool(server, ctx) {
|
|
|
2113
2522
|
...swapAthleteExerciseArgsSchema.shape,
|
|
2114
2523
|
confirm: z.boolean().optional()
|
|
2115
2524
|
},
|
|
2525
|
+
outputSchema: toolOutputSchema(exerciseSwapOutputSchema),
|
|
2116
2526
|
annotations: DESTRUCTIVE
|
|
2117
2527
|
}, ({ savedWorkoutSetExerciseId, exerciseId, confirm }, extra) => attempt(async () => {
|
|
2118
2528
|
const sweId = toId(savedWorkoutSetExerciseId);
|
|
@@ -2125,6 +2535,29 @@ function registerSwapTool(server, ctx) {
|
|
|
2125
2535
|
}));
|
|
2126
2536
|
}));
|
|
2127
2537
|
}
|
|
2538
|
+
function registerCatalogReads(server, ctx) {
|
|
2539
|
+
server.registerTool("athlete_circuits", {
|
|
2540
|
+
title: "Circuit history",
|
|
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).",
|
|
2542
|
+
inputSchema: { kind: z.enum(["recent", "history"]).optional() },
|
|
2543
|
+
outputSchema: opaqueOutputSchema,
|
|
2544
|
+
annotations: READ
|
|
2545
|
+
}, ({ kind }) => attempt(async () => jsonResult(await fetchAthleteCircuits(ctx.client, kind ?? "recent"))));
|
|
2546
|
+
server.registerTool("athlete_programming_programs", {
|
|
2547
|
+
title: "Subscribed programs",
|
|
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.",
|
|
2549
|
+
inputSchema: {},
|
|
2550
|
+
outputSchema: opaqueOutputSchema,
|
|
2551
|
+
annotations: READ
|
|
2552
|
+
}, () => attempt(async () => jsonResult(await fetchAthleteProgrammingPrograms(ctx.client))));
|
|
2553
|
+
server.registerTool("athlete_recent_exercises", {
|
|
2554
|
+
title: "Recent exercises",
|
|
2555
|
+
description: "Recently used exercises (GET /v5/users/exercises/recent). Distinct from athlete_exercises (full logged catalog) and athlete_exercise_history (one lift).",
|
|
2556
|
+
inputSchema: {},
|
|
2557
|
+
outputSchema: opaqueOutputSchema,
|
|
2558
|
+
annotations: READ
|
|
2559
|
+
}, () => attempt(async () => jsonResult(await fetchRecentExercises(ctx.client))));
|
|
2560
|
+
}
|
|
2128
2561
|
/**
|
|
2129
2562
|
* Live tools over the logged-in user's own training (history, scheduled/completed workouts,
|
|
2130
2563
|
* PRs, working maxes), plus a gated set-logging write. The athlete user id is
|
|
@@ -2147,6 +2580,7 @@ function registerAthleteTrainingTools(server, ctx) {
|
|
|
2147
2580
|
};
|
|
2148
2581
|
registerProfileTools(server, ctx, whoami, userId);
|
|
2149
2582
|
registerExerciseTools(server, ctx, userId);
|
|
2583
|
+
registerCatalogReads(server, ctx);
|
|
2150
2584
|
registerLogTargetsTool(server, ctx);
|
|
2151
2585
|
registerSessionTools(server, ctx);
|
|
2152
2586
|
registerLogTool(server, ctx);
|
|
@@ -2154,7 +2588,7 @@ function registerAthleteTrainingTools(server, ctx) {
|
|
|
2154
2588
|
}
|
|
2155
2589
|
//#endregion
|
|
2156
2590
|
//#region package.json
|
|
2157
|
-
var version = "3.
|
|
2591
|
+
var version = "3.3.0";
|
|
2158
2592
|
//#endregion
|
|
2159
2593
|
//#region src/server.ts
|
|
2160
2594
|
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.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.
|
|
25
|
-
"@trainheroic-unofficial/js": "3.
|
|
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.
|
|
30
|
+
"tsx": "^4.23.12",
|
|
31
31
|
"typescript": "^7.0.2",
|
|
32
32
|
"vitest": "^4.1.10"
|
|
33
33
|
},
|