@trainheroic-unofficial/athlete-mcp 3.3.2 → 3.4.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 +109 -14
- package/package.json +5 -5
package/dist/server.mjs
CHANGED
|
@@ -257,6 +257,20 @@ const athleteSessionRemoveArgsSchema = z.object({
|
|
|
257
257
|
programWorkoutId: idArgSchema,
|
|
258
258
|
date: dateString
|
|
259
259
|
});
|
|
260
|
+
/**
|
|
261
|
+
* Args for the athlete session-note write. `programWorkoutId` is the range item's top-level `id`
|
|
262
|
+
* (athlete_workouts); `date` locates that day so the SDK can resolve the saved-workout id the PUT
|
|
263
|
+
* targets. `notes` is the free-text box on the workout screen (empty string clears it). `rpe` is
|
|
264
|
+
* the session RPE (1–10). At least one of `notes` / `rpe` is required — a notes-only PUT leaves
|
|
265
|
+
* rpe untouched and vice versa.
|
|
266
|
+
*/
|
|
267
|
+
const athleteWorkoutNoteObject = z.object({
|
|
268
|
+
date: dateString,
|
|
269
|
+
programWorkoutId: idArgSchema,
|
|
270
|
+
notes: z.string().optional(),
|
|
271
|
+
rpe: z.number().int().min(1).max(10).optional()
|
|
272
|
+
});
|
|
273
|
+
const athleteWorkoutNoteArgsSchema = athleteWorkoutNoteObject.refine((v) => v.notes !== void 0 || v.rpe !== void 0, { message: "Provide notes and/or rpe" });
|
|
260
274
|
const presentedNullNumber = z.number().nullable();
|
|
261
275
|
const presentedNullString = z.string().nullable();
|
|
262
276
|
/** Flattened exercise within a presented workout: prescriptions, logged results, units. */
|
|
@@ -282,6 +296,8 @@ const athleteWorkoutViewSchema = z.object({
|
|
|
282
296
|
program: presentedNullString,
|
|
283
297
|
team: presentedNullString,
|
|
284
298
|
instruction: presentedNullString,
|
|
299
|
+
notes: presentedNullString,
|
|
300
|
+
rpe: presentedNullNumber,
|
|
285
301
|
logged: z.boolean(),
|
|
286
302
|
personal: z.boolean(),
|
|
287
303
|
blocks: z.array(athleteWorkoutBlockSchema)
|
|
@@ -574,6 +590,13 @@ const personalWorkoutCreatedOutputSchema = z.object({
|
|
|
574
590
|
groupId: z.number(),
|
|
575
591
|
date: z.string()
|
|
576
592
|
});
|
|
593
|
+
const athleteWorkoutNoteOutputSchema = z.object({
|
|
594
|
+
programWorkoutId: z.number(),
|
|
595
|
+
savedWorkoutId: z.number(),
|
|
596
|
+
date: z.string(),
|
|
597
|
+
notes: z.string(),
|
|
598
|
+
rpe: nullableNumber
|
|
599
|
+
});
|
|
577
600
|
const workoutReadExerciseOutputSchema = z.object({
|
|
578
601
|
order: z.number(),
|
|
579
602
|
title: z.string(),
|
|
@@ -725,17 +748,17 @@ function toId(value) {
|
|
|
725
748
|
const READ = {
|
|
726
749
|
readOnlyHint: true,
|
|
727
750
|
destructiveHint: false,
|
|
728
|
-
openWorldHint:
|
|
751
|
+
openWorldHint: false
|
|
729
752
|
};
|
|
730
753
|
const ADDITIVE = {
|
|
731
754
|
readOnlyHint: false,
|
|
732
755
|
destructiveHint: false,
|
|
733
|
-
openWorldHint:
|
|
756
|
+
openWorldHint: false
|
|
734
757
|
};
|
|
735
758
|
const DESTRUCTIVE = {
|
|
736
759
|
readOnlyHint: false,
|
|
737
760
|
destructiveHint: true,
|
|
738
|
-
openWorldHint:
|
|
761
|
+
openWorldHint: false
|
|
739
762
|
};
|
|
740
763
|
/**
|
|
741
764
|
* Run a tool body, converting thrown errors into an in-band tool error. Generic so a caller that
|
|
@@ -1202,6 +1225,15 @@ function isPersonalSession(pw) {
|
|
|
1202
1225
|
return isRecord(pw) && pw.personal_cal === true;
|
|
1203
1226
|
}
|
|
1204
1227
|
/**
|
|
1228
|
+
* The `saved_workout` blob on a program-workout range item, or null if the athlete has no saved
|
|
1229
|
+
* copy yet. Session notes, RPE, and set writes all resolve through this.
|
|
1230
|
+
*/
|
|
1231
|
+
function savedWorkoutOf(pw) {
|
|
1232
|
+
if (!isRecord(pw)) return null;
|
|
1233
|
+
const ssw = isRecord(pw.summarizedSavedWorkout) ? pw.summarizedSavedWorkout : {};
|
|
1234
|
+
return isRecord(ssw.saved_workout) ? ssw.saved_workout : null;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1205
1237
|
* Rank candidate rows for a free-text query (FTS5 replacement). Higher is better:
|
|
1206
1238
|
* exact title, then prefix, then count of matched tokens, with shorter titles and
|
|
1207
1239
|
* standard (non-custom) exercises preferred on ties.
|
|
@@ -1278,6 +1310,15 @@ function fetchExerciseStats(client, exerciseId, userId, date) {
|
|
|
1278
1310
|
function fetchAthleteWorkouts(client, startDate, endDate) {
|
|
1279
1311
|
return getArray(client, `/3.0/athlete/programworkout/range?startDate=${startDate}&endDate=${endDate}`, "athlete workouts");
|
|
1280
1312
|
}
|
|
1313
|
+
/**
|
|
1314
|
+
* The program-workout row with this id on this date. Throws if that id is not on the day's range;
|
|
1315
|
+
* callers should take the id from `athlete_workouts`.
|
|
1316
|
+
*/
|
|
1317
|
+
async function programWorkoutOnDate(client, date, programWorkoutId) {
|
|
1318
|
+
const target = (await fetchAthleteWorkouts(client, date, date)).find((pw) => coerceInt(pw.id) === programWorkoutId);
|
|
1319
|
+
if (target === void 0) throw new Error(`No workout with id ${programWorkoutId} on ${date}. Get the id and date from athlete_workouts.`);
|
|
1320
|
+
return target;
|
|
1321
|
+
}
|
|
1281
1322
|
function fetchLeaderboard(client, workoutId, opts = {}) {
|
|
1282
1323
|
const qs = new URLSearchParams();
|
|
1283
1324
|
if (opts.page !== void 0) qs.set("page", String(opts.page));
|
|
@@ -1413,7 +1454,7 @@ function mergeAthleteWorkout(raw) {
|
|
|
1413
1454
|
const rec = raw;
|
|
1414
1455
|
const ssw = isRecord(rec.summarizedSavedWorkout) ? rec.summarizedSavedWorkout : {};
|
|
1415
1456
|
const workout = isRecord(ssw.workout) ? ssw.workout : {};
|
|
1416
|
-
const saved =
|
|
1457
|
+
const saved = savedWorkoutOf(rec) ?? {};
|
|
1417
1458
|
const prescriptionSets = (Array.isArray(workout.workoutSets) ? workout.workoutSets : []).filter(isRecord);
|
|
1418
1459
|
const logged = savedSets(saved);
|
|
1419
1460
|
const performedById = performedSlotsByExerciseId(logged);
|
|
@@ -1442,6 +1483,8 @@ function mergeAthleteWorkout(raw) {
|
|
|
1442
1483
|
program: str(rec.program_title),
|
|
1443
1484
|
team: str(rec.team_title),
|
|
1444
1485
|
instruction: str(workout.instruction),
|
|
1486
|
+
notes: str(saved.notes),
|
|
1487
|
+
rpe: coerceInt(saved.rpe),
|
|
1445
1488
|
logged: blocks.some((b) => b.exercises.some((e) => e.sets.some((s) => s.performed !== null))),
|
|
1446
1489
|
personal: isPersonalSession(rec),
|
|
1447
1490
|
blocks
|
|
@@ -1489,6 +1532,8 @@ function presentAthleteWorkout(raw) {
|
|
|
1489
1532
|
program: w.program,
|
|
1490
1533
|
team: w.team,
|
|
1491
1534
|
instruction: w.instruction,
|
|
1535
|
+
notes: w.notes,
|
|
1536
|
+
rpe: w.rpe,
|
|
1492
1537
|
logged: w.logged,
|
|
1493
1538
|
personal: w.personal,
|
|
1494
1539
|
blocks: w.blocks.map(toStringBlock)
|
|
@@ -1751,9 +1796,7 @@ function assertUniqueExerciseResults(results) {
|
|
|
1751
1796
|
function findSavedWorkoutSet(workouts, savedWorkoutSetId) {
|
|
1752
1797
|
const available = [];
|
|
1753
1798
|
for (const pw of workouts) {
|
|
1754
|
-
const
|
|
1755
|
-
const ssw = isRecord(rec.summarizedSavedWorkout) ? rec.summarizedSavedWorkout : {};
|
|
1756
|
-
const sw = isRecord(ssw.saved_workout) ? ssw.saved_workout : null;
|
|
1799
|
+
const sw = savedWorkoutOf(pw);
|
|
1757
1800
|
if (!sw) continue;
|
|
1758
1801
|
const sets = Array.isArray(sw.workoutSets) ? sw.workoutSets : [];
|
|
1759
1802
|
for (const s of sets) {
|
|
@@ -2002,9 +2045,7 @@ async function addExercisesToWorkout(client, workoutId, exercises) {
|
|
|
2002
2045
|
* Throws on a non-ok response.
|
|
2003
2046
|
*/
|
|
2004
2047
|
async function removePersonalWorkout(client, args) {
|
|
2005
|
-
|
|
2006
|
-
if (target === void 0) throw new Error(`No workout with id ${args.programWorkoutId} on ${args.date}. Get the id and date from athlete_workouts.`);
|
|
2007
|
-
if (!isPersonalSession(target)) throw new Error(`Workout ${args.programWorkoutId} on ${args.date} is a coach-scheduled workout, not a personal session, so it can't be removed. To change logged results on a scheduled workout, use athlete_log_set.`);
|
|
2048
|
+
if (!isPersonalSession(await programWorkoutOnDate(client, args.date, args.programWorkoutId))) throw new Error(`Workout ${args.programWorkoutId} on ${args.date} is a coach-scheduled workout, not a personal session, so it can't be removed. To change logged results on a scheduled workout, use athlete_log_set.`);
|
|
2008
2049
|
const res = await client.request("DELETE", `/v5/programWorkouts/${args.programWorkoutId}`);
|
|
2009
2050
|
if (!res.ok) throw new Error(`Remove personal workout failed (HTTP ${res.status}).`);
|
|
2010
2051
|
}
|
|
@@ -2057,8 +2098,7 @@ function eachPrescribedExercise(workouts) {
|
|
|
2057
2098
|
const rows = [];
|
|
2058
2099
|
for (const pw of workouts) {
|
|
2059
2100
|
const rec = pw;
|
|
2060
|
-
const
|
|
2061
|
-
const sw = isRecord(ssw.saved_workout) ? ssw.saved_workout : null;
|
|
2101
|
+
const sw = savedWorkoutOf(rec);
|
|
2062
2102
|
if (!sw) continue;
|
|
2063
2103
|
const sets = [...Array.isArray(sw.workoutSets) ? sw.workoutSets : [], ...Array.isArray(sw.addedWorkoutSets) ? sw.addedWorkoutSets : []];
|
|
2064
2104
|
for (const s of sets) {
|
|
@@ -2182,6 +2222,37 @@ function definedProps(obj) {
|
|
|
2182
2222
|
return result;
|
|
2183
2223
|
}
|
|
2184
2224
|
//#endregion
|
|
2225
|
+
//#region ../js/src/athlete-workout-note.ts
|
|
2226
|
+
/**
|
|
2227
|
+
* PUT /1.0/athlete/savedworkout/{id} — set the athlete's session note (the free-text box on the
|
|
2228
|
+
* workout screen) and/or session RPE. The date + programWorkoutId (`athlete_workouts` `id`) locate
|
|
2229
|
+
* the saved workout; GET on that path 405s, so the range read is the lookup. A notes-only body
|
|
2230
|
+
* leaves rpe untouched and vice versa. Empty `notes` clears the note. Coach-visible.
|
|
2231
|
+
*/
|
|
2232
|
+
async function setAthleteWorkoutNote(client, args) {
|
|
2233
|
+
if (args.notes === void 0 && args.rpe === void 0) throw new Error("Provide notes and/or rpe.");
|
|
2234
|
+
const programWorkoutId = coerceInt(args.programWorkoutId);
|
|
2235
|
+
if (programWorkoutId === null) throw new Error(`Invalid programWorkoutId ${String(args.programWorkoutId)}.`);
|
|
2236
|
+
const saved = savedWorkoutOf(await programWorkoutOnDate(client, args.date, programWorkoutId));
|
|
2237
|
+
const savedWorkoutId = saved !== null ? coerceInt(saved.id) : null;
|
|
2238
|
+
if (savedWorkoutId === null) throw new Error(`Workout ${programWorkoutId} on ${args.date} has no saved workout to attach a note to.`);
|
|
2239
|
+
const body = definedProps({
|
|
2240
|
+
id: savedWorkoutId,
|
|
2241
|
+
notes: args.notes,
|
|
2242
|
+
rpe: args.rpe
|
|
2243
|
+
});
|
|
2244
|
+
const res = await client.request("PUT", `/1.0/athlete/savedworkout/${savedWorkoutId}`, { body });
|
|
2245
|
+
if (!res.ok) throw new Error(`Set workout note failed (HTTP ${res.status}).`);
|
|
2246
|
+
const data = isRecord(res.data) ? res.data : {};
|
|
2247
|
+
return {
|
|
2248
|
+
programWorkoutId,
|
|
2249
|
+
savedWorkoutId,
|
|
2250
|
+
date: args.date,
|
|
2251
|
+
notes: typeof data.notes === "string" ? data.notes : args.notes ?? "",
|
|
2252
|
+
rpe: coerceInt(data.rpe) ?? (args.rpe !== void 0 ? args.rpe : null)
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
//#endregion
|
|
2185
2256
|
//#region ../core/src/history.ts
|
|
2186
2257
|
/**
|
|
2187
2258
|
* Trim a presented exercise history's session time-series to an inclusive YYYY-MM-DD window.
|
|
@@ -2261,7 +2332,7 @@ function registerProfileTools(server, ctx, whoami, userId) {
|
|
|
2261
2332
|
return jsonResult(await fetchLeaderboard(ctx.client, toId(workoutId), opts));
|
|
2262
2333
|
}));
|
|
2263
2334
|
}
|
|
2264
|
-
const ATHLETE_WORKOUTS_DESC = "Workouts on the AUTHENTICATED user's own athlete calendar in an inclusive YYYY-MM-DD window, flattened to blocks/exercises. When a coach account calls this, it returns the coach's own training schedule — not a roster athlete's. To inspect a roster athlete use athlete_saved_workouts (or athlete_training for a month overview); to verify a coach-published team session from the calendar side use workout_read. Each exercise carries both its `prescribed` sets (what the program called for) and `performed` sets (what the athlete actually logged); each workout has a top-level `logged` flag. Use `performed`/`logged` to tell what was recorded or done. That is the reliable signal: a session can hold logged sets while the API's own completion flags stay 0, and an empty `performed` means nothing was logged for that exercise. For 'did I record anything / what did I do', set loggedOnly:true to return only sessions with logged sets (it keeps whole sessions that have any logged set, so individual exercises inside can still show empty `performed`; it also shrinks a large result); limit returns the most recent N workouts (newest first). For a high-level overview ('what's on my schedule this week', 'what have I been training lately') set summary:true to get one compact row per session (date, program, title, logged flag, and exerciseCount/performedCount — read performedCount against exerciseCount, e.g. 1 of 12 logged) instead of every set — a multi-program week of full detail is large, so prefer summary first, then re-query a single day without it to drill into that day's sets. For 'what's my next workout', query a forward window from today; if it comes back empty, the most recent session is likely yesterday's still-unlogged one, so widen the window backward a day or two. Both filters apply to the presented view, not raw. raw:true returns the untouched API objects. This is a date-windowed fetch, NOT an aggregate: for lifetime totals (all-time session count, total volume, first/last logged date) call athlete_profile instead of summing windows — a multi-year range here can time out. Narrow the window if the result is truncated.";
|
|
2335
|
+
const ATHLETE_WORKOUTS_DESC = "Workouts on the AUTHENTICATED user's own athlete calendar in an inclusive YYYY-MM-DD window, flattened to blocks/exercises. When a coach account calls this, it returns the coach's own training schedule — not a roster athlete's. To inspect a roster athlete use athlete_saved_workouts (or athlete_training for a month overview); to verify a coach-published team session from the calendar side use workout_read. Each exercise carries both its `prescribed` sets (what the program called for) and `performed` sets (what the athlete actually logged); each workout has a top-level `logged` flag. Session `notes` and `rpe` are the athlete's own note/RPE on that workout (null when unset), distinct from coach `instruction`. Use `performed`/`logged` to tell what was recorded or done. That is the reliable signal: a session can hold logged sets while the API's own completion flags stay 0, and an empty `performed` means nothing was logged for that exercise. For 'did I record anything / what did I do', set loggedOnly:true to return only sessions with logged sets (it keeps whole sessions that have any logged set, so individual exercises inside can still show empty `performed`; it also shrinks a large result); limit returns the most recent N workouts (newest first). For a high-level overview ('what's on my schedule this week', 'what have I been training lately') set summary:true to get one compact row per session (date, program, title, logged flag, and exerciseCount/performedCount — read performedCount against exerciseCount, e.g. 1 of 12 logged) instead of every set — a multi-program week of full detail is large, so prefer summary first, then re-query a single day without it to drill into that day's sets. For 'what's my next workout', query a forward window from today; if it comes back empty, the most recent session is likely yesterday's still-unlogged one, so widen the window backward a day or two. Both filters apply to the presented view, not raw. raw:true returns the untouched API objects. This is a date-windowed fetch, NOT an aggregate: for lifetime totals (all-time session count, total volume, first/last logged date) call athlete_profile instead of summing windows — a multi-year range here can time out. Narrow the window if the result is truncated.";
|
|
2265
2336
|
const ATHLETE_LOG_TARGETS_DESC = "The savedWorkoutSetId + savedWorkoutSetExerciseId that athlete_log_set needs, read straight off your scheduled/logged workouts in an inclusive YYYY-MM-DD window — no raw needed. This is the self-service path for logging into a COACH-SCHEDULED workout: read the ids here, then pass them to athlete_log_set. The default view is COMPACT: one row per saved set, each carrying its program/programId, the savedWorkoutSetId, and every exercise's savedWorkoutSetExerciseId with prescribed/performed values. When several workouts fall on the same day (you're on more than one program), narrow to one with program (a case-insensitive title substring, e.g. 'bodybuilding' — no id lookup needed), or programId/teamId if you have the id. raw:true returns the untouched API objects, but that blob is large and can be truncated when several workouts share a date — prefer the program filter + the default view. For reading what you actually did (not the log ids), use athlete_workouts.";
|
|
2266
2337
|
const ATHLETE_EXERCISE_HISTORY_DESC = "Per-exercise PRs and the dated session time-series (sets performed, estimated 1RM). The returned `sessions` run all-time, newest first; pass since/until (YYYY-MM-DD, inclusive) to keep only sessions in that window — use it for 'last 3 months' / 'this year' questions so the result stays small. estimated1RM is a formula off the session's best set and reads high when a session mixes a heavy single with high-rep backdown work, so treat it as approximate, not a logged single. `liftPRs` are always all-time and ignore since/until, but each carries the date it was set, so filter those dates yourself to answer 'PRs set this year'. Set raw:true for the untouched API object. Get the exercise id from athlete_exercises.";
|
|
2267
2338
|
const ATHLETE_EXERCISES_DESC = "The exercises the athlete has logged (id + title + positional units). Call with no arguments to get the athlete's complete catalog, which often runs to several hundred entries; leave limit off when you want the whole list, since a no-q call returns everything by default. Pass q to free-text search by name (returns the top ranked matches, capped by limit, default 20). Use a returned id with athlete_exercise_history, athlete_personal_records, or athlete_exercise_stats (all of which require an exercise id). If a no-q result comes back wrapped with a __truncated marker, you passed a limit smaller than the catalog and are seeing a partial list; re-call without limit for all of it. A common name returns several variants (e.g. plain 'Bench Press' id 1162 vs 'BARBELL BENCH PRESS'); each variant keeps its own separate history and PR board, so prefer the plain canonical entry, and if a PR or trend looks incomplete, the work may be split across variants, so check the others.";
|
|
@@ -2432,6 +2503,29 @@ function registerSessionTools(server, ctx) {
|
|
|
2432
2503
|
});
|
|
2433
2504
|
}));
|
|
2434
2505
|
}
|
|
2506
|
+
/** Set the athlete's session note / RPE on a saved workout (coach-visible). */
|
|
2507
|
+
function registerWorkoutNoteTool(server, ctx) {
|
|
2508
|
+
server.registerTool("athlete_workout_note", {
|
|
2509
|
+
title: "Set your session note or RPE",
|
|
2510
|
+
description: "Athlete-facing write: set the free-text note (and/or session RPE 1–10) on a workout. This is the note box on the workout screen, not the coach's Instructions. programWorkoutId is the `id` from athlete_workouts for that date. Pass notes (empty string clears) and/or rpe; a notes-only write leaves rpe untouched and vice versa. Visible to your coach. Requires confirmation (elicitation or confirm:true).",
|
|
2511
|
+
inputSchema: {
|
|
2512
|
+
...athleteWorkoutNoteObject.shape,
|
|
2513
|
+
confirm: z.boolean().optional()
|
|
2514
|
+
},
|
|
2515
|
+
outputSchema: toolOutputSchema(athleteWorkoutNoteOutputSchema),
|
|
2516
|
+
annotations: DESTRUCTIVE
|
|
2517
|
+
}, ({ date, programWorkoutId, notes, rpe, confirm }, extra) => attempt(async () => {
|
|
2518
|
+
const patch = athleteWorkoutNoteArgsSchema.parse(definedProps({
|
|
2519
|
+
date,
|
|
2520
|
+
programWorkoutId,
|
|
2521
|
+
notes,
|
|
2522
|
+
rpe
|
|
2523
|
+
}));
|
|
2524
|
+
const blocked = confirmGate(extra, `Set the session note on workout ${toId(patch.programWorkoutId)} on ${patch.date}? This is visible to your coach.`, confirm);
|
|
2525
|
+
if (blocked) return blocked;
|
|
2526
|
+
return jsonResult(await setAthleteWorkoutNote(ctx.client, patch));
|
|
2527
|
+
}));
|
|
2528
|
+
}
|
|
2435
2529
|
/** Map validated logSession exercises to the SDK's SessionExercise[] (ids coerced, slots trimmed). */
|
|
2436
2530
|
function mapSessionExercises(exercises) {
|
|
2437
2531
|
return exercises.map((e) => {
|
|
@@ -2583,12 +2677,13 @@ function registerAthleteTrainingTools(server, ctx) {
|
|
|
2583
2677
|
registerCatalogReads(server, ctx);
|
|
2584
2678
|
registerLogTargetsTool(server, ctx);
|
|
2585
2679
|
registerSessionTools(server, ctx);
|
|
2680
|
+
registerWorkoutNoteTool(server, ctx);
|
|
2586
2681
|
registerLogTool(server, ctx);
|
|
2587
2682
|
registerSwapTool(server, ctx);
|
|
2588
2683
|
}
|
|
2589
2684
|
//#endregion
|
|
2590
2685
|
//#region package.json
|
|
2591
|
-
var version = "3.
|
|
2686
|
+
var version = "3.4.0";
|
|
2592
2687
|
//#endregion
|
|
2593
2688
|
//#region src/server.ts
|
|
2594
2689
|
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.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/server": "2.0.0",
|
|
23
23
|
"zod": "^4.4.3",
|
|
24
|
-
"@trainheroic-unofficial/
|
|
25
|
-
"@trainheroic-unofficial/
|
|
24
|
+
"@trainheroic-unofficial/core": "3.4.0",
|
|
25
|
+
"@trainheroic-unofficial/js": "3.4.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^26.
|
|
28
|
+
"@types/node": "^26.3.0",
|
|
29
29
|
"tsdown": "^0.22.14",
|
|
30
30
|
"tsx": "^4.23.12",
|
|
31
31
|
"typescript": "^7.0.2",
|
|
32
|
-
"vitest": "^4.1.
|
|
32
|
+
"vitest": "^4.1.11"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"start": "tsx src/server.ts",
|