@trainheroic-unofficial/athlete-mcp 3.6.0 → 3.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.mjs +202 -124
- package/package.json +7 -7
package/dist/server.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { McpServer, acceptedContent, inputRequired, inputResponse } from "@modelcontextprotocol/server";
|
|
1
|
+
import { CLIENT_CAPABILITIES_META_KEY, McpServer, acceptedContent, inputRequired, inputResponse } from "@modelcontextprotocol/server";
|
|
2
2
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -441,10 +441,35 @@ z.object({
|
|
|
441
441
|
match: exerciseViewSchema.nullable(),
|
|
442
442
|
candidates: z.array(exerciseViewSchema)
|
|
443
443
|
});
|
|
444
|
+
/** TrainHeroic's documented exercise parameter-type codes. */
|
|
445
|
+
const exerciseParamTypeSchema = z.union([
|
|
446
|
+
z.literal(0),
|
|
447
|
+
z.literal(1),
|
|
448
|
+
z.literal(2),
|
|
449
|
+
z.literal(3),
|
|
450
|
+
z.literal(4),
|
|
451
|
+
z.literal(5),
|
|
452
|
+
z.literal(6),
|
|
453
|
+
z.literal(7),
|
|
454
|
+
z.literal(10),
|
|
455
|
+
z.literal(11),
|
|
456
|
+
z.literal(12),
|
|
457
|
+
z.literal(13),
|
|
458
|
+
z.literal(14),
|
|
459
|
+
z.literal(18)
|
|
460
|
+
]);
|
|
461
|
+
const exerciseWriteShape = {
|
|
462
|
+
title: z.string().trim().min(1),
|
|
463
|
+
param_1_type: exerciseParamTypeSchema.optional(),
|
|
464
|
+
param_2_type: exerciseParamTypeSchema.optional()
|
|
465
|
+
};
|
|
444
466
|
z.looseObject({
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
467
|
+
...exerciseWriteShape,
|
|
468
|
+
points_of_performance: z.string().default("")
|
|
469
|
+
});
|
|
470
|
+
z.looseObject({
|
|
471
|
+
...exerciseWriteShape,
|
|
472
|
+
points_of_performance: z.string().optional()
|
|
448
473
|
});
|
|
449
474
|
z.object({
|
|
450
475
|
streamId: idSchema,
|
|
@@ -951,6 +976,15 @@ function errorResult(message) {
|
|
|
951
976
|
const NOT_CONFIRMED = "Not confirmed — the user declined. Nothing was changed.";
|
|
952
977
|
/** Appended to the prompt so the fallback survives whichever way a client surfaces the request. */
|
|
953
978
|
const FALLBACK_HINT = " (If you cannot show this prompt, re-run the tool with confirm:true once the user has agreed.)";
|
|
979
|
+
const CONFIRMATION_REQUIRED = "Ask the user to approve this action, then retry the tool with confirm:true only after they agree. Nothing was changed.";
|
|
980
|
+
/** `undefined` means a legacy request, whose capability check belongs to the SDK's shim. */
|
|
981
|
+
function modernClientSupportsFormElicitation(ctx) {
|
|
982
|
+
const envelope = ctx.mcpReq.envelope;
|
|
983
|
+
if (envelope === void 0) return void 0;
|
|
984
|
+
const elicitation = envelope[CLIENT_CAPABILITIES_META_KEY]?.elicitation;
|
|
985
|
+
if (elicitation === void 0) return false;
|
|
986
|
+
return elicitation.form !== void 0 || elicitation.url === void 0;
|
|
987
|
+
}
|
|
954
988
|
/**
|
|
955
989
|
* Confirm a destructive/athlete-facing action.
|
|
956
990
|
*
|
|
@@ -966,14 +1000,15 @@ const FALLBACK_HINT = " (If you cannot show this prompt, re-run the tool with co
|
|
|
966
1000
|
* confirmation message ever needs data fetched first, that fetch must be idempotent.
|
|
967
1001
|
*
|
|
968
1002
|
* Written once in the 2026 `inputRequired` style: modern clients retry with `inputResponses`.
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
* the
|
|
1003
|
+
* MCP correctly rejects unsupported embedded requests with `-32021`. This application has an
|
|
1004
|
+
* alternate `confirm: true` flow, so a modern client that does not declare form elicitation gets
|
|
1005
|
+
* a model-readable tool error that tells it how to recover while the gate remains closed.
|
|
972
1006
|
*/
|
|
973
1007
|
function confirmGate(ctx, message, confirmArg) {
|
|
974
1008
|
if (confirmArg === true) return void 0;
|
|
975
1009
|
if (acceptedContent(ctx.mcpReq.inputResponses, "confirm")?.confirm === true) return void 0;
|
|
976
1010
|
if (inputResponse(ctx.mcpReq.inputResponses, "confirm").kind !== "missing") return errorResult(NOT_CONFIRMED);
|
|
1011
|
+
if (modernClientSupportsFormElicitation(ctx) === false) return errorResult(`${message} ${CONFIRMATION_REQUIRED}`);
|
|
977
1012
|
const prompt = message + FALLBACK_HINT;
|
|
978
1013
|
return inputRequired({ inputRequests: { confirm: inputRequired.elicit({
|
|
979
1014
|
message: prompt,
|
|
@@ -1000,12 +1035,29 @@ function confirmGate(ctx, message, confirmArg) {
|
|
|
1000
1035
|
*/
|
|
1001
1036
|
const SERVER_INSTRUCTIONS = "Speak to the user in plain, everyday language about their training. Describe what you are doing in the TrainHeroic app's own terms (for example, say you are creating a workout rather than naming a tool). Do not surface internal tool names (the snake_case identifiers such as athlete_session_create), raw parameter names, or numeric ids in your replies unless the user explicitly asks for them; they are implementation details. The tool descriptions cross-reference each other by name only so you can chain them correctly. Keep that wiring to yourself.";
|
|
1002
1037
|
//#endregion
|
|
1038
|
+
//#region ../js/src/date-window.ts
|
|
1039
|
+
const DAY_MS = 864e5;
|
|
1040
|
+
const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/u;
|
|
1041
|
+
function dayNumber(value) {
|
|
1042
|
+
if (!ISO_DAY.test(value)) return null;
|
|
1043
|
+
const timestamp = Date.parse(`${value}T00:00:00Z`);
|
|
1044
|
+
if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString().slice(0, 10) !== value) return null;
|
|
1045
|
+
return timestamp / DAY_MS;
|
|
1046
|
+
}
|
|
1047
|
+
/** Number of calendar days in an inclusive ISO-date range, or null for an invalid range. */
|
|
1048
|
+
function dateSpanDays(start, end) {
|
|
1049
|
+
const first = dayNumber(start);
|
|
1050
|
+
const last = dayNumber(end);
|
|
1051
|
+
return first === null || last === null || first > last ? null : last - first + 1;
|
|
1052
|
+
}
|
|
1053
|
+
//#endregion
|
|
1003
1054
|
//#region ../js/src/http-error.ts
|
|
1004
1055
|
const MAX_REQUEST_KEYS = 50;
|
|
1005
1056
|
const MAX_RESPONSE_DEPTH = 4;
|
|
1006
1057
|
const MAX_RESPONSE_KEYS = 20;
|
|
1007
1058
|
const MAX_RESPONSE_ITEMS = 10;
|
|
1008
1059
|
const MAX_RESPONSE_NODES = 50;
|
|
1060
|
+
const MAX_RESPONSE_STRING = 2e3;
|
|
1009
1061
|
const RESPONSE_DIAGNOSTIC_KEYS = /* @__PURE__ */ new Set([
|
|
1010
1062
|
"code",
|
|
1011
1063
|
"detail",
|
|
@@ -1043,6 +1095,11 @@ const PARAMETER_TYPES = /* @__PURE__ */ new Set([
|
|
|
1043
1095
|
14,
|
|
1044
1096
|
18
|
|
1045
1097
|
]);
|
|
1098
|
+
/** Keep provider diagnostics useful while removing common credentials and direct identifiers. */
|
|
1099
|
+
function redactText(value) {
|
|
1100
|
+
const redacted = value.slice(0, MAX_RESPONSE_STRING).replace(/\bAuthorization(["']?\s*[:=]\s*)(?:"(?:\\(?:[\s\S]|$)|[^"\\])*(?:"|$)|'(?:\\(?:[\s\S]|$)|[^'\\])*(?:'|$)|[^"'\r\n,;&]+)/giu, (_match, separator) => `Authorization${separator}[Redacted]`).replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/giu, "Bearer [Redacted]").replace(/\b(password|passwd|secret|token|access[_-]?token|refresh[_-]?token|session(?:[_-]?(?:id|token))?|api[_-]?key|client[_-]?secret)(["']?\s*[:=]\s*)(?:"(?:\\(?:[\s\S]|$)|[^"\\])*(?:"|$)|'(?:\\(?:[\s\S]|$)|[^'\\])*(?:'|$)|[^"'\r\n,;&}\]]+)/giu, (_match, key, separator) => `${key}${separator}[Redacted]`).replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu, "[Redacted email]").replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/gu, "[Redacted IP]").replace(/\b\d{3}-\d{2}-\d{4}\b/gu, "[Redacted]").replace(/\b(?:bearer\s+)?[a-f0-9]{32,}\b/giu, "[Redacted]");
|
|
1101
|
+
return value.length <= MAX_RESPONSE_STRING ? redacted : `${redacted}…[truncated]`;
|
|
1102
|
+
}
|
|
1046
1103
|
function safeFieldName(key) {
|
|
1047
1104
|
if (key === "__proto__" || key === "constructor" || key === "prototype") return "[Redacted key]";
|
|
1048
1105
|
if (!/^[A-Za-z_][A-Za-z0-9_.-]{0,79}$/.test(key)) return "[Redacted key]";
|
|
@@ -1070,17 +1127,22 @@ function requestBodySummary(body) {
|
|
|
1070
1127
|
const entries = boundedEntries(body, MAX_REQUEST_KEYS).sort(([left], [right]) => left.localeCompare(right));
|
|
1071
1128
|
const keys = entries.map(([key]) => safeFieldName(key));
|
|
1072
1129
|
const values = {};
|
|
1130
|
+
const arrayLengths = {};
|
|
1073
1131
|
for (const [key, value] of entries) {
|
|
1074
1132
|
const safeValue = safeRequestValue(key, value);
|
|
1075
1133
|
if (safeValue !== void 0) values[safeFieldName(key)] = safeValue;
|
|
1134
|
+
if (Array.isArray(value)) arrayLengths[safeFieldName(key)] = value.length;
|
|
1076
1135
|
}
|
|
1077
|
-
|
|
1136
|
+
const byKey = new Map(entries);
|
|
1137
|
+
const dateStart = byKey.get("date_start");
|
|
1138
|
+
const dateEnd = byKey.get("date_end");
|
|
1139
|
+
const span = typeof dateStart === "string" && typeof dateEnd === "string" ? dateSpanDays(dateStart, dateEnd) : null;
|
|
1140
|
+
return {
|
|
1078
1141
|
type: "object",
|
|
1079
1142
|
keys,
|
|
1080
|
-
values
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
keys
|
|
1143
|
+
...Object.keys(values).length > 0 ? { values } : {},
|
|
1144
|
+
...Object.keys(arrayLengths).length > 0 ? { arrayLengths } : {},
|
|
1145
|
+
...span === null ? {} : { dateSpanDays: span }
|
|
1084
1146
|
};
|
|
1085
1147
|
}
|
|
1086
1148
|
const primitive = typeof body;
|
|
@@ -1092,7 +1154,7 @@ function sanitizedDiagnosticValue(value, depth, budget, field) {
|
|
|
1092
1154
|
if (value === null) return null;
|
|
1093
1155
|
if (typeof value === "boolean") return field && RESPONSE_BOOLEAN_KEYS.has(field) ? value : "[Redacted]";
|
|
1094
1156
|
if (typeof value === "number") return field && RESPONSE_STATUS_KEYS.has(field) && Number.isInteger(value) && value >= 100 && value <= 599 ? value : "[Redacted]";
|
|
1095
|
-
if (typeof value === "string") return
|
|
1157
|
+
if (typeof value === "string") return redactText(value);
|
|
1096
1158
|
if (depth >= MAX_RESPONSE_DEPTH) return "[Truncated]";
|
|
1097
1159
|
if (Array.isArray(value)) {
|
|
1098
1160
|
const result = [];
|
|
@@ -1198,6 +1260,9 @@ function notifyHttpError(handler, method, url, status, diagnostics = {}) {
|
|
|
1198
1260
|
} catch {}
|
|
1199
1261
|
}
|
|
1200
1262
|
//#endregion
|
|
1263
|
+
//#region ../js/src/transport.ts
|
|
1264
|
+
const defaultTrainHeroicTransport = (url, init) => fetch(url, init);
|
|
1265
|
+
//#endregion
|
|
1201
1266
|
//#region ../js/src/auth.ts
|
|
1202
1267
|
const DEFAULT_AUTH_URL = "https://apis.trainheroic.com/auth";
|
|
1203
1268
|
/**
|
|
@@ -1221,7 +1286,7 @@ function authUrl() {
|
|
|
1221
1286
|
*/
|
|
1222
1287
|
async function loginTrainHeroic(email, password, options = {}) {
|
|
1223
1288
|
const url = authUrl();
|
|
1224
|
-
const res = await
|
|
1289
|
+
const res = await (options.transport ?? defaultTrainHeroicTransport)(url, {
|
|
1225
1290
|
method: "POST",
|
|
1226
1291
|
headers: {
|
|
1227
1292
|
"content-type": "application/x-www-form-urlencoded",
|
|
@@ -1233,6 +1298,7 @@ async function loginTrainHeroic(email, password, options = {}) {
|
|
|
1233
1298
|
}).toString()
|
|
1234
1299
|
});
|
|
1235
1300
|
if (!res.ok) {
|
|
1301
|
+
await res.body?.cancel().catch(() => {});
|
|
1236
1302
|
notifyHttpError(options.onHttpError, "POST", url, res.status);
|
|
1237
1303
|
return null;
|
|
1238
1304
|
}
|
|
@@ -1246,115 +1312,6 @@ async function loginTrainHeroic(email, password, options = {}) {
|
|
|
1246
1312
|
};
|
|
1247
1313
|
}
|
|
1248
1314
|
//#endregion
|
|
1249
|
-
//#region ../js/src/client.ts
|
|
1250
|
-
const DEFAULT_COACH_BASE = "https://api.trainheroic.com";
|
|
1251
|
-
const DEFAULT_APIS_BASE = "https://apis.trainheroic.com";
|
|
1252
|
-
/**
|
|
1253
|
-
* Resolve an API host, allowing an env override. The override exists so a test harness can point
|
|
1254
|
-
* the client at a local fake backend (and it doubles as a staging knob); production leaves these
|
|
1255
|
-
* unset and gets the real hosts. Read through `globalThis.process?.env` — not an `import process`
|
|
1256
|
-
* — so the runtime-agnostic `.` entry stays free of `node:*` and runs unchanged on workerd, and
|
|
1257
|
-
* read per request (not at module load) so a value the harness sets in the child env always wins.
|
|
1258
|
-
*/
|
|
1259
|
-
function envBase(key, fallback) {
|
|
1260
|
-
const v = (globalThis.process?.env)?.[key];
|
|
1261
|
-
return v && v.length > 0 ? v : fallback;
|
|
1262
|
-
}
|
|
1263
|
-
var TrainHeroicAuthError = class extends Error {
|
|
1264
|
-
name = "TrainHeroicAuthError";
|
|
1265
|
-
};
|
|
1266
|
-
/**
|
|
1267
|
-
* Authenticated TrainHeroic API client. Holds the credentials (on the hosted Worker, from the
|
|
1268
|
-
* grant's encrypted props) and a lazily-acquired session token cached in memory for the life of
|
|
1269
|
-
* *this client instance* — nothing longer. On a 401/403 it re-logs in once and retries, since
|
|
1270
|
-
* TrainHeroic has no refresh token and sessions expire after ~1-2h.
|
|
1271
|
-
*
|
|
1272
|
-
* Anything that wants a session to outlive one client owns that itself: pass a previously
|
|
1273
|
-
* acquired token as `sessionId` and keep the new one via `options.onSession`. That matters
|
|
1274
|
-
* wherever clients are short-lived — the hosted Worker builds one per HTTP request, and each CLI
|
|
1275
|
-
* invocation is a fresh process — because otherwise every operation replays the password.
|
|
1276
|
-
*/
|
|
1277
|
-
var TrainHeroicClient = class {
|
|
1278
|
-
#email;
|
|
1279
|
-
#password;
|
|
1280
|
-
#onSession;
|
|
1281
|
-
#onHttpError;
|
|
1282
|
-
#sessionId;
|
|
1283
|
-
#loginInFlight = null;
|
|
1284
|
-
constructor(email, password, sessionId = null, options = {}) {
|
|
1285
|
-
this.#email = email;
|
|
1286
|
-
this.#password = password;
|
|
1287
|
-
this.#sessionId = sessionId;
|
|
1288
|
-
this.#onSession = options.onSession;
|
|
1289
|
-
this.#onHttpError = options.onHttpError;
|
|
1290
|
-
}
|
|
1291
|
-
get sessionId() {
|
|
1292
|
-
return this.#sessionId;
|
|
1293
|
-
}
|
|
1294
|
-
async #ensureSession() {
|
|
1295
|
-
if (this.#sessionId) return this.#sessionId;
|
|
1296
|
-
this.#loginInFlight ??= this.#login();
|
|
1297
|
-
try {
|
|
1298
|
-
return await this.#loginInFlight;
|
|
1299
|
-
} finally {
|
|
1300
|
-
this.#loginInFlight = null;
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
async #login() {
|
|
1304
|
-
const session = await loginTrainHeroic(this.#email, this.#password, this.#onHttpError ? { onHttpError: this.#onHttpError } : {});
|
|
1305
|
-
if (!session) throw new TrainHeroicAuthError("TrainHeroic login failed");
|
|
1306
|
-
this.#sessionId = session.sessionId;
|
|
1307
|
-
try {
|
|
1308
|
-
this.#onSession?.(this.#sessionId);
|
|
1309
|
-
} catch {}
|
|
1310
|
-
return this.#sessionId;
|
|
1311
|
-
}
|
|
1312
|
-
async request(method, path, options = {}) {
|
|
1313
|
-
const url = `${options.base === "apis" ? envBase("TH_APIS_BASE", DEFAULT_APIS_BASE) : envBase("TH_COACH_BASE", DEFAULT_COACH_BASE)}/${path.replace(/^\//, "")}`;
|
|
1314
|
-
let session = await this.#ensureSession();
|
|
1315
|
-
let res = await this.#send(method, url, session, options.body);
|
|
1316
|
-
if (res.status === 401 || res.status === 403) {
|
|
1317
|
-
if (this.#sessionId === session) this.#sessionId = null;
|
|
1318
|
-
session = await this.#ensureSession();
|
|
1319
|
-
res = await this.#send(method, url, session, options.body);
|
|
1320
|
-
}
|
|
1321
|
-
const shouldReport = !res.ok && !options.expectedStatuses?.includes(res.status);
|
|
1322
|
-
let text;
|
|
1323
|
-
try {
|
|
1324
|
-
text = await res.text();
|
|
1325
|
-
} catch (error) {
|
|
1326
|
-
if (shouldReport) notifyHttpError(this.#onHttpError, method, url, res.status, { requestBody: options.body });
|
|
1327
|
-
throw error;
|
|
1328
|
-
}
|
|
1329
|
-
const data = parseResponseText(text);
|
|
1330
|
-
if (shouldReport) notifyHttpError(this.#onHttpError, method, url, res.status, {
|
|
1331
|
-
requestBody: options.body,
|
|
1332
|
-
responseBody: data
|
|
1333
|
-
});
|
|
1334
|
-
return {
|
|
1335
|
-
status: res.status,
|
|
1336
|
-
ok: res.ok,
|
|
1337
|
-
data
|
|
1338
|
-
};
|
|
1339
|
-
}
|
|
1340
|
-
#send(method, url, session, body) {
|
|
1341
|
-
const upper = method.toUpperCase();
|
|
1342
|
-
const headers = {
|
|
1343
|
-
accept: "application/json",
|
|
1344
|
-
"session-token": session
|
|
1345
|
-
};
|
|
1346
|
-
const init = {
|
|
1347
|
-
method: upper,
|
|
1348
|
-
headers
|
|
1349
|
-
};
|
|
1350
|
-
if (body !== void 0 && upper !== "GET" && upper !== "DELETE") {
|
|
1351
|
-
headers["content-type"] = "application/json";
|
|
1352
|
-
init.body = JSON.stringify(body);
|
|
1353
|
-
}
|
|
1354
|
-
return fetch(url, init);
|
|
1355
|
-
}
|
|
1356
|
-
};
|
|
1357
|
-
//#endregion
|
|
1358
1315
|
//#region ../js/src/exercise-util.ts
|
|
1359
1316
|
/**
|
|
1360
1317
|
* Display labels for TrainHeroic parameter types. The unit is FIXED PER EXERCISE
|
|
@@ -1534,6 +1491,127 @@ function assertPositiveInteger(value, label) {
|
|
|
1534
1491
|
if (!Number.isInteger(value) || value < 1) throw new RangeError(`${label} must be a positive integer; received ${value}.`);
|
|
1535
1492
|
}
|
|
1536
1493
|
//#endregion
|
|
1494
|
+
//#region ../js/src/client.ts
|
|
1495
|
+
const DEFAULT_COACH_BASE = "https://api.trainheroic.com";
|
|
1496
|
+
const DEFAULT_APIS_BASE = "https://apis.trainheroic.com";
|
|
1497
|
+
const MAX_REQUEST_CONCURRENCY = 4;
|
|
1498
|
+
/**
|
|
1499
|
+
* Resolve an API host, allowing an env override. The override exists so a test harness can point
|
|
1500
|
+
* the client at a local fake backend (and it doubles as a staging knob); production leaves these
|
|
1501
|
+
* unset and gets the real hosts. Read through `globalThis.process?.env` — not an `import process`
|
|
1502
|
+
* — so the runtime-agnostic `.` entry stays free of `node:*` and runs unchanged on workerd, and
|
|
1503
|
+
* read per request (not at module load) so a value the harness sets in the child env always wins.
|
|
1504
|
+
*/
|
|
1505
|
+
function envBase(key, fallback) {
|
|
1506
|
+
const v = (globalThis.process?.env)?.[key];
|
|
1507
|
+
return v && v.length > 0 ? v : fallback;
|
|
1508
|
+
}
|
|
1509
|
+
var TrainHeroicAuthError = class extends Error {
|
|
1510
|
+
name = "TrainHeroicAuthError";
|
|
1511
|
+
};
|
|
1512
|
+
/**
|
|
1513
|
+
* Authenticated TrainHeroic API client. Holds the credentials (on the hosted Worker, from the
|
|
1514
|
+
* grant's encrypted props) and a lazily-acquired session token cached in memory for the life of
|
|
1515
|
+
* *this client instance* — nothing longer. On a 401/403 it re-logs in once and retries, since
|
|
1516
|
+
* TrainHeroic has no refresh token and sessions expire after ~1-2h.
|
|
1517
|
+
*
|
|
1518
|
+
* Anything that wants a session to outlive one client owns that itself: pass a previously
|
|
1519
|
+
* acquired token as `sessionId` and keep the new one via `options.onSession`. That matters
|
|
1520
|
+
* wherever clients are short-lived — the hosted Worker builds one per HTTP request, and each CLI
|
|
1521
|
+
* invocation is a fresh process — because otherwise every operation replays the password.
|
|
1522
|
+
*/
|
|
1523
|
+
var TrainHeroicClient = class {
|
|
1524
|
+
#email;
|
|
1525
|
+
#password;
|
|
1526
|
+
#onSession;
|
|
1527
|
+
#onHttpError;
|
|
1528
|
+
#transport;
|
|
1529
|
+
#requestLimit = createLimiter(MAX_REQUEST_CONCURRENCY);
|
|
1530
|
+
#sessionId;
|
|
1531
|
+
#loginInFlight = null;
|
|
1532
|
+
constructor(email, password, sessionId = null, options = {}) {
|
|
1533
|
+
this.#email = email;
|
|
1534
|
+
this.#password = password;
|
|
1535
|
+
this.#sessionId = sessionId;
|
|
1536
|
+
this.#onSession = options.onSession;
|
|
1537
|
+
this.#onHttpError = options.onHttpError;
|
|
1538
|
+
this.#transport = options.transport ?? defaultTrainHeroicTransport;
|
|
1539
|
+
}
|
|
1540
|
+
get sessionId() {
|
|
1541
|
+
return this.#sessionId;
|
|
1542
|
+
}
|
|
1543
|
+
async #ensureSession() {
|
|
1544
|
+
if (this.#sessionId) return this.#sessionId;
|
|
1545
|
+
this.#loginInFlight ??= this.#login();
|
|
1546
|
+
try {
|
|
1547
|
+
return await this.#loginInFlight;
|
|
1548
|
+
} finally {
|
|
1549
|
+
this.#loginInFlight = null;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
async #login() {
|
|
1553
|
+
const session = await loginTrainHeroic(this.#email, this.#password, {
|
|
1554
|
+
...this.#onHttpError ? { onHttpError: this.#onHttpError } : {},
|
|
1555
|
+
transport: this.#transport
|
|
1556
|
+
});
|
|
1557
|
+
if (!session) throw new TrainHeroicAuthError("TrainHeroic login failed");
|
|
1558
|
+
this.#sessionId = session.sessionId;
|
|
1559
|
+
try {
|
|
1560
|
+
this.#onSession?.(this.#sessionId);
|
|
1561
|
+
} catch {}
|
|
1562
|
+
return this.#sessionId;
|
|
1563
|
+
}
|
|
1564
|
+
async request(method, path, options = {}) {
|
|
1565
|
+
await this.#ensureSession();
|
|
1566
|
+
return this.#requestLimit.run(() => this.#request(method, path, options));
|
|
1567
|
+
}
|
|
1568
|
+
async #request(method, path, options) {
|
|
1569
|
+
const url = `${options.base === "apis" ? envBase("TH_APIS_BASE", DEFAULT_APIS_BASE) : envBase("TH_COACH_BASE", DEFAULT_COACH_BASE)}/${path.replace(/^\//, "")}`;
|
|
1570
|
+
let session = await this.#ensureSession();
|
|
1571
|
+
let res = await this.#send(method, url, session, options.body);
|
|
1572
|
+
if (res.status === 401 || res.status === 403) {
|
|
1573
|
+
await res.body?.cancel().catch(() => {});
|
|
1574
|
+
if (this.#sessionId === session) this.#sessionId = null;
|
|
1575
|
+
session = await this.#ensureSession();
|
|
1576
|
+
res = await this.#send(method, url, session, options.body);
|
|
1577
|
+
}
|
|
1578
|
+
const shouldReport = !res.ok && !options.expectedStatuses?.includes(res.status);
|
|
1579
|
+
let text;
|
|
1580
|
+
try {
|
|
1581
|
+
text = await res.text();
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
if (shouldReport) notifyHttpError(this.#onHttpError, method, url, res.status, { requestBody: options.body });
|
|
1584
|
+
throw error;
|
|
1585
|
+
}
|
|
1586
|
+
const data = parseResponseText(text);
|
|
1587
|
+
if (shouldReport) notifyHttpError(this.#onHttpError, method, url, res.status, {
|
|
1588
|
+
requestBody: options.body,
|
|
1589
|
+
responseBody: data
|
|
1590
|
+
});
|
|
1591
|
+
return {
|
|
1592
|
+
status: res.status,
|
|
1593
|
+
ok: res.ok,
|
|
1594
|
+
data
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
#send(method, url, session, body) {
|
|
1598
|
+
const upper = method.toUpperCase();
|
|
1599
|
+
const headers = {
|
|
1600
|
+
accept: "application/json",
|
|
1601
|
+
"session-token": session
|
|
1602
|
+
};
|
|
1603
|
+
const init = {
|
|
1604
|
+
method: upper,
|
|
1605
|
+
headers
|
|
1606
|
+
};
|
|
1607
|
+
if (body !== void 0 && upper !== "GET" && upper !== "DELETE") {
|
|
1608
|
+
headers["content-type"] = "application/json";
|
|
1609
|
+
init.body = JSON.stringify(body);
|
|
1610
|
+
}
|
|
1611
|
+
return this.#transport(url, init);
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
//#endregion
|
|
1537
1615
|
//#region ../js/src/athlete.ts
|
|
1538
1616
|
async function getJson(client, path, label) {
|
|
1539
1617
|
const res = await client.request("GET", path);
|
|
@@ -3135,7 +3213,7 @@ function registerAthleteTrainingTools(server, ctx) {
|
|
|
3135
3213
|
}
|
|
3136
3214
|
//#endregion
|
|
3137
3215
|
//#region package.json
|
|
3138
|
-
var version = "3.
|
|
3216
|
+
var version = "3.7.2";
|
|
3139
3217
|
//#endregion
|
|
3140
3218
|
//#region src/server.ts
|
|
3141
3219
|
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.7.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,14 +20,14 @@
|
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/server": "2.0.0",
|
|
23
|
-
"zod": "^4.
|
|
24
|
-
"@trainheroic-unofficial/core": "3.
|
|
25
|
-
"@trainheroic-unofficial/js": "3.
|
|
23
|
+
"zod": "^4.6.3",
|
|
24
|
+
"@trainheroic-unofficial/core": "3.7.2",
|
|
25
|
+
"@trainheroic-unofficial/js": "3.7.2"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^26.
|
|
29
|
-
"tsdown": "^0.
|
|
30
|
-
"tsx": "^4.23.
|
|
28
|
+
"@types/node": "^26.5.1",
|
|
29
|
+
"tsdown": "^0.23.0",
|
|
30
|
+
"tsx": "^4.23.13",
|
|
31
31
|
"typescript": "^7.0.2",
|
|
32
32
|
"vitest": "^4.1.11"
|
|
33
33
|
},
|