@trainheroic-unofficial/athlete-mcp 1.7.4 → 2.0.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 +83 -45
- package/package.json +8 -8
package/dist/server.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/
|
|
2
|
-
import {
|
|
1
|
+
import { McpServer, acceptedContent, inputRequired, inputResponse } from "@modelcontextprotocol/server";
|
|
2
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
//#region ../dto/src/common.ts
|
|
@@ -338,7 +338,11 @@ const DESTRUCTIVE = {
|
|
|
338
338
|
destructiveHint: true,
|
|
339
339
|
openWorldHint: true
|
|
340
340
|
};
|
|
341
|
-
/**
|
|
341
|
+
/**
|
|
342
|
+
* Run a tool body, converting thrown errors into an in-band tool error. Generic so a caller that
|
|
343
|
+
* only ever produces a `CallToolResult` keeps that narrower type instead of widening to the
|
|
344
|
+
* MRTR union — that is what lets `apiCall` delegate here rather than repeat this catch.
|
|
345
|
+
*/
|
|
342
346
|
async function attempt(fn) {
|
|
343
347
|
try {
|
|
344
348
|
return await fn();
|
|
@@ -473,32 +477,50 @@ function errorResult(message) {
|
|
|
473
477
|
}
|
|
474
478
|
//#endregion
|
|
475
479
|
//#region ../core/src/confirm.ts
|
|
476
|
-
const NOT_CONFIRMED = "Not confirmed. Re-run with confirm:true, or connect a client that supports MCP elicitation.";
|
|
477
480
|
/**
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
+
* Returned when the user was asked and said no. It must not tell the model how to retry: the
|
|
482
|
+
* refusal is the answer. (The `confirm: true` fallback for clients that cannot elicit is
|
|
483
|
+
* advertised in each gated tool's description, which is where a model looks before calling.)
|
|
481
484
|
*/
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
485
|
+
const NOT_CONFIRMED = "Not confirmed — the user declined. Nothing was changed.";
|
|
486
|
+
/** Appended to the prompt so the fallback survives whichever way a client surfaces the request. */
|
|
487
|
+
const FALLBACK_HINT = " (If you cannot show this prompt, re-run the tool with confirm:true once the user has agreed.)";
|
|
488
|
+
/**
|
|
489
|
+
* Confirm a destructive/athlete-facing action.
|
|
490
|
+
*
|
|
491
|
+
* Prefers MCP multi-round-trip elicitation (`input_required`); falls back to an explicit
|
|
492
|
+
* `confirm:true` argument when the client already confirmed. Never proceeds without one of the two.
|
|
493
|
+
*
|
|
494
|
+
* Returns `undefined` when the action may proceed, otherwise the result to return to the client
|
|
495
|
+
* (`input_required` or an in-band denial).
|
|
496
|
+
*
|
|
497
|
+
* **Call this before any read or write.** MRTR is re-entrant: the client answers by re-sending
|
|
498
|
+
* the whole `tools/call`, and the stateless handler builds a fresh server and runs the handler
|
|
499
|
+
* from the top. Anything above the gate therefore executes twice on a confirmed action. If a
|
|
500
|
+
* confirmation message ever needs data fetched first, that fetch must be idempotent.
|
|
501
|
+
*
|
|
502
|
+
* Written once in the 2026 `inputRequired` style: modern clients retry with `inputResponses`.
|
|
503
|
+
* A client that cannot elicit at all never reaches the denial below — the SDK rejects the
|
|
504
|
+
* request before the retry — so it must pass `confirm: true`, which is why the hint rides inside
|
|
505
|
+
* the prompt text itself.
|
|
506
|
+
*/
|
|
507
|
+
function confirmGate(ctx, message, confirmArg) {
|
|
508
|
+
if (confirmArg === true) return void 0;
|
|
509
|
+
if (acceptedContent(ctx.mcpReq.inputResponses, "confirm")?.confirm === true) return void 0;
|
|
510
|
+
if (inputResponse(ctx.mcpReq.inputResponses, "confirm").kind !== "missing") return errorResult(NOT_CONFIRMED);
|
|
511
|
+
const prompt = message + FALLBACK_HINT;
|
|
512
|
+
return inputRequired({ inputRequests: { confirm: inputRequired.elicit({
|
|
513
|
+
message: prompt,
|
|
514
|
+
requestedSchema: {
|
|
515
|
+
type: "object",
|
|
516
|
+
properties: { confirm: {
|
|
517
|
+
type: "boolean",
|
|
518
|
+
title: "Confirm",
|
|
519
|
+
description: prompt
|
|
520
|
+
} },
|
|
521
|
+
required: ["confirm"]
|
|
522
|
+
}
|
|
523
|
+
}) } });
|
|
502
524
|
}
|
|
503
525
|
//#endregion
|
|
504
526
|
//#region ../core/src/instructions.ts
|
|
@@ -574,20 +596,27 @@ var TrainHeroicAuthError = class extends Error {
|
|
|
574
596
|
name = "TrainHeroicAuthError";
|
|
575
597
|
};
|
|
576
598
|
/**
|
|
577
|
-
* Authenticated TrainHeroic API client. Holds the
|
|
578
|
-
* encrypted props) and a lazily-acquired session token cached in memory for the life
|
|
579
|
-
*
|
|
599
|
+
* Authenticated TrainHeroic API client. Holds the credentials (on the hosted Worker, from the
|
|
600
|
+
* grant's encrypted props) and a lazily-acquired session token cached in memory for the life of
|
|
601
|
+
* *this client instance* — nothing longer. On a 401/403 it re-logs in once and retries, since
|
|
580
602
|
* TrainHeroic has no refresh token and sessions expire after ~1-2h.
|
|
603
|
+
*
|
|
604
|
+
* Anything that wants a session to outlive one client owns that itself: pass a previously
|
|
605
|
+
* acquired token as `sessionId` and keep the new one via `options.onSession`. That matters
|
|
606
|
+
* wherever clients are short-lived — the hosted Worker builds one per HTTP request, and each CLI
|
|
607
|
+
* invocation is a fresh process — because otherwise every operation replays the password.
|
|
581
608
|
*/
|
|
582
609
|
var TrainHeroicClient = class {
|
|
583
610
|
#email;
|
|
584
611
|
#password;
|
|
612
|
+
#onSession;
|
|
585
613
|
#sessionId;
|
|
586
614
|
#loginInFlight = null;
|
|
587
|
-
constructor(email, password, sessionId = null) {
|
|
615
|
+
constructor(email, password, sessionId = null, options = {}) {
|
|
588
616
|
this.#email = email;
|
|
589
617
|
this.#password = password;
|
|
590
618
|
this.#sessionId = sessionId;
|
|
619
|
+
this.#onSession = options.onSession;
|
|
591
620
|
}
|
|
592
621
|
get sessionId() {
|
|
593
622
|
return this.#sessionId;
|
|
@@ -605,6 +634,9 @@ var TrainHeroicClient = class {
|
|
|
605
634
|
const session = await loginTrainHeroic(this.#email, this.#password);
|
|
606
635
|
if (!session) throw new TrainHeroicAuthError("TrainHeroic login failed");
|
|
607
636
|
this.#sessionId = session.sessionId;
|
|
637
|
+
try {
|
|
638
|
+
this.#onSession?.(this.#sessionId);
|
|
639
|
+
} catch {}
|
|
608
640
|
return this.#sessionId;
|
|
609
641
|
}
|
|
610
642
|
async request(method, path, options = {}) {
|
|
@@ -1942,7 +1974,8 @@ function registerSessionTools(server, ctx) {
|
|
|
1942
1974
|
annotations: DESTRUCTIVE
|
|
1943
1975
|
}, ({ programWorkoutId, date, confirm }, extra) => attempt(async () => {
|
|
1944
1976
|
const id = toId(programWorkoutId);
|
|
1945
|
-
|
|
1977
|
+
const blocked = confirmGate(extra, `Permanently remove personal session ${id} on ${date}? This deletes the session and anything logged in it.`, confirm);
|
|
1978
|
+
if (blocked) return blocked;
|
|
1946
1979
|
await removePersonalWorkout(ctx.client, {
|
|
1947
1980
|
programWorkoutId: id,
|
|
1948
1981
|
date
|
|
@@ -1982,7 +2015,8 @@ function registerLogTool(server, ctx) {
|
|
|
1982
2015
|
},
|
|
1983
2016
|
annotations: DESTRUCTIVE
|
|
1984
2017
|
}, ({ date, exercises, confirm }, extra) => attempt(async () => {
|
|
1985
|
-
|
|
2018
|
+
const blocked = confirmGate(extra, `Log a session of ${exercises.length} exercise(s) on ${date}? This writes to your coach-visible training log.`, confirm);
|
|
2019
|
+
if (blocked) return blocked;
|
|
1986
2020
|
const res = await logAdHocSession(ctx.client, {
|
|
1987
2021
|
date,
|
|
1988
2022
|
exercises: mapSessionExercises(exercises)
|
|
@@ -1999,7 +2033,8 @@ function registerLogTool(server, ctx) {
|
|
|
1999
2033
|
},
|
|
2000
2034
|
annotations: DESTRUCTIVE
|
|
2001
2035
|
}, ({ date, savedWorkoutSetId, results, confirm }, extra) => attempt(async () => {
|
|
2002
|
-
|
|
2036
|
+
const blocked = confirmGate(extra, `Log results to saved workout set ${toId(savedWorkoutSetId)} on ${date}? This writes to your coach-visible training log.`, confirm);
|
|
2037
|
+
if (blocked) return blocked;
|
|
2003
2038
|
return jsonResult(await logAthleteSet(ctx.client, {
|
|
2004
2039
|
date,
|
|
2005
2040
|
savedWorkoutSetId: toId(savedWorkoutSetId),
|
|
@@ -2025,7 +2060,8 @@ function registerSwapTool(server, ctx) {
|
|
|
2025
2060
|
}, ({ savedWorkoutSetExerciseId, exerciseId, confirm }, extra) => attempt(async () => {
|
|
2026
2061
|
const sweId = toId(savedWorkoutSetExerciseId);
|
|
2027
2062
|
const exId = toId(exerciseId);
|
|
2028
|
-
|
|
2063
|
+
const blocked = confirmGate(extra, `Swap the exercise in your saved workout slot ${sweId} to exercise ${exId}? This changes what that slot is prescribed (your copy only).`, confirm);
|
|
2064
|
+
if (blocked) return blocked;
|
|
2029
2065
|
return jsonResult(await swapAthleteExercise(ctx.client, {
|
|
2030
2066
|
savedWorkoutSetExerciseId: sweId,
|
|
2031
2067
|
exerciseId: exId
|
|
@@ -2061,10 +2097,10 @@ function registerAthleteTrainingTools(server, ctx) {
|
|
|
2061
2097
|
}
|
|
2062
2098
|
//#endregion
|
|
2063
2099
|
//#region package.json
|
|
2064
|
-
var version = "
|
|
2100
|
+
var version = "2.0.0";
|
|
2065
2101
|
//#endregion
|
|
2066
2102
|
//#region src/server.ts
|
|
2067
|
-
|
|
2103
|
+
function main() {
|
|
2068
2104
|
const email = process.env.TRAINHEROIC_EMAIL;
|
|
2069
2105
|
const password = process.env.TRAINHEROIC_PASSWORD;
|
|
2070
2106
|
if (!email || !password) {
|
|
@@ -2072,13 +2108,15 @@ async function main() {
|
|
|
2072
2108
|
process.exit(1);
|
|
2073
2109
|
}
|
|
2074
2110
|
const client = new TrainHeroicClient(email, password);
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2111
|
+
serveStdio(() => {
|
|
2112
|
+
const server = new McpServer({
|
|
2113
|
+
name: "trainheroic-athlete",
|
|
2114
|
+
version
|
|
2115
|
+
}, { instructions: SERVER_INSTRUCTIONS });
|
|
2116
|
+
registerAthleteTrainingTools(server, { client });
|
|
2117
|
+
return server;
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
main();
|
|
2083
2121
|
//#endregion
|
|
2084
2122
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trainheroic-unofficial/athlete-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,16 +19,16 @@
|
|
|
19
19
|
"dist"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@modelcontextprotocol/
|
|
22
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
23
23
|
"zod": "^4.4.3",
|
|
24
|
-
"@trainheroic-unofficial/core": "
|
|
25
|
-
"@trainheroic-unofficial/js": "
|
|
24
|
+
"@trainheroic-unofficial/core": "2.0.0",
|
|
25
|
+
"@trainheroic-unofficial/js": "2.0.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^26.1.
|
|
29
|
-
"tsdown": "^0.22.
|
|
30
|
-
"tsx": "^4.23.
|
|
31
|
-
"typescript": "^
|
|
28
|
+
"@types/node": "^26.1.2",
|
|
29
|
+
"tsdown": "^0.22.14",
|
|
30
|
+
"tsx": "^4.23.4",
|
|
31
|
+
"typescript": "^7.0.2",
|
|
32
32
|
"vitest": "^4.1.10"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|