hevy-mcp 1.26.0 → 1.26.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/README.md +68 -16
- package/dist/cli.mjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{src-Di7keTQA.mjs → src-CuOZcBEo.mjs} +567 -206
- package/dist/src-CuOZcBEo.mjs.map +1 -0
- package/package.json +2 -1
- package/dist/src-Di7keTQA.mjs.map +0 -1
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
(function() {
|
|
5
5
|
try {
|
|
6
6
|
var e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {};
|
|
7
|
-
e.SENTRY_RELEASE = { id: "hevy-mcp@1.26.
|
|
7
|
+
e.SENTRY_RELEASE = { id: "hevy-mcp@1.26.2" };
|
|
8
8
|
var n = new e.Error().stack;
|
|
9
|
-
n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "
|
|
9
|
+
n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "425564da-031b-406e-b160-907df2faf2eb", e._sentryDebugIdIdentifier = "sentry-dbid-425564da-031b-406e-b160-907df2faf2eb");
|
|
10
10
|
} catch (e) {}
|
|
11
11
|
})();
|
|
12
12
|
import * as Sentry from "@sentry/node";
|
|
@@ -36,10 +36,10 @@ import { deserializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
|
|
|
36
36
|
* OTel Collector → Honeycomb: performance traces, metrics
|
|
37
37
|
*/
|
|
38
38
|
const name$1 = "hevy-mcp";
|
|
39
|
-
const version$1 = "1.26.
|
|
39
|
+
const version$1 = "1.26.2";
|
|
40
40
|
const collectorToken = process.env.OTEL_COLLECTOR_TOKEN ?? "";
|
|
41
41
|
const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
|
|
42
|
-
const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@1.26.
|
|
42
|
+
const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@1.26.2`;
|
|
43
43
|
const resource = resourceFromAttributes({
|
|
44
44
|
"service.name": name$1,
|
|
45
45
|
"service.version": version$1
|
|
@@ -127,6 +127,48 @@ const serverStartups = meter.createCounter("mcp.server.startups", { description:
|
|
|
127
127
|
/**
|
|
128
128
|
* Centralized error handling utility for MCP tools
|
|
129
129
|
*/
|
|
130
|
+
function normalizeHeaderValue$1(value) {
|
|
131
|
+
if (typeof value === "string") {
|
|
132
|
+
const trimmed = value.trim();
|
|
133
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
134
|
+
}
|
|
135
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
136
|
+
if (Array.isArray(value) && value.length > 0) return normalizeHeaderValue$1(value[0]);
|
|
137
|
+
}
|
|
138
|
+
function getHeaderValue$1(headers, key) {
|
|
139
|
+
if (!headers || typeof headers !== "object") return;
|
|
140
|
+
if ("get" in headers && typeof headers.get === "function") return normalizeHeaderValue$1(headers.get(key));
|
|
141
|
+
const headerRecord = headers;
|
|
142
|
+
return normalizeHeaderValue$1(headerRecord[key] ?? headerRecord[key.toLowerCase()] ?? headerRecord[key.toUpperCase()]);
|
|
143
|
+
}
|
|
144
|
+
function formatSecondsLabel(seconds) {
|
|
145
|
+
const roundedSeconds = Math.max(0, Math.round(seconds));
|
|
146
|
+
return `${roundedSeconds} second${roundedSeconds === 1 ? "" : "s"}`;
|
|
147
|
+
}
|
|
148
|
+
function getRateLimitMessage(error) {
|
|
149
|
+
if (!isAxiosError(error)) return "Rate limited by Hevy. Please wait and retry.";
|
|
150
|
+
const retryAfterHeader = getHeaderValue$1(error.response?.headers, "retry-after");
|
|
151
|
+
if (!retryAfterHeader) return "Rate limited by Hevy (HTTP 429). Please wait and retry your request.";
|
|
152
|
+
const seconds = Number(retryAfterHeader);
|
|
153
|
+
if (Number.isFinite(seconds) && seconds >= 0) return `Rate limited by Hevy (HTTP 429). Please wait about ${formatSecondsLabel(seconds)} before retrying.`;
|
|
154
|
+
const retryAtMillis = Date.parse(retryAfterHeader);
|
|
155
|
+
if (!Number.isNaN(retryAtMillis)) return `Rate limited by Hevy (HTTP 429). Please wait about ${formatSecondsLabel(Math.ceil(Math.max(0, retryAtMillis - Date.now()) / 1e3))} before retrying.`;
|
|
156
|
+
return "Rate limited by Hevy (HTTP 429). Please wait and retry your request.";
|
|
157
|
+
}
|
|
158
|
+
function isRetryExhaustedError(error) {
|
|
159
|
+
return !!error && typeof error === "object" && error.hevyRetryExhausted === true;
|
|
160
|
+
}
|
|
161
|
+
function getRetryExhaustedMessage(error) {
|
|
162
|
+
const retryCount = typeof error === "object" && error !== null ? error.hevyRetryCount : void 0;
|
|
163
|
+
const attemptCount = typeof retryCount === "number" && Number.isFinite(retryCount) ? retryCount + 1 : void 0;
|
|
164
|
+
if (attemptCount) return `Unable to complete the request after ${attemptCount} attempts to the Hevy API due to transient failures. Please try again shortly.`;
|
|
165
|
+
return "Unable to complete the request after multiple attempts to the Hevy API due to transient failures. Please try again shortly.";
|
|
166
|
+
}
|
|
167
|
+
function getUserFacingMessage(error, defaultMessage) {
|
|
168
|
+
if (isRetryExhaustedError(error)) return getRetryExhaustedMessage(error);
|
|
169
|
+
if (isAxiosError(error) && error.response?.status === 429) return getRateLimitMessage(error);
|
|
170
|
+
return defaultMessage;
|
|
171
|
+
}
|
|
130
172
|
/**
|
|
131
173
|
* Create a standardized error response for MCP tools
|
|
132
174
|
*
|
|
@@ -135,18 +177,22 @@ const serverStartups = meter.createCounter("mcp.server.startups", { description:
|
|
|
135
177
|
* @returns A formatted MCP tool response with error information
|
|
136
178
|
*/
|
|
137
179
|
function createErrorResponse(error, context) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
errorMessage = String(data);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
180
|
+
const originalErrorMessage = extractErrorMessage(error);
|
|
181
|
+
let errorMessage = originalErrorMessage;
|
|
182
|
+
const axiosErrorContext = extractAxiosErrorContext(error);
|
|
183
|
+
const mappedHevyErrorMessage = mapHevyErrorMessageByStatus(axiosErrorContext?.status);
|
|
184
|
+
if (mappedHevyErrorMessage) errorMessage = mappedHevyErrorMessage;
|
|
185
|
+
if (!mappedHevyErrorMessage && axiosErrorContext?.data) errorMessage = stringifyErrorData(axiosErrorContext.data);
|
|
186
|
+
errorMessage = getUserFacingMessage(error, errorMessage);
|
|
148
187
|
const errorCode = error instanceof Error && "code" in error ? error.code : void 0;
|
|
149
188
|
const errorType = determineErrorType(error, errorMessage);
|
|
189
|
+
const errorContext = {
|
|
190
|
+
sourceContext: context,
|
|
191
|
+
originalErrorMessage,
|
|
192
|
+
errorCode,
|
|
193
|
+
errorType,
|
|
194
|
+
axios: axiosErrorContext ?? void 0
|
|
195
|
+
};
|
|
150
196
|
const formattedMessage = `${context ? `[${context}] ` : ""}Error: ${errorMessage}`;
|
|
151
197
|
const errorCodeSuffix = errorCode ? `, Code: ${errorCode}` : "";
|
|
152
198
|
console.error(`${formattedMessage} (Type: ${errorType}${errorCodeSuffix})`, error);
|
|
@@ -155,13 +201,55 @@ function createErrorResponse(error, context) {
|
|
|
155
201
|
type: "text",
|
|
156
202
|
text: formattedMessage
|
|
157
203
|
}],
|
|
158
|
-
isError: true
|
|
204
|
+
isError: true,
|
|
205
|
+
errorContext
|
|
159
206
|
};
|
|
160
207
|
}
|
|
208
|
+
function extractErrorMessage(error) {
|
|
209
|
+
if (error instanceof Error) return error.message;
|
|
210
|
+
if (typeof error === "string") return error;
|
|
211
|
+
if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return error.message;
|
|
212
|
+
if (error && typeof error === "object") try {
|
|
213
|
+
return JSON.stringify(error);
|
|
214
|
+
} catch (_e) {
|
|
215
|
+
return "Unknown error object";
|
|
216
|
+
}
|
|
217
|
+
return String(error);
|
|
218
|
+
}
|
|
219
|
+
function extractAxiosErrorContext(error) {
|
|
220
|
+
if (!isAxiosError(error)) return null;
|
|
221
|
+
return {
|
|
222
|
+
status: error.response?.status,
|
|
223
|
+
statusText: error.response?.statusText,
|
|
224
|
+
data: error.response?.data,
|
|
225
|
+
method: error.config?.method,
|
|
226
|
+
url: error.config?.url
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function mapHevyErrorMessageByStatus(status) {
|
|
230
|
+
if (status === 401 || status === 403) return "The Hevy API key is invalid or has expired. Check HEVY_API_KEY.";
|
|
231
|
+
if (status === 404) return "The requested resource was not found in Hevy.";
|
|
232
|
+
if (status === 409) return "A conflict occurred (e.g., a body measurement already exists for this date). Use the update tool instead.";
|
|
233
|
+
if (status === 422) return "The request failed Hevy validation. Check the field values and try again.";
|
|
234
|
+
if (status === 429) return "Rate limited by Hevy. Please wait and retry.";
|
|
235
|
+
if (status && status >= 500 && status <= 599) return "Hevy API experienced an error. Please retry later.";
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
function stringifyErrorData(data) {
|
|
239
|
+
if (typeof data === "string") return data;
|
|
240
|
+
if (data && typeof data === "object") try {
|
|
241
|
+
return JSON.stringify(data);
|
|
242
|
+
} catch (_e) {
|
|
243
|
+
return "Unable to serialize error response data";
|
|
244
|
+
}
|
|
245
|
+
return String(data);
|
|
246
|
+
}
|
|
161
247
|
/**
|
|
162
248
|
* Determine the type of error based on error characteristics
|
|
163
249
|
*/
|
|
164
250
|
function determineErrorType(error, message) {
|
|
251
|
+
if (isRetryExhaustedError(error)) return "NETWORK_ERROR";
|
|
252
|
+
if (isAxiosError(error) && error.response?.status === 429) return "RATE_LIMIT";
|
|
165
253
|
const messageLower = message.toLowerCase();
|
|
166
254
|
const nameLower = error instanceof Error ? error.name.toLowerCase() : "";
|
|
167
255
|
if (nameLower.includes("network") || messageLower.includes("network") || nameLower.includes("fetch") || messageLower.includes("fetch") || nameLower.includes("timeout") || messageLower.includes("timeout")) return "NETWORK_ERROR";
|
|
@@ -405,7 +493,7 @@ function createJsonResponse(data, options = {
|
|
|
405
493
|
}) {
|
|
406
494
|
return { content: [{
|
|
407
495
|
type: "text",
|
|
408
|
-
text: options.pretty ? JSON.stringify(data, null, options.indent) : JSON.stringify(data)
|
|
496
|
+
text: (options.pretty ? JSON.stringify(data, null, options.indent) : JSON.stringify(data)) ?? "null"
|
|
409
497
|
}] };
|
|
410
498
|
}
|
|
411
499
|
/**
|
|
@@ -427,10 +515,7 @@ function createTextResponse(message) {
|
|
|
427
515
|
* @returns A formatted MCP tool response for empty results
|
|
428
516
|
*/
|
|
429
517
|
function createEmptyResponse(message = "No data found") {
|
|
430
|
-
return
|
|
431
|
-
type: "text",
|
|
432
|
-
text: message
|
|
433
|
-
}] };
|
|
518
|
+
return createTextResponse(message);
|
|
434
519
|
}
|
|
435
520
|
//#endregion
|
|
436
521
|
//#region src/utils/tool-annotations.ts
|
|
@@ -466,8 +551,86 @@ function updateAnnotations(title) {
|
|
|
466
551
|
};
|
|
467
552
|
}
|
|
468
553
|
//#endregion
|
|
469
|
-
//#region src/
|
|
554
|
+
//#region src/utils/tool-helpers.ts
|
|
555
|
+
const HEVY_CLIENT_NOT_INITIALIZED_ERROR = "API client not initialized. Please provide HEVY_API_KEY.";
|
|
556
|
+
/**
|
|
557
|
+
* Assert that the Hevy API client is initialized before running a tool.
|
|
558
|
+
*/
|
|
559
|
+
function requireClient(client) {
|
|
560
|
+
if (!client) throw new Error(HEVY_CLIENT_NOT_INITIALIZED_ERROR);
|
|
561
|
+
return client;
|
|
562
|
+
}
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/utils/schemas.ts
|
|
565
|
+
function coerceNullishNumberInput(value) {
|
|
566
|
+
if (value === null || value === void 0) return value;
|
|
567
|
+
if (typeof value !== "string") return value;
|
|
568
|
+
const trimmed = value.trim();
|
|
569
|
+
if (trimmed === "") return;
|
|
570
|
+
const lowered = trimmed.toLowerCase();
|
|
571
|
+
if (lowered === "null") return null;
|
|
572
|
+
if (lowered === "undefined") return;
|
|
573
|
+
const asNumber = Number(trimmed);
|
|
574
|
+
if (Number.isNaN(asNumber)) return value;
|
|
575
|
+
return asNumber;
|
|
576
|
+
}
|
|
577
|
+
const zNullableInt = z.preprocess(coerceNullishNumberInput, z.number().int().nullable().optional());
|
|
470
578
|
const zNullableNumber = z.preprocess((value) => value === "" ? void 0 : value, z.coerce.number().nullable().optional());
|
|
579
|
+
const zOptionalRepRange = z.preprocess((value) => value === null ? void 0 : value, z.object({
|
|
580
|
+
start: zNullableInt,
|
|
581
|
+
end: zNullableInt
|
|
582
|
+
}).optional());
|
|
583
|
+
const setTypeEnum = z.enum([
|
|
584
|
+
"warmup",
|
|
585
|
+
"normal",
|
|
586
|
+
"failure",
|
|
587
|
+
"dropset"
|
|
588
|
+
]).default("normal");
|
|
589
|
+
const muscleGroupEnum = z.enum([
|
|
590
|
+
"abdominals",
|
|
591
|
+
"shoulders",
|
|
592
|
+
"biceps",
|
|
593
|
+
"triceps",
|
|
594
|
+
"forearms",
|
|
595
|
+
"quadriceps",
|
|
596
|
+
"hamstrings",
|
|
597
|
+
"calves",
|
|
598
|
+
"glutes",
|
|
599
|
+
"abductors",
|
|
600
|
+
"adductors",
|
|
601
|
+
"lats",
|
|
602
|
+
"upper_back",
|
|
603
|
+
"traps",
|
|
604
|
+
"lower_back",
|
|
605
|
+
"chest",
|
|
606
|
+
"cardio",
|
|
607
|
+
"neck",
|
|
608
|
+
"full_body",
|
|
609
|
+
"other"
|
|
610
|
+
]);
|
|
611
|
+
const exerciseTypeEnum = z.enum([
|
|
612
|
+
"weight_reps",
|
|
613
|
+
"reps_only",
|
|
614
|
+
"bodyweight_reps",
|
|
615
|
+
"bodyweight_assisted_reps",
|
|
616
|
+
"duration",
|
|
617
|
+
"weight_duration",
|
|
618
|
+
"distance_duration",
|
|
619
|
+
"short_distance_weight"
|
|
620
|
+
]);
|
|
621
|
+
const equipmentCategoryEnum = z.enum([
|
|
622
|
+
"none",
|
|
623
|
+
"barbell",
|
|
624
|
+
"dumbbell",
|
|
625
|
+
"kettlebell",
|
|
626
|
+
"machine",
|
|
627
|
+
"plate",
|
|
628
|
+
"resistance_band",
|
|
629
|
+
"suspension",
|
|
630
|
+
"other"
|
|
631
|
+
]);
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region src/tools/body-measurements.ts
|
|
471
634
|
const bodyMeasurementFieldsSchema = {
|
|
472
635
|
weightKg: zNullableNumber.describe("Body weight in kilograms"),
|
|
473
636
|
leanMassKg: zNullableNumber.describe("Lean body mass in kilograms"),
|
|
@@ -520,9 +683,9 @@ function registerBodyMeasurementTools(server, hevyClient) {
|
|
|
520
683
|
pageSize: z.coerce.number().int().gte(1).lte(10).default(10)
|
|
521
684
|
};
|
|
522
685
|
server.tool("get-body-measurements", "Get a paginated list of body measurements for the authenticated user. Returns measurements including weight, body fat, and various circumference measurements.", getBodyMeasurementsSchema, readOnlyAnnotations("Get Body Measurements"), withErrorHandling(async (args) => {
|
|
523
|
-
|
|
686
|
+
const client = requireClient(hevyClient);
|
|
524
687
|
const { page, pageSize } = args;
|
|
525
|
-
const measurements = (await
|
|
688
|
+
const measurements = (await client.getBodyMeasurements({
|
|
526
689
|
page,
|
|
527
690
|
pageSize
|
|
528
691
|
}))?.body_measurements?.map((measurement) => formatBodyMeasurement(measurement)) || [];
|
|
@@ -531,9 +694,9 @@ function registerBodyMeasurementTools(server, hevyClient) {
|
|
|
531
694
|
}, "get-body-measurements"));
|
|
532
695
|
const getBodyMeasurementSchema = { date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").describe("The date of the body measurement (YYYY-MM-DD)") };
|
|
533
696
|
server.tool("get-body-measurement", "Get a single body measurement by date. Returns all measurement fields for the specified date.", getBodyMeasurementSchema, readOnlyAnnotations("Get Body Measurement"), withErrorHandling(async (args) => {
|
|
534
|
-
|
|
697
|
+
const client = requireClient(hevyClient);
|
|
535
698
|
const { date } = args;
|
|
536
|
-
const data = await
|
|
699
|
+
const data = await client.getBodyMeasurement(date);
|
|
537
700
|
if (!data) return createEmptyResponse(`No body measurement found for date ${date}`);
|
|
538
701
|
return createJsonResponse(formatBodyMeasurement(data));
|
|
539
702
|
}, "get-body-measurement"));
|
|
@@ -542,9 +705,9 @@ function registerBodyMeasurementTools(server, hevyClient) {
|
|
|
542
705
|
...bodyMeasurementFieldsSchema
|
|
543
706
|
};
|
|
544
707
|
server.tool("create-body-measurement", "Create a body measurement entry for a given date. All measurement fields are optional; null values are treated as omitted, since the Hevy API does not support clearing individual fields. Returns 409 if an entry already exists for that date — use update-body-measurement instead.", createBodyMeasurementSchema, createAnnotations("Create Body Measurement"), withErrorHandling(async (args) => {
|
|
545
|
-
|
|
708
|
+
const client = requireClient(hevyClient);
|
|
546
709
|
const { date, ...fields } = args;
|
|
547
|
-
await
|
|
710
|
+
await client.createBodyMeasurement({
|
|
548
711
|
date,
|
|
549
712
|
...buildMeasurementPayload(fields)
|
|
550
713
|
});
|
|
@@ -555,11 +718,11 @@ function registerBodyMeasurementTools(server, hevyClient) {
|
|
|
555
718
|
...bodyMeasurementFieldsSchema
|
|
556
719
|
};
|
|
557
720
|
server.tool("update-body-measurement", "Update an existing body measurement entry for a given date. Only the fields you provide are sent and updated; null values are treated as omitted, since the Hevy API does not support clearing individual fields. Requires at least one measurement field. Returns 404 if no entry exists for the date.", updateBodyMeasurementSchema, updateAnnotations("Update Body Measurement"), withErrorHandling(async (args) => {
|
|
558
|
-
|
|
721
|
+
const client = requireClient(hevyClient);
|
|
559
722
|
const { date, ...fields } = args;
|
|
560
723
|
const payload = buildMeasurementPayload(fields);
|
|
561
724
|
if (Object.keys(payload).length === 0) throw new Error("No measurement fields provided. Include at least one numeric measurement field (e.g. weightKg) to update.");
|
|
562
|
-
await
|
|
725
|
+
await client.updateBodyMeasurement(date, payload);
|
|
563
726
|
return createTextResponse(`Body measurement for ${date} updated successfully.`);
|
|
564
727
|
}, "update-body-measurement"));
|
|
565
728
|
}
|
|
@@ -574,9 +737,9 @@ function registerFolderTools(server, hevyClient) {
|
|
|
574
737
|
pageSize: z.coerce.number().int().gte(1).lte(10).default(5)
|
|
575
738
|
};
|
|
576
739
|
server.tool("get-routine-folders", "Get a paginated list of your routine folders, including both default and custom folders. Useful for organizing and browsing your workout routines.", getRoutineFoldersSchema, readOnlyAnnotations("Get Routine Folders"), withErrorHandling(async (args) => {
|
|
577
|
-
|
|
740
|
+
const client = requireClient(hevyClient);
|
|
578
741
|
const { page, pageSize } = args;
|
|
579
|
-
const folders = (await
|
|
742
|
+
const folders = (await client.getRoutineFolders({
|
|
580
743
|
page,
|
|
581
744
|
pageSize
|
|
582
745
|
}))?.routine_folders?.map((folder) => formatRoutineFolder(folder)) || [];
|
|
@@ -585,17 +748,17 @@ function registerFolderTools(server, hevyClient) {
|
|
|
585
748
|
}, "get-routine-folders"));
|
|
586
749
|
const getRoutineFolderSchema = { folderId: z.string().min(1) };
|
|
587
750
|
server.tool("get-routine-folder", "Get complete details of a specific routine folder by its ID, including name, creation date, and associated routines.", getRoutineFolderSchema, readOnlyAnnotations("Get Routine Folder"), withErrorHandling(async (args) => {
|
|
588
|
-
|
|
751
|
+
const client = requireClient(hevyClient);
|
|
589
752
|
const { folderId } = args;
|
|
590
|
-
const data = await
|
|
753
|
+
const data = await client.getRoutineFolder(folderId);
|
|
591
754
|
if (!data) return createEmptyResponse(`Routine folder with ID ${folderId} not found`);
|
|
592
755
|
return createJsonResponse(formatRoutineFolder(data));
|
|
593
756
|
}, "get-routine-folder"));
|
|
594
757
|
const createRoutineFolderSchema = { name: z.string().min(1) };
|
|
595
758
|
server.tool("create-routine-folder", "Create a new routine folder in your Hevy account. Requires a name for the folder. Returns the full folder details including the new folder ID.", createRoutineFolderSchema, createAnnotations("Create Routine Folder"), withErrorHandling(async (args) => {
|
|
596
|
-
|
|
759
|
+
const client = requireClient(hevyClient);
|
|
597
760
|
const { name } = args;
|
|
598
|
-
const data = await
|
|
761
|
+
const data = await client.createRoutineFolder({ routine_folder: { title: name } });
|
|
599
762
|
if (!data) return createEmptyResponse("Failed to create routine folder: Server returned no data");
|
|
600
763
|
return createJsonResponse(formatRoutineFolder(data), {
|
|
601
764
|
pretty: true,
|
|
@@ -625,23 +788,6 @@ function parseJsonArray(val) {
|
|
|
625
788
|
}
|
|
626
789
|
//#endregion
|
|
627
790
|
//#region src/tools/routines.ts
|
|
628
|
-
function coerceNullishNumberInput(value) {
|
|
629
|
-
if (value === null || value === void 0) return value;
|
|
630
|
-
if (typeof value !== "string") return value;
|
|
631
|
-
const trimmed = value.trim();
|
|
632
|
-
if (trimmed === "") return;
|
|
633
|
-
const lowered = trimmed.toLowerCase();
|
|
634
|
-
if (lowered === "null") return null;
|
|
635
|
-
if (lowered === "undefined") return;
|
|
636
|
-
const asNumber = Number(trimmed);
|
|
637
|
-
if (Number.isNaN(asNumber)) return value;
|
|
638
|
-
return asNumber;
|
|
639
|
-
}
|
|
640
|
-
const zNullableInt = z.preprocess(coerceNullishNumberInput, z.number().int().nullable().optional());
|
|
641
|
-
const zOptionalRepRange = z.preprocess((value) => value === null ? void 0 : value, z.object({
|
|
642
|
-
start: zNullableInt,
|
|
643
|
-
end: zNullableInt
|
|
644
|
-
}).optional());
|
|
645
791
|
function buildRepRange(repRange) {
|
|
646
792
|
if (!repRange) return null;
|
|
647
793
|
const start = repRange.start ?? void 0;
|
|
@@ -674,9 +820,9 @@ function registerRoutineTools(server, hevyClient) {
|
|
|
674
820
|
pageSize: z.coerce.number().int().gte(1).lte(10).default(5)
|
|
675
821
|
};
|
|
676
822
|
server.tool("get-routines", "Get a paginated list of your workout routines, including custom and default routines. Useful for browsing or searching your available routines.", getRoutinesSchema, readOnlyAnnotations("Get Routines"), withErrorHandling(async (args) => {
|
|
677
|
-
|
|
823
|
+
const client = requireClient(hevyClient);
|
|
678
824
|
const { page, pageSize } = args;
|
|
679
|
-
const routines = (await
|
|
825
|
+
const routines = (await client.getRoutines({
|
|
680
826
|
page,
|
|
681
827
|
pageSize
|
|
682
828
|
}))?.routines?.map((routine) => formatRoutine(routine)) || [];
|
|
@@ -685,9 +831,9 @@ function registerRoutineTools(server, hevyClient) {
|
|
|
685
831
|
}, "get-routines"));
|
|
686
832
|
const getRoutineSchema = { routineId: z.string().min(1) };
|
|
687
833
|
server.tool("get-routine", "Get a routine by its ID using the direct endpoint. Returns all details for the specified routine.", getRoutineSchema, readOnlyAnnotations("Get Routine"), withErrorHandling(async (args) => {
|
|
688
|
-
|
|
834
|
+
const client = requireClient(hevyClient);
|
|
689
835
|
const { routineId } = args;
|
|
690
|
-
const data = await
|
|
836
|
+
const data = await client.getRoutineById(String(routineId));
|
|
691
837
|
if (!data || !data.routine) return createEmptyResponse(`Routine with ID ${routineId} not found`);
|
|
692
838
|
return createJsonResponse(formatRoutine(data.routine));
|
|
693
839
|
}, "get-routine"));
|
|
@@ -701,12 +847,7 @@ function registerRoutineTools(server, hevyClient) {
|
|
|
701
847
|
restSeconds: z.coerce.number().int().min(0).optional(),
|
|
702
848
|
notes: z.string().optional(),
|
|
703
849
|
sets: z.array(z.object({
|
|
704
|
-
type:
|
|
705
|
-
"warmup",
|
|
706
|
-
"normal",
|
|
707
|
-
"failure",
|
|
708
|
-
"dropset"
|
|
709
|
-
]).default("normal"),
|
|
850
|
+
type: setTypeEnum,
|
|
710
851
|
weight: z.coerce.number().optional(),
|
|
711
852
|
weightKg: z.coerce.number().optional(),
|
|
712
853
|
reps: zNullableInt,
|
|
@@ -720,10 +861,10 @@ function registerRoutineTools(server, hevyClient) {
|
|
|
720
861
|
})))
|
|
721
862
|
};
|
|
722
863
|
server.tool("create-routine", "Create a new workout routine in your Hevy account. Requires a title and at least one exercise with sets. Optionally assign to a folder. Returns the full routine details including the new routine ID.", createRoutineSchema, createAnnotations("Create Routine"), withErrorHandling(async (args) => {
|
|
723
|
-
|
|
864
|
+
const client = requireClient(hevyClient);
|
|
724
865
|
const { title, folderId, notes, exercises } = args;
|
|
725
866
|
let usesRepRanges = false;
|
|
726
|
-
const data = await
|
|
867
|
+
const data = await client.createRoutine({ routine: {
|
|
727
868
|
title,
|
|
728
869
|
folder_id: folderId ?? null,
|
|
729
870
|
notes: notes ?? "",
|
|
@@ -773,12 +914,7 @@ function registerRoutineTools(server, hevyClient) {
|
|
|
773
914
|
restSeconds: z.coerce.number().int().min(0).optional(),
|
|
774
915
|
notes: z.string().optional(),
|
|
775
916
|
sets: z.array(z.object({
|
|
776
|
-
type:
|
|
777
|
-
"warmup",
|
|
778
|
-
"normal",
|
|
779
|
-
"failure",
|
|
780
|
-
"dropset"
|
|
781
|
-
]).default("normal"),
|
|
917
|
+
type: setTypeEnum,
|
|
782
918
|
weight: z.coerce.number().optional(),
|
|
783
919
|
weightKg: z.coerce.number().optional(),
|
|
784
920
|
reps: zNullableInt,
|
|
@@ -792,10 +928,10 @@ function registerRoutineTools(server, hevyClient) {
|
|
|
792
928
|
})))
|
|
793
929
|
};
|
|
794
930
|
server.tool("update-routine", "Update an existing routine by ID. You can modify the title, notes, and exercise configurations. Returns the updated routine with all changes applied.", updateRoutineSchema, updateAnnotations("Update Routine"), withErrorHandling(async (args) => {
|
|
795
|
-
|
|
931
|
+
const client = requireClient(hevyClient);
|
|
796
932
|
const { routineId, title, notes, exercises } = args;
|
|
797
933
|
let usesRepRanges = false;
|
|
798
|
-
const data = await
|
|
934
|
+
const data = await client.updateRoutine(routineId, { routine: {
|
|
799
935
|
title,
|
|
800
936
|
notes: notes ?? null,
|
|
801
937
|
exercises: exercises.map((exercise) => {
|
|
@@ -836,31 +972,114 @@ function registerRoutineTools(server, hevyClient) {
|
|
|
836
972
|
}, "update-routine"));
|
|
837
973
|
}
|
|
838
974
|
//#endregion
|
|
975
|
+
//#region src/utils/cache.ts
|
|
976
|
+
/**
|
|
977
|
+
* Shared in-memory cache for async fetches with TTL, LRU eviction,
|
|
978
|
+
* in-flight de-duplication, and explicit refresh/invalidation support.
|
|
979
|
+
*/
|
|
980
|
+
var AsyncTtlCache = class {
|
|
981
|
+
ttlMs;
|
|
982
|
+
maxSize;
|
|
983
|
+
now;
|
|
984
|
+
entries = /* @__PURE__ */ new Map();
|
|
985
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
986
|
+
requestCounter = 0;
|
|
987
|
+
constructor(options, now = Date.now) {
|
|
988
|
+
const { ttlMs, maxSize } = options;
|
|
989
|
+
if (ttlMs <= 0) throw new Error("Cache ttlMs must be greater than 0.");
|
|
990
|
+
if (maxSize <= 0) throw new Error("Cache maxSize must be greater than 0.");
|
|
991
|
+
this.ttlMs = ttlMs;
|
|
992
|
+
this.maxSize = maxSize;
|
|
993
|
+
this.now = now;
|
|
994
|
+
}
|
|
995
|
+
async getOrFetch(key, fetcher, options = {}) {
|
|
996
|
+
const { refresh = false } = options;
|
|
997
|
+
if (refresh) this.invalidate(key);
|
|
998
|
+
else {
|
|
999
|
+
const cachedEntry = this.entries.get(key);
|
|
1000
|
+
if (cachedEntry !== void 0) {
|
|
1001
|
+
if (cachedEntry.expiresAt > this.now()) {
|
|
1002
|
+
this.markAsRecentlyUsed(key, cachedEntry);
|
|
1003
|
+
return cachedEntry.value;
|
|
1004
|
+
}
|
|
1005
|
+
this.entries.delete(key);
|
|
1006
|
+
}
|
|
1007
|
+
const inFlightEntry = this.inFlight.get(key);
|
|
1008
|
+
if (inFlightEntry !== void 0) return inFlightEntry.promise;
|
|
1009
|
+
}
|
|
1010
|
+
const requestId = ++this.requestCounter;
|
|
1011
|
+
const request = (async () => {
|
|
1012
|
+
try {
|
|
1013
|
+
const value = await fetcher();
|
|
1014
|
+
if (this.isCurrentRequest(key, requestId)) this.setValue(key, value);
|
|
1015
|
+
return value;
|
|
1016
|
+
} finally {
|
|
1017
|
+
if (this.inFlight.get(key)?.requestId === requestId) this.inFlight.delete(key);
|
|
1018
|
+
}
|
|
1019
|
+
})();
|
|
1020
|
+
this.inFlight.set(key, {
|
|
1021
|
+
promise: request,
|
|
1022
|
+
requestId
|
|
1023
|
+
});
|
|
1024
|
+
return request;
|
|
1025
|
+
}
|
|
1026
|
+
invalidate(key) {
|
|
1027
|
+
this.entries.delete(key);
|
|
1028
|
+
this.inFlight.delete(key);
|
|
1029
|
+
}
|
|
1030
|
+
clear() {
|
|
1031
|
+
this.entries.clear();
|
|
1032
|
+
this.inFlight.clear();
|
|
1033
|
+
}
|
|
1034
|
+
get size() {
|
|
1035
|
+
return this.entries.size;
|
|
1036
|
+
}
|
|
1037
|
+
isCurrentRequest(key, requestId) {
|
|
1038
|
+
return this.inFlight.get(key)?.requestId === requestId;
|
|
1039
|
+
}
|
|
1040
|
+
markAsRecentlyUsed(key, entry) {
|
|
1041
|
+
this.entries.delete(key);
|
|
1042
|
+
this.entries.set(key, entry);
|
|
1043
|
+
}
|
|
1044
|
+
setValue(key, value) {
|
|
1045
|
+
this.entries.delete(key);
|
|
1046
|
+
this.entries.set(key, {
|
|
1047
|
+
value,
|
|
1048
|
+
expiresAt: this.now() + this.ttlMs
|
|
1049
|
+
});
|
|
1050
|
+
this.evictLeastRecentlyUsed();
|
|
1051
|
+
}
|
|
1052
|
+
evictLeastRecentlyUsed() {
|
|
1053
|
+
while (this.entries.size > this.maxSize) {
|
|
1054
|
+
const oldestKey = this.entries.keys().next().value;
|
|
1055
|
+
if (oldestKey === void 0) return;
|
|
1056
|
+
this.entries.delete(oldestKey);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
//#endregion
|
|
839
1061
|
//#region src/tools/templates.ts
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
];
|
|
862
|
-
let exerciseTemplateCache = null;
|
|
863
|
-
let exerciseTemplateFetch = null;
|
|
1062
|
+
const EXERCISE_TEMPLATE_CATALOG_CACHE_KEY = "exercise-template-catalog";
|
|
1063
|
+
const exerciseTemplateCatalogCache = new AsyncTtlCache({
|
|
1064
|
+
ttlMs: 300 * 1e3,
|
|
1065
|
+
maxSize: 1
|
|
1066
|
+
});
|
|
1067
|
+
async function fetchExerciseTemplateCatalog(hevyClient) {
|
|
1068
|
+
const allTemplates = [];
|
|
1069
|
+
let page = 1;
|
|
1070
|
+
let pageCount = 1;
|
|
1071
|
+
do {
|
|
1072
|
+
const data = await hevyClient.getExerciseTemplates({
|
|
1073
|
+
page,
|
|
1074
|
+
pageSize: 100
|
|
1075
|
+
});
|
|
1076
|
+
const templates = data?.exercise_templates ?? [];
|
|
1077
|
+
allTemplates.push(...templates);
|
|
1078
|
+
pageCount = data?.page_count ?? 1;
|
|
1079
|
+
page++;
|
|
1080
|
+
} while (page <= pageCount);
|
|
1081
|
+
return allTemplates;
|
|
1082
|
+
}
|
|
864
1083
|
/**
|
|
865
1084
|
* Register all exercise template-related tools with the MCP server
|
|
866
1085
|
*/
|
|
@@ -870,9 +1089,9 @@ function registerTemplateTools(server, hevyClient) {
|
|
|
870
1089
|
pageSize: z.coerce.number().int().gte(1).lte(100).default(5)
|
|
871
1090
|
};
|
|
872
1091
|
server.tool("get-exercise-templates", "Get a paginated list of exercise templates (default and custom) with details like name, category, equipment, and muscle groups. Useful for browsing or searching available exercises.", getExerciseTemplatesSchema, readOnlyAnnotations("Get Exercise Templates"), withErrorHandling(async (args) => {
|
|
873
|
-
|
|
1092
|
+
const client = requireClient(hevyClient);
|
|
874
1093
|
const { page, pageSize } = args;
|
|
875
|
-
const templates = (await
|
|
1094
|
+
const templates = (await client.getExerciseTemplates({
|
|
876
1095
|
page,
|
|
877
1096
|
pageSize
|
|
878
1097
|
}))?.exercise_templates?.map((template) => formatExerciseTemplate(template)) || [];
|
|
@@ -881,9 +1100,9 @@ function registerTemplateTools(server, hevyClient) {
|
|
|
881
1100
|
}, "get-exercise-templates"));
|
|
882
1101
|
const getExerciseTemplateSchema = { exerciseTemplateId: z.string().min(1) };
|
|
883
1102
|
server.tool("get-exercise-template", "Get complete details of a specific exercise template by its ID, including name, category, equipment, muscle groups, and notes.", getExerciseTemplateSchema, readOnlyAnnotations("Get Exercise Template"), withErrorHandling(async (args) => {
|
|
884
|
-
|
|
1103
|
+
const client = requireClient(hevyClient);
|
|
885
1104
|
const { exerciseTemplateId } = args;
|
|
886
|
-
const data = await
|
|
1105
|
+
const data = await client.getExerciseTemplate(exerciseTemplateId);
|
|
887
1106
|
if (!data) return createEmptyResponse(`Exercise template with ID ${exerciseTemplateId} not found`);
|
|
888
1107
|
return createJsonResponse(formatExerciseTemplate(data));
|
|
889
1108
|
}, "get-exercise-template"));
|
|
@@ -893,9 +1112,9 @@ function registerTemplateTools(server, hevyClient) {
|
|
|
893
1112
|
endDate: z.string().datetime({ offset: true }).describe("ISO 8601 end date for filtering history").optional()
|
|
894
1113
|
};
|
|
895
1114
|
server.tool("get-exercise-history", "Get past sets for a specific exercise template, optionally filtered by start and end dates.", getExerciseHistorySchema, readOnlyAnnotations("Get Exercise History"), withErrorHandling(async (args) => {
|
|
896
|
-
|
|
1115
|
+
const client = requireClient(hevyClient);
|
|
897
1116
|
const { exerciseTemplateId, startDate, endDate } = args;
|
|
898
|
-
const history = (await
|
|
1117
|
+
const history = (await client.getExerciseHistory(exerciseTemplateId, {
|
|
899
1118
|
...startDate ? { start_date: startDate } : {},
|
|
900
1119
|
...endDate ? { end_date: endDate } : {}
|
|
901
1120
|
}))?.exercise_history?.map((entry) => formatExerciseHistoryEntry(entry)) || [];
|
|
@@ -904,35 +1123,16 @@ function registerTemplateTools(server, hevyClient) {
|
|
|
904
1123
|
}, "get-exercise-history"));
|
|
905
1124
|
const createExerciseTemplateSchema = {
|
|
906
1125
|
title: z.string().min(1),
|
|
907
|
-
exerciseType:
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
"bodyweight_assisted_reps",
|
|
912
|
-
"duration",
|
|
913
|
-
"weight_duration",
|
|
914
|
-
"distance_duration",
|
|
915
|
-
"short_distance_weight"
|
|
916
|
-
]),
|
|
917
|
-
equipmentCategory: z.enum([
|
|
918
|
-
"none",
|
|
919
|
-
"barbell",
|
|
920
|
-
"dumbbell",
|
|
921
|
-
"kettlebell",
|
|
922
|
-
"machine",
|
|
923
|
-
"plate",
|
|
924
|
-
"resistance_band",
|
|
925
|
-
"suspension",
|
|
926
|
-
"other"
|
|
927
|
-
]),
|
|
928
|
-
muscleGroup: z.enum(MUSCLE_GROUPS),
|
|
929
|
-
otherMuscles: z.array(z.enum(MUSCLE_GROUPS)).default([])
|
|
1126
|
+
exerciseType: exerciseTypeEnum,
|
|
1127
|
+
equipmentCategory: equipmentCategoryEnum,
|
|
1128
|
+
muscleGroup: muscleGroupEnum,
|
|
1129
|
+
otherMuscles: z.array(muscleGroupEnum).default([])
|
|
930
1130
|
};
|
|
931
1131
|
server.tool("create-exercise-template", "Create a custom exercise template with title, type, equipment, and muscle groups.", createExerciseTemplateSchema, createAnnotations("Create Exercise Template"), withErrorHandling(async (args) => {
|
|
932
|
-
|
|
1132
|
+
const client = requireClient(hevyClient);
|
|
933
1133
|
const { title, exerciseType, equipmentCategory, muscleGroup, otherMuscles } = args;
|
|
934
1134
|
return createJsonResponse({
|
|
935
|
-
id: (await
|
|
1135
|
+
id: (await client.createExerciseTemplate({ exercise: {
|
|
936
1136
|
title,
|
|
937
1137
|
exercise_type: exerciseType,
|
|
938
1138
|
equipment_category: equipmentCategory,
|
|
@@ -944,40 +1144,15 @@ function registerTemplateTools(server, hevyClient) {
|
|
|
944
1144
|
}, "create-exercise-template"));
|
|
945
1145
|
const searchExerciseTemplatesSchema = {
|
|
946
1146
|
query: z.string().min(1).describe("Case-insensitive substring to match against exercise template titles"),
|
|
947
|
-
primaryMuscleGroup:
|
|
948
|
-
refresh: z.boolean().optional().default(false).describe("Set to true to
|
|
1147
|
+
primaryMuscleGroup: muscleGroupEnum.optional().describe("Optional filter to restrict results to a specific primary muscle group"),
|
|
1148
|
+
refresh: z.boolean().optional().default(false).describe("Set to true to invalidate the catalog cache and re-fetch all templates from the API")
|
|
949
1149
|
};
|
|
950
|
-
server.tool("search-exercise-templates", "Search exercise templates by name with optional muscle group filter. Fetches all templates from the Hevy API on first call
|
|
951
|
-
|
|
1150
|
+
server.tool("search-exercise-templates", "Search exercise templates by name with optional muscle group filter. Fetches all templates from the Hevy API on first call, caches the catalog in memory with a bounded TTL cache, and reuses it for subsequent searches. Use refresh:true to force a re-fetch.", searchExerciseTemplatesSchema, readOnlyAnnotations("Search Exercise Templates"), withErrorHandling(async (args) => {
|
|
1151
|
+
const client = requireClient(hevyClient);
|
|
952
1152
|
const { query, primaryMuscleGroup, refresh } = args;
|
|
953
|
-
|
|
954
|
-
if (refresh) exerciseTemplateFetch = null;
|
|
955
|
-
if (exerciseTemplateFetch === null) exerciseTemplateFetch = (async () => {
|
|
956
|
-
try {
|
|
957
|
-
const allTemplates = [];
|
|
958
|
-
let page = 1;
|
|
959
|
-
let pageCount = 1;
|
|
960
|
-
do {
|
|
961
|
-
const data = await hevyClient.getExerciseTemplates({
|
|
962
|
-
page,
|
|
963
|
-
pageSize: 100
|
|
964
|
-
});
|
|
965
|
-
const templates = data?.exercise_templates ?? [];
|
|
966
|
-
allTemplates.push(...templates);
|
|
967
|
-
pageCount = data?.page_count ?? 1;
|
|
968
|
-
page++;
|
|
969
|
-
} while (page <= pageCount);
|
|
970
|
-
exerciseTemplateCache = allTemplates;
|
|
971
|
-
return allTemplates;
|
|
972
|
-
} finally {
|
|
973
|
-
exerciseTemplateFetch = null;
|
|
974
|
-
}
|
|
975
|
-
})();
|
|
976
|
-
await exerciseTemplateFetch;
|
|
977
|
-
}
|
|
1153
|
+
const catalog = await exerciseTemplateCatalogCache.getOrFetch(EXERCISE_TEMPLATE_CATALOG_CACHE_KEY, () => fetchExerciseTemplateCatalog(client), { refresh });
|
|
978
1154
|
const queryLower = query.toLowerCase();
|
|
979
|
-
|
|
980
|
-
let results = exerciseTemplateCache.filter((t) => (t.title ?? "").toLowerCase().includes(queryLower));
|
|
1155
|
+
let results = catalog.filter((t) => (t.title ?? "").toLowerCase().includes(queryLower));
|
|
981
1156
|
if (primaryMuscleGroup !== void 0) results = results.filter((t) => t.primary_muscle_group === primaryMuscleGroup);
|
|
982
1157
|
if (results.length === 0) return createEmptyResponse(`No exercise templates found matching "${query}"${primaryMuscleGroup ? ` with primary muscle group "${primaryMuscleGroup}"` : ""}`);
|
|
983
1158
|
return createJsonResponse(results.map(formatExerciseTemplate));
|
|
@@ -987,8 +1162,7 @@ function registerTemplateTools(server, hevyClient) {
|
|
|
987
1162
|
//#region src/tools/user.ts
|
|
988
1163
|
function registerUserTools(server, hevyClient) {
|
|
989
1164
|
server.tool("get-user-info", "Get the authenticated user's account info, including user ID, display name, and public profile URL. Useful for verifying which account the API key belongs to.", {}, readOnlyAnnotations("Get User Info"), withErrorHandling(async (_args) => {
|
|
990
|
-
|
|
991
|
-
const data = await hevyClient.getUserInfo();
|
|
1165
|
+
const data = await requireClient(hevyClient).getUserInfo();
|
|
992
1166
|
if (!data?.data) return createEmptyResponse("No user info found for the authenticated user");
|
|
993
1167
|
return createJsonResponse(data.data);
|
|
994
1168
|
}, "get-user-info"));
|
|
@@ -1004,9 +1178,9 @@ function registerWorkoutTools(server, hevyClient) {
|
|
|
1004
1178
|
pageSize: z.coerce.number().int().gte(1).lte(10).default(5)
|
|
1005
1179
|
};
|
|
1006
1180
|
server.tool("get-workouts", "Get a paginated list of workouts. Returns workout details including title, description, start/end times, and exercises performed. Results are ordered from newest to oldest.", getWorkoutsSchema, readOnlyAnnotations("Get Workouts"), withErrorHandling(async (args) => {
|
|
1007
|
-
|
|
1181
|
+
const client = requireClient(hevyClient);
|
|
1008
1182
|
const { page, pageSize } = args;
|
|
1009
|
-
const workouts = (await
|
|
1183
|
+
const workouts = (await client.getWorkouts({
|
|
1010
1184
|
page,
|
|
1011
1185
|
pageSize
|
|
1012
1186
|
}))?.workouts?.map((workout) => formatWorkout(workout)) || [];
|
|
@@ -1015,15 +1189,14 @@ function registerWorkoutTools(server, hevyClient) {
|
|
|
1015
1189
|
}, "get-workouts"));
|
|
1016
1190
|
const getWorkoutSchema = { workoutId: z.string().min(1) };
|
|
1017
1191
|
server.tool("get-workout", "Get complete details of a specific workout by ID. Returns all workout information including title, description, start/end times, and detailed exercise data.", getWorkoutSchema, readOnlyAnnotations("Get Workout"), withErrorHandling(async (args) => {
|
|
1018
|
-
|
|
1192
|
+
const client = requireClient(hevyClient);
|
|
1019
1193
|
const { workoutId } = args;
|
|
1020
|
-
const data = await
|
|
1194
|
+
const data = await client.getWorkout(workoutId);
|
|
1021
1195
|
if (!data) return createEmptyResponse(`Workout with ID ${workoutId} not found`);
|
|
1022
1196
|
return createJsonResponse(formatWorkout(data));
|
|
1023
1197
|
}, "get-workout"));
|
|
1024
1198
|
server.tool("get-workout-count", "Get the total number of workouts on the account. Useful for pagination or statistics.", {}, readOnlyAnnotations("Get Workout Count"), withErrorHandling(async () => {
|
|
1025
|
-
|
|
1026
|
-
return createJsonResponse({ count: (await hevyClient.getWorkoutCount())?.workout_count ?? 0 });
|
|
1199
|
+
return createJsonResponse({ count: (await requireClient(hevyClient).getWorkoutCount())?.workout_count ?? 0 });
|
|
1027
1200
|
}, "get-workout-count"));
|
|
1028
1201
|
const getWorkoutEventsSchema = {
|
|
1029
1202
|
page: z.coerce.number().int().gte(1).default(1),
|
|
@@ -1031,9 +1204,9 @@ function registerWorkoutTools(server, hevyClient) {
|
|
|
1031
1204
|
since: z.string().default("1970-01-01T00:00:00Z")
|
|
1032
1205
|
};
|
|
1033
1206
|
server.tool("get-workout-events", "Retrieve a paged list of workout events (updates or deletes) since a given date. Events are ordered from newest to oldest. The intention is to allow clients to keep their local cache of workouts up to date without having to fetch the entire list of workouts.", getWorkoutEventsSchema, readOnlyAnnotations("Get Workout Events"), withErrorHandling(async (args) => {
|
|
1034
|
-
|
|
1207
|
+
const client = requireClient(hevyClient);
|
|
1035
1208
|
const { page, pageSize, since } = args;
|
|
1036
|
-
const events = (await
|
|
1209
|
+
const events = (await client.getWorkoutEvents({
|
|
1037
1210
|
page,
|
|
1038
1211
|
pageSize,
|
|
1039
1212
|
since
|
|
@@ -1052,12 +1225,7 @@ function registerWorkoutTools(server, hevyClient) {
|
|
|
1052
1225
|
supersetId: z.coerce.number().nullable().optional(),
|
|
1053
1226
|
notes: z.string().optional().nullable(),
|
|
1054
1227
|
sets: z.array(z.object({
|
|
1055
|
-
type:
|
|
1056
|
-
"warmup",
|
|
1057
|
-
"normal",
|
|
1058
|
-
"failure",
|
|
1059
|
-
"dropset"
|
|
1060
|
-
]).default("normal"),
|
|
1228
|
+
type: setTypeEnum,
|
|
1061
1229
|
weight: z.coerce.number().optional().nullable(),
|
|
1062
1230
|
weightKg: z.coerce.number().optional().nullable(),
|
|
1063
1231
|
reps: z.coerce.number().int().optional().nullable(),
|
|
@@ -1071,7 +1239,7 @@ function registerWorkoutTools(server, hevyClient) {
|
|
|
1071
1239
|
})))
|
|
1072
1240
|
};
|
|
1073
1241
|
server.tool("create-workout", "Create a new workout in your Hevy account. Requires title, start/end times, and at least one exercise with sets. Returns the complete workout details upon successful creation including the newly assigned workout ID.", createWorkoutSchema, createAnnotations("Create Workout"), withErrorHandling(async (args) => {
|
|
1074
|
-
|
|
1242
|
+
const client = requireClient(hevyClient);
|
|
1075
1243
|
const { title, description, startTime, endTime, isPrivate, exercises } = args;
|
|
1076
1244
|
const requestBody = { workout: {
|
|
1077
1245
|
title,
|
|
@@ -1094,7 +1262,7 @@ function registerWorkoutTools(server, hevyClient) {
|
|
|
1094
1262
|
}))
|
|
1095
1263
|
}))
|
|
1096
1264
|
} };
|
|
1097
|
-
const data = await
|
|
1265
|
+
const data = await client.createWorkout(requestBody);
|
|
1098
1266
|
if (!data) return createEmptyResponse("Failed to create workout: Server returned no data");
|
|
1099
1267
|
return createJsonResponse(formatWorkout(data), {
|
|
1100
1268
|
pretty: true,
|
|
@@ -1113,12 +1281,7 @@ function registerWorkoutTools(server, hevyClient) {
|
|
|
1113
1281
|
supersetId: z.coerce.number().nullable().optional(),
|
|
1114
1282
|
notes: z.string().optional().nullable(),
|
|
1115
1283
|
sets: z.array(z.object({
|
|
1116
|
-
type:
|
|
1117
|
-
"warmup",
|
|
1118
|
-
"normal",
|
|
1119
|
-
"failure",
|
|
1120
|
-
"dropset"
|
|
1121
|
-
]).default("normal"),
|
|
1284
|
+
type: setTypeEnum,
|
|
1122
1285
|
weight: z.coerce.number().optional().nullable(),
|
|
1123
1286
|
weightKg: z.coerce.number().optional().nullable(),
|
|
1124
1287
|
reps: z.coerce.number().int().optional().nullable(),
|
|
@@ -1132,7 +1295,7 @@ function registerWorkoutTools(server, hevyClient) {
|
|
|
1132
1295
|
})))
|
|
1133
1296
|
};
|
|
1134
1297
|
server.tool("update-workout", "Update an existing workout by ID. You can modify the title, description, start/end times, privacy setting, and exercise data. Returns the updated workout with all changes applied.", updateWorkoutSchema, updateAnnotations("Update Workout"), withErrorHandling(async (args) => {
|
|
1135
|
-
|
|
1298
|
+
const client = requireClient(hevyClient);
|
|
1136
1299
|
const { workoutId, title, description, startTime, endTime, isPrivate, exercises } = args;
|
|
1137
1300
|
const requestBody = { workout: {
|
|
1138
1301
|
title,
|
|
@@ -1155,7 +1318,7 @@ function registerWorkoutTools(server, hevyClient) {
|
|
|
1155
1318
|
}))
|
|
1156
1319
|
}))
|
|
1157
1320
|
} };
|
|
1158
|
-
const data = await
|
|
1321
|
+
const data = await client.updateWorkout(workoutId, requestBody);
|
|
1159
1322
|
if (!data) return createEmptyResponse(`Failed to update workout with ID ${workoutId}`);
|
|
1160
1323
|
return createJsonResponse(formatWorkout(data), {
|
|
1161
1324
|
pretty: true,
|
|
@@ -1165,16 +1328,23 @@ function registerWorkoutTools(server, hevyClient) {
|
|
|
1165
1328
|
}
|
|
1166
1329
|
//#endregion
|
|
1167
1330
|
//#region src/utils/config.ts
|
|
1331
|
+
const DEPRECATED_CLI_ARGUMENT_WARNING = [
|
|
1332
|
+
"DEPRECATION WARNING: Passing the Hevy API key via CLI arguments",
|
|
1333
|
+
"(--hevy-api-key=..., --hevyApiKey=..., hevy-api-key=...) is",
|
|
1334
|
+
"deprecated and insecure. Use the HEVY_API_KEY environment",
|
|
1335
|
+
"variable instead."
|
|
1336
|
+
].join(" ");
|
|
1168
1337
|
/**
|
|
1169
1338
|
* Parse CLI arguments and environment to derive configuration.
|
|
1170
|
-
* Priority order for API key: CLI flag forms > environment variable.
|
|
1171
|
-
* Supported CLI arg forms:
|
|
1339
|
+
* Priority order for API key: deprecated CLI flag forms > environment variable.
|
|
1340
|
+
* Supported deprecated CLI arg forms:
|
|
1172
1341
|
* --hevy-api-key=KEY
|
|
1173
1342
|
* --hevyApiKey=KEY
|
|
1174
1343
|
* hevy-api-key=KEY (bare, e.g. when passed after npm start -- )
|
|
1175
1344
|
*/
|
|
1176
1345
|
function parseConfig(argv, env) {
|
|
1177
1346
|
let apiKey = "";
|
|
1347
|
+
let usedDeprecatedApiKeyArg = false;
|
|
1178
1348
|
const apiKeyArgPatterns = [
|
|
1179
1349
|
/^--hevy-api-key=(.+)$/i,
|
|
1180
1350
|
/^--hevyApiKey=(.+)$/i,
|
|
@@ -1185,17 +1355,19 @@ function parseConfig(argv, env) {
|
|
|
1185
1355
|
const m = raw.match(pattern);
|
|
1186
1356
|
if (m) {
|
|
1187
1357
|
apiKey = m[1];
|
|
1358
|
+
usedDeprecatedApiKeyArg = true;
|
|
1188
1359
|
break;
|
|
1189
1360
|
}
|
|
1190
1361
|
}
|
|
1191
1362
|
if (apiKey) break;
|
|
1192
1363
|
}
|
|
1364
|
+
if (usedDeprecatedApiKeyArg) console.error(DEPRECATED_CLI_ARGUMENT_WARNING);
|
|
1193
1365
|
if (!apiKey) apiKey = env.HEVY_API_KEY || "";
|
|
1194
1366
|
return { apiKey };
|
|
1195
1367
|
}
|
|
1196
1368
|
function assertApiKey(apiKey) {
|
|
1197
1369
|
if (!apiKey) {
|
|
1198
|
-
console.error("Hevy API key is required. Provide it via the HEVY_API_KEY environment variable
|
|
1370
|
+
console.error("Hevy API key is required. Provide it via the HEVY_API_KEY environment variable.");
|
|
1199
1371
|
process.exit(1);
|
|
1200
1372
|
}
|
|
1201
1373
|
}
|
|
@@ -1794,6 +1966,134 @@ async function putV1WorkoutsWorkoutid(workoutId, data, headers, config = {}) {
|
|
|
1794
1966
|
function wrapApi(fn) {
|
|
1795
1967
|
return fn;
|
|
1796
1968
|
}
|
|
1969
|
+
const DEFAULT_API_TIMEOUT_MS = 3e4;
|
|
1970
|
+
const RETRY_BACKOFF_MAX_MS = 5e3;
|
|
1971
|
+
const HEVY_RETRY_EXHAUSTED_ERROR_CODE = "HEVY_RETRY_EXHAUSTED";
|
|
1972
|
+
const RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
1973
|
+
408,
|
|
1974
|
+
429,
|
|
1975
|
+
500,
|
|
1976
|
+
502,
|
|
1977
|
+
503,
|
|
1978
|
+
504
|
|
1979
|
+
]);
|
|
1980
|
+
const RETRYABLE_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1981
|
+
"ECONNABORTED",
|
|
1982
|
+
"ECONNREFUSED",
|
|
1983
|
+
"ECONNRESET",
|
|
1984
|
+
"EAI_AGAIN",
|
|
1985
|
+
"ENETUNREACH",
|
|
1986
|
+
"ENOTFOUND",
|
|
1987
|
+
"ERR_NETWORK",
|
|
1988
|
+
"ERR_SOCKET_TIMEOUT",
|
|
1989
|
+
"ETIMEDOUT"
|
|
1990
|
+
]);
|
|
1991
|
+
function getApiTimeoutMs() {
|
|
1992
|
+
const rawValue = process.env.HEVY_MCP_API_TIMEOUT;
|
|
1993
|
+
if (!rawValue) return DEFAULT_API_TIMEOUT_MS;
|
|
1994
|
+
const parsed = Number(rawValue);
|
|
1995
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_API_TIMEOUT_MS;
|
|
1996
|
+
return Math.trunc(parsed);
|
|
1997
|
+
}
|
|
1998
|
+
function sleep(ms) {
|
|
1999
|
+
return new Promise((resolve) => {
|
|
2000
|
+
setTimeout(resolve, ms);
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
function isGetRequest(config) {
|
|
2004
|
+
return config.method?.toUpperCase() === "GET";
|
|
2005
|
+
}
|
|
2006
|
+
function isRetryableStatus(status) {
|
|
2007
|
+
if (status === void 0) return false;
|
|
2008
|
+
return RETRYABLE_STATUS_CODES.has(status);
|
|
2009
|
+
}
|
|
2010
|
+
function isTransientNetworkError(error) {
|
|
2011
|
+
if (error.code === "ERR_CANCELED") return false;
|
|
2012
|
+
if (error.code && RETRYABLE_NETWORK_ERROR_CODES.has(error.code)) return true;
|
|
2013
|
+
if (error.response) return false;
|
|
2014
|
+
const message = error.message.toLowerCase();
|
|
2015
|
+
return message.includes("network") || message.includes("socket hang up") || message.includes("timeout");
|
|
2016
|
+
}
|
|
2017
|
+
function normalizeHeaderValue(value) {
|
|
2018
|
+
if (typeof value === "string") {
|
|
2019
|
+
const trimmed = value.trim();
|
|
2020
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
2021
|
+
}
|
|
2022
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
2023
|
+
if (Array.isArray(value) && value.length > 0) return normalizeHeaderValue(value[0]);
|
|
2024
|
+
}
|
|
2025
|
+
function getHeaderValue(headers, key) {
|
|
2026
|
+
if (!headers || typeof headers !== "object") return;
|
|
2027
|
+
if ("get" in headers && typeof headers.get === "function") return normalizeHeaderValue(headers.get(key));
|
|
2028
|
+
const headerRecord = headers;
|
|
2029
|
+
return normalizeHeaderValue(headerRecord[key] ?? headerRecord[key.toLowerCase()] ?? headerRecord[key.toUpperCase()]);
|
|
2030
|
+
}
|
|
2031
|
+
function parseRetryAfterMs(value) {
|
|
2032
|
+
const numericSeconds = Number(value);
|
|
2033
|
+
if (Number.isFinite(numericSeconds) && numericSeconds >= 0) return Math.min(RETRY_BACKOFF_MAX_MS, Math.round(numericSeconds * 1e3));
|
|
2034
|
+
const dateMillis = Date.parse(value);
|
|
2035
|
+
if (Number.isNaN(dateMillis)) return;
|
|
2036
|
+
return Math.min(RETRY_BACKOFF_MAX_MS, Math.max(0, dateMillis - Date.now()));
|
|
2037
|
+
}
|
|
2038
|
+
function getRetryAfterDelayMs(error) {
|
|
2039
|
+
if (error.response?.status !== 429) return;
|
|
2040
|
+
const retryAfterHeader = getHeaderValue(error.response.headers, "retry-after");
|
|
2041
|
+
if (!retryAfterHeader) return;
|
|
2042
|
+
return parseRetryAfterMs(retryAfterHeader);
|
|
2043
|
+
}
|
|
2044
|
+
function getExponentialBackoffMs(retryAttempt) {
|
|
2045
|
+
const delay = 300 * 2 ** Math.max(0, retryAttempt - 1);
|
|
2046
|
+
return Math.min(RETRY_BACKOFF_MAX_MS, delay);
|
|
2047
|
+
}
|
|
2048
|
+
function getRetryDelayMs(error, retryAttempt) {
|
|
2049
|
+
const exponentialDelay = getExponentialBackoffMs(retryAttempt);
|
|
2050
|
+
const retryAfterDelay = getRetryAfterDelayMs(error);
|
|
2051
|
+
if (retryAfterDelay === void 0) return exponentialDelay;
|
|
2052
|
+
return Math.min(RETRY_BACKOFF_MAX_MS, Math.max(exponentialDelay, retryAfterDelay));
|
|
2053
|
+
}
|
|
2054
|
+
function shouldRetryRequest(config, error) {
|
|
2055
|
+
if (!isGetRequest(config)) return false;
|
|
2056
|
+
return isRetryableStatus(error.response?.status) || isTransientNetworkError(error);
|
|
2057
|
+
}
|
|
2058
|
+
function markRetryExhausted(error, retryCount) {
|
|
2059
|
+
const retryError = error;
|
|
2060
|
+
retryError.hevyRetryExhausted = true;
|
|
2061
|
+
retryError.hevyRetryCount = retryCount;
|
|
2062
|
+
retryError.hevyRetryStatus = error.response?.status;
|
|
2063
|
+
if (retryError.code) retryError.hevyRetryOriginalCode = retryError.code;
|
|
2064
|
+
retryError.code = HEVY_RETRY_EXHAUSTED_ERROR_CODE;
|
|
2065
|
+
}
|
|
2066
|
+
async function requestWithRetries(axiosInstance, config) {
|
|
2067
|
+
let retryCount = 0;
|
|
2068
|
+
while (true) try {
|
|
2069
|
+
return await axiosInstance.request(config);
|
|
2070
|
+
} catch (error) {
|
|
2071
|
+
if (!isAxiosError(error)) throw error;
|
|
2072
|
+
if (!shouldRetryRequest(config, error)) throw error;
|
|
2073
|
+
if (retryCount >= 3) {
|
|
2074
|
+
markRetryExhausted(error, retryCount);
|
|
2075
|
+
throw error;
|
|
2076
|
+
}
|
|
2077
|
+
retryCount += 1;
|
|
2078
|
+
await sleep(getRetryDelayMs(error, retryCount));
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
function createResilientClient(axiosInstance) {
|
|
2082
|
+
let clientConfig = { baseURL: axiosInstance.defaults.baseURL };
|
|
2083
|
+
const resilientClient = (async (config) => {
|
|
2084
|
+
return requestWithRetries(axiosInstance, config);
|
|
2085
|
+
});
|
|
2086
|
+
resilientClient.getConfig = () => ({ ...clientConfig });
|
|
2087
|
+
resilientClient.setConfig = (config) => {
|
|
2088
|
+
clientConfig = {
|
|
2089
|
+
...clientConfig,
|
|
2090
|
+
...config
|
|
2091
|
+
};
|
|
2092
|
+
if (config.baseURL !== void 0) axiosInstance.defaults.baseURL = config.baseURL;
|
|
2093
|
+
return resilientClient.getConfig();
|
|
2094
|
+
};
|
|
2095
|
+
return resilientClient;
|
|
2096
|
+
}
|
|
1797
2097
|
/**
|
|
1798
2098
|
* Extract common request metadata from an Axios config.
|
|
1799
2099
|
* Shared between the success and error response interceptors to avoid
|
|
@@ -1838,6 +2138,7 @@ function finalizeRequestTrace(opts) {
|
|
|
1838
2138
|
function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com") {
|
|
1839
2139
|
const axiosInstance = axios.create({
|
|
1840
2140
|
baseURL: baseUrl,
|
|
2141
|
+
timeout: getApiTimeoutMs(),
|
|
1841
2142
|
headers: { "api-key": apiKey }
|
|
1842
2143
|
});
|
|
1843
2144
|
axiosInstance.interceptors.request.use((config) => {
|
|
@@ -1887,7 +2188,7 @@ function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com") {
|
|
|
1887
2188
|
throw error;
|
|
1888
2189
|
});
|
|
1889
2190
|
const headers = { "api-key": apiKey };
|
|
1890
|
-
const client = axiosInstance;
|
|
2191
|
+
const client = createResilientClient(axiosInstance);
|
|
1891
2192
|
return {
|
|
1892
2193
|
getWorkouts: (params) => wrapApi(getV1Workouts)(headers, params, { client }),
|
|
1893
2194
|
getWorkout: (workoutId) => wrapApi(getV1WorkoutsWorkoutid)(workoutId, headers, { client }),
|
|
@@ -1921,6 +2222,44 @@ function createClient(apiKey, baseUrl) {
|
|
|
1921
2222
|
//#endregion
|
|
1922
2223
|
//#region src/utils/stdio-observability.ts
|
|
1923
2224
|
const UTF8_BOM = "";
|
|
2225
|
+
/**
|
|
2226
|
+
* Adapter boundary around MCP SDK stdio internals.
|
|
2227
|
+
*
|
|
2228
|
+
* MCP SDK v1.29.0 exposes public message-level hooks but does not expose a
|
|
2229
|
+
* public raw-chunk hook on `StdioServerTransport`. To capture chunk metadata,
|
|
2230
|
+
* we currently rely on private internals (`_ondata`, `_readBuffer`, `_buffer`)
|
|
2231
|
+
* in this one place.
|
|
2232
|
+
*
|
|
2233
|
+
* If those internals change in a future SDK release, this adapter should fail
|
|
2234
|
+
* closed and preserve default transport behavior (no instrumentation patching).
|
|
2235
|
+
*/
|
|
2236
|
+
function createSdkPrivateStdioAdapter(transport) {
|
|
2237
|
+
const mutableTransport = transport;
|
|
2238
|
+
return {
|
|
2239
|
+
wrapOnData(onChunk) {
|
|
2240
|
+
const originalOnData = mutableTransport._ondata;
|
|
2241
|
+
if (typeof originalOnData !== "function") return;
|
|
2242
|
+
mutableTransport._ondata = (chunk) => {
|
|
2243
|
+
onChunk(chunk);
|
|
2244
|
+
originalOnData(chunk);
|
|
2245
|
+
};
|
|
2246
|
+
},
|
|
2247
|
+
installReadMessageHook(onReadLine) {
|
|
2248
|
+
const readBuffer = mutableTransport._readBuffer;
|
|
2249
|
+
if (!readBuffer || typeof readBuffer.readMessage !== "function") return false;
|
|
2250
|
+
readBuffer.readMessage = () => {
|
|
2251
|
+
const buffer = readBuffer._buffer;
|
|
2252
|
+
if (!buffer) return null;
|
|
2253
|
+
const index = buffer.indexOf("\n");
|
|
2254
|
+
if (index === -1) return null;
|
|
2255
|
+
const lineBuffer = buffer.subarray(0, index);
|
|
2256
|
+
readBuffer._buffer = buffer.subarray(index + 1);
|
|
2257
|
+
return onReadLine(lineBuffer.toString("utf8").replace(/\r$/, ""));
|
|
2258
|
+
};
|
|
2259
|
+
return true;
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
1924
2263
|
function hasUtf8BomPrefix(chunk) {
|
|
1925
2264
|
return chunk.length >= 3 && chunk[0] === 239 && chunk[1] === 187 && chunk[2] === 191;
|
|
1926
2265
|
}
|
|
@@ -1987,36 +2326,48 @@ function deserializeMessageWithObservability(line, chunkSnapshot) {
|
|
|
1987
2326
|
});
|
|
1988
2327
|
}
|
|
1989
2328
|
function createInstrumentedStdioTransport(transport) {
|
|
1990
|
-
const
|
|
2329
|
+
const privateAdapter = createSdkPrivateStdioAdapter(transport);
|
|
1991
2330
|
let lastChunkSnapshot = {
|
|
1992
2331
|
lastChunkByteLength: 0,
|
|
1993
2332
|
lastChunkStartsWithUtf8Bom: false
|
|
1994
2333
|
};
|
|
1995
|
-
|
|
1996
|
-
if (typeof originalOnData === "function") mutableTransport._ondata = (chunk) => {
|
|
2334
|
+
privateAdapter.wrapOnData((chunk) => {
|
|
1997
2335
|
lastChunkSnapshot = {
|
|
1998
2336
|
lastChunkByteLength: chunk.byteLength,
|
|
1999
2337
|
lastChunkStartsWithUtf8Bom: hasUtf8BomPrefix(chunk)
|
|
2000
2338
|
};
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
const readBuffer = mutableTransport._readBuffer;
|
|
2004
|
-
if (!readBuffer || typeof readBuffer.readMessage !== "function") return transport;
|
|
2005
|
-
readBuffer.readMessage = () => {
|
|
2006
|
-
const buffer = readBuffer._buffer;
|
|
2007
|
-
if (!buffer) return null;
|
|
2008
|
-
const index = buffer.indexOf("\n");
|
|
2009
|
-
if (index === -1) return null;
|
|
2010
|
-
const lineBuffer = buffer.subarray(0, index);
|
|
2011
|
-
readBuffer._buffer = buffer.subarray(index + 1);
|
|
2012
|
-
return deserializeMessageWithObservability(lineBuffer.toString("utf8").replace(/\r$/, ""), lastChunkSnapshot);
|
|
2013
|
-
};
|
|
2339
|
+
});
|
|
2340
|
+
if (!privateAdapter.installReadMessageHook((line) => deserializeMessageWithObservability(line, lastChunkSnapshot))) return transport;
|
|
2014
2341
|
return transport;
|
|
2015
2342
|
}
|
|
2016
2343
|
//#endregion
|
|
2017
2344
|
//#region src/index.ts
|
|
2018
2345
|
const name = name$1;
|
|
2019
2346
|
const version = version$1;
|
|
2347
|
+
const HELP_TEXT = [
|
|
2348
|
+
"Usage:",
|
|
2349
|
+
" hevy-mcp [options]",
|
|
2350
|
+
"",
|
|
2351
|
+
"Options:",
|
|
2352
|
+
" -h, --help Show this help message and exit",
|
|
2353
|
+
" -v, --version Show version and exit",
|
|
2354
|
+
" --hevy-api-key=<api-key> (deprecated, use HEVY_API_KEY env var)",
|
|
2355
|
+
"",
|
|
2356
|
+
"Environment:",
|
|
2357
|
+
" HEVY_API_KEY=<api-key> Hevy API key from Hevy app settings",
|
|
2358
|
+
"",
|
|
2359
|
+
"Examples:",
|
|
2360
|
+
" HEVY_API_KEY=your-key npx hevy-mcp",
|
|
2361
|
+
" npx hevy-mcp --hevy-api-key=your-key",
|
|
2362
|
+
" npm start -- --hevy-api-key=your-key"
|
|
2363
|
+
].join("\n");
|
|
2364
|
+
function getCliAction(args) {
|
|
2365
|
+
for (const arg of args) {
|
|
2366
|
+
if (arg === "--version" || arg === "-v") return "version";
|
|
2367
|
+
if (arg === "--help" || arg === "-h") return "help";
|
|
2368
|
+
}
|
|
2369
|
+
return "start";
|
|
2370
|
+
}
|
|
2020
2371
|
const HEVY_API_BASEURL = "https://api.hevyapp.com";
|
|
2021
2372
|
const SENTRY_USER_ID_CONTEXT = "hevy-mcp:sentry-user-id:v1";
|
|
2022
2373
|
function fingerprintApiKey(apiKey) {
|
|
@@ -2075,10 +2426,20 @@ function createServer({ config }) {
|
|
|
2075
2426
|
return buildServer(apiKey);
|
|
2076
2427
|
}
|
|
2077
2428
|
async function runServer() {
|
|
2429
|
+
const args = process.argv.slice(2);
|
|
2430
|
+
const cliAction = getCliAction(args);
|
|
2431
|
+
if (cliAction === "version") {
|
|
2432
|
+
console.log(version);
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
if (cliAction === "help") {
|
|
2436
|
+
console.log(HELP_TEXT);
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2078
2439
|
serverStartups.add(1, { version });
|
|
2079
2440
|
await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "stdio" } }, async (span) => {
|
|
2080
2441
|
try {
|
|
2081
|
-
const apiKey = parseConfig(
|
|
2442
|
+
const apiKey = parseConfig(args, process.env).apiKey;
|
|
2082
2443
|
assertApiKey(apiKey);
|
|
2083
2444
|
const server = buildServer(apiKey);
|
|
2084
2445
|
console.error("Starting MCP server in stdio mode");
|
|
@@ -2106,4 +2467,4 @@ async function runServer() {
|
|
|
2106
2467
|
//#endregion
|
|
2107
2468
|
export { createServer as n, runServer as r, configSchema as t };
|
|
2108
2469
|
|
|
2109
|
-
//# sourceMappingURL=src-
|
|
2470
|
+
//# sourceMappingURL=src-CuOZcBEo.mjs.map
|