hevy-mcp 1.28.0 → 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.
@@ -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.28.0" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@2.0.0" };
8
8
  var n = new e.Error().stack;
9
- n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "13440721-a791-4fcc-9147-27a915a2863d", e._sentryDebugIdIdentifier = "sentry-dbid-13440721-a791-4fcc-9147-27a915a2863d");
9
+ n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "17d5086b-9850-447d-9372-27bb2ce5dcc5", e._sentryDebugIdIdentifier = "sentry-dbid-17d5086b-9850-447d-9372-27bb2ce5dcc5");
10
10
  } catch (e) {}
11
11
  })();
12
12
  import * as Sentry from "@sentry/node";
@@ -18,13 +18,17 @@ import { resourceFromAttributes } from "@opentelemetry/resources";
18
18
  import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
19
19
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
20
20
  import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
21
- import { createHmac } from "node:crypto";
21
+ import { createHmac, randomUUID } from "node:crypto";
22
22
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
23
23
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
24
24
  import { z } from "zod";
25
25
  import axios, { isAxiosError } from "axios";
26
26
  import { z as z$1 } from "zod/v4";
27
27
  import { deserializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
28
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
29
+ import { homedir } from "node:os";
30
+ import { dirname, join } from "node:path";
31
+ import semver from "semver";
28
32
  //#region src/utils/telemetry.ts
29
33
  /**
30
34
  * Centralized telemetry initialization.
@@ -37,10 +41,10 @@ import { deserializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
37
41
  * OTel Collector → Honeycomb: performance traces, metrics
38
42
  */
39
43
  const name$1 = "hevy-mcp";
40
- const version$1 = "1.28.0";
44
+ const version$1 = "2.0.0";
41
45
  const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
42
46
  const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
43
- const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@1.28.0`;
47
+ const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@2.0.0`;
44
48
  const resource = resourceFromAttributes({
45
49
  "service.name": name$1,
46
50
  "service.version": version$1
@@ -693,6 +697,93 @@ function withErrorHandling(fn, context) {
693
697
  };
694
698
  }
695
699
  //#endregion
700
+ //#region src/utils/debug.ts
701
+ const DEBUG_PREFIX = "[hevy-mcp:debug] ";
702
+ const MAX_DEBUG_RECORD_LENGTH = 8192;
703
+ const MAX_REDACTION_DEPTH = 4;
704
+ const MAX_OBJECT_KEYS = 20;
705
+ function isDebugEnabled(env = process.env) {
706
+ return env.HEVY_MCP_DEBUG === "1";
707
+ }
708
+ function redactValue(value, depth, seen) {
709
+ if (value === null) return "[null]";
710
+ if (typeof value !== "object") return `[${typeof value}]`;
711
+ if (seen.has(value)) return "[circular]";
712
+ if (Array.isArray(value)) {
713
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
714
+ const length = lengthDescriptor && "value" in lengthDescriptor && typeof lengthDescriptor.value === "number" ? lengthDescriptor.value : 0;
715
+ if (depth >= MAX_REDACTION_DEPTH) return {
716
+ type: "array",
717
+ length,
718
+ items: "[max-depth]"
719
+ };
720
+ seen.add(value);
721
+ try {
722
+ const itemCount = Math.min(length, MAX_OBJECT_KEYS);
723
+ const items = {};
724
+ for (let index = 0; index < itemCount; index += 1) {
725
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
726
+ items[`item-${index + 1}`] = descriptor && "value" in descriptor ? redactValue(descriptor.value, depth + 1, seen) : "[empty-or-accessor]";
727
+ }
728
+ return {
729
+ type: "array",
730
+ length,
731
+ items,
732
+ ...length > MAX_OBJECT_KEYS ? { truncatedItems: length - MAX_OBJECT_KEYS } : {}
733
+ };
734
+ } finally {
735
+ seen.delete(value);
736
+ }
737
+ }
738
+ const keys = Object.keys(value);
739
+ if (depth >= MAX_REDACTION_DEPTH) return {
740
+ type: "object",
741
+ fieldCount: keys.length,
742
+ fields: "[max-depth]"
743
+ };
744
+ seen.add(value);
745
+ try {
746
+ const fields = {};
747
+ for (const [index, rawKey] of keys.slice(0, MAX_OBJECT_KEYS).entries()) {
748
+ const descriptor = Object.getOwnPropertyDescriptor(value, rawKey);
749
+ fields[`field-${index + 1}`] = descriptor && "value" in descriptor ? redactValue(descriptor.value, depth + 1, seen) : "[accessor]";
750
+ }
751
+ return {
752
+ type: "object",
753
+ fieldCount: keys.length,
754
+ fields,
755
+ ...keys.length > MAX_OBJECT_KEYS ? { truncatedFields: keys.length - MAX_OBJECT_KEYS } : {}
756
+ };
757
+ } finally {
758
+ seen.delete(value);
759
+ }
760
+ }
761
+ /** Redact every input scalar while preserving bounded argument structure. */
762
+ function redactToolArgs(args) {
763
+ try {
764
+ return redactValue(args, 0, /* @__PURE__ */ new WeakSet());
765
+ } catch {
766
+ return "[unavailable]";
767
+ }
768
+ }
769
+ /**
770
+ * Write a single bounded structured debug record to stderr without throwing.
771
+ */
772
+ function debugLog(event, data) {
773
+ if (!isDebugEnabled()) return;
774
+ try {
775
+ const record = JSON.stringify({
776
+ event,
777
+ ...data
778
+ });
779
+ const output = record.length <= MAX_DEBUG_RECORD_LENGTH ? record : JSON.stringify({
780
+ event,
781
+ truncated: true
782
+ });
783
+ process.stderr.write(`${DEBUG_PREFIX}${output}\n`);
784
+ } catch {}
785
+ }
786
+ //#endregion
696
787
  //#region src/utils/telemetry-wrapper.ts
697
788
  /** Whitelist of safe argument keys that can be logged without exposing PII. */
698
789
  const ARGUMENT_WHITELIST = /* @__PURE__ */ new Set([
@@ -729,6 +820,10 @@ function extractSafeArgs(args) {
729
820
  function withTelemetry(fn, context) {
730
821
  return async (rawArgs) => {
731
822
  const args = rawArgs ?? {};
823
+ if (isDebugEnabled()) debugLog("tool_invocation", {
824
+ tool: context,
825
+ params: redactToolArgs(args)
826
+ });
732
827
  const argumentKeyCount = Object.keys(args).length;
733
828
  const startTime = Date.now();
734
829
  let isError = false;
@@ -786,62 +881,6 @@ function withObservability(fn, context) {
786
881
  return withErrorHandling(withTelemetry(fn, context), context);
787
882
  }
788
883
  //#endregion
789
- //#region src/generated/client/schemas/deletedWorkoutSchema.ts
790
- /**
791
- * Generated by Kubb (https://kubb.dev/).
792
- * Do not edit manually.
793
- */
794
- const deletedWorkoutSchema = z$1.object({
795
- type: z$1.string().describe("Indicates the type of the event (deleted)"),
796
- id: z$1.string().describe("The unique identifier of the deleted workout"),
797
- deleted_at: z$1.optional(z$1.string().describe("A date string indicating when the workout was deleted"))
798
- });
799
- //#endregion
800
- //#region src/generated/client/schemas/workoutSchema.ts
801
- /**
802
- * Generated by Kubb (https://kubb.dev/).
803
- * Do not edit manually.
804
- */
805
- const workoutSchema = z$1.object({
806
- id: z$1.optional(z$1.string().describe("The workout ID.")),
807
- title: z$1.optional(z$1.string().describe("The workout title.")),
808
- routine_id: z$1.optional(z$1.string().describe("The ID of the routine that this workout belongs to.")),
809
- description: z$1.optional(z$1.string().describe("The workout description.")),
810
- start_time: z$1.optional(z$1.string().describe("ISO 8601 timestamp of when the workout was recorded to have started.")),
811
- end_time: z$1.optional(z$1.string().describe("ISO 8601 timestamp of when the workout was recorded to have ended.")),
812
- updated_at: z$1.optional(z$1.string().describe("ISO 8601 timestamp of when the workout was last updated.")),
813
- created_at: z$1.optional(z$1.string().describe("ISO 8601 timestamp of when the workout was created.")),
814
- exercises: z$1.optional(z$1.array(z$1.object({
815
- index: z$1.optional(z$1.number().describe("Index indicating the order of the exercise in the workout.")),
816
- title: z$1.optional(z$1.string().describe("Title of the exercise")),
817
- notes: z$1.optional(z$1.string().describe("Notes on the exercise")),
818
- exercise_template_id: z$1.optional(z$1.string().describe("The id of the exercise template. This can be used to fetch the exercise template.")),
819
- supersets_id: z$1.number().describe("The id of the superset that the exercise belongs to. A value of null indicates the exercise is not part of a superset.").nullish(),
820
- sets: z$1.optional(z$1.array(z$1.object({
821
- index: z$1.optional(z$1.number().describe("Index indicating the order of the set in the workout.")),
822
- type: z$1.optional(z$1.string().describe("The type of set. This can be one of 'normal', 'warmup', 'dropset', 'failure'")),
823
- weight_kg: z$1.number().describe("Weight lifted in kilograms.").nullish(),
824
- reps: z$1.number().describe("Number of reps logged for the set").nullish(),
825
- distance_meters: z$1.number().describe("Number of meters logged for the set").nullish(),
826
- duration_seconds: z$1.number().describe("Number of seconds logged for the set").nullish(),
827
- rpe: z$1.number().describe("RPE (Relative perceived exertion) value logged for the set").nullish(),
828
- custom_metric: z$1.number().describe("Custom metric logged for the set (Currently only used to log floors or steps for stair machine exercises)").nullish()
829
- })))
830
- })))
831
- });
832
- //#endregion
833
- //#region src/generated/client/schemas/updatedWorkoutSchema.ts
834
- /**
835
- * Generated by Kubb (https://kubb.dev/).
836
- * Do not edit manually.
837
- */
838
- const updatedWorkoutSchema = z$1.object({
839
- type: z$1.string().describe("Indicates the type of the event (updated)"),
840
- get workout() {
841
- return workoutSchema;
842
- }
843
- });
844
- //#endregion
845
884
  //#region src/generated/client/schemas/userInfoSchema.ts
846
885
  /**
847
886
  * Generated by Kubb (https://kubb.dev/).
@@ -966,7 +1005,16 @@ const formattedBodyMeasurementSchema = z.object({
966
1005
  const workoutsOutputSchema = { workouts: z.array(formattedWorkoutSchema) };
967
1006
  const workoutOutputSchema = { workout: formattedWorkoutSchema.nullable() };
968
1007
  const workoutCountOutputSchema = { count: z.number().int() };
969
- const workoutEventsOutputSchema = { events: z.array(z.union([updatedWorkoutSchema, deletedWorkoutSchema])) };
1008
+ const formattedUpdatedWorkoutSchema = z.object({
1009
+ type: z.literal("updated"),
1010
+ workout: formattedWorkoutSchema
1011
+ });
1012
+ const formattedDeletedWorkoutSchema = z.object({
1013
+ type: z.literal("deleted"),
1014
+ id: z.string(),
1015
+ deletedAt: z.string().optional()
1016
+ });
1017
+ const workoutEventsOutputSchema = { events: z.array(z.union([formattedUpdatedWorkoutSchema, formattedDeletedWorkoutSchema])) };
970
1018
  const routinesOutputSchema = { routines: z.array(formattedRoutineSchema) };
971
1019
  const routineOutputSchema = { routine: formattedRoutineSchema.nullable() };
972
1020
  const exerciseTemplatesOutputSchema = { exerciseTemplates: z.array(formattedExerciseTemplateSchema) };
@@ -1074,6 +1122,16 @@ function updateAnnotations(title) {
1074
1122
  };
1075
1123
  }
1076
1124
  //#endregion
1125
+ //#region src/utils/tool-descriptions.ts
1126
+ function describeTool({ summary, aliases, useCase, importantNotes }) {
1127
+ return [
1128
+ summary,
1129
+ `Aliases: ${aliases.join(", ")}.`,
1130
+ `<use_case>${useCase}</use_case>`,
1131
+ `<important_notes>${importantNotes}</important_notes>`
1132
+ ].join(" ");
1133
+ }
1134
+ //#endregion
1077
1135
  //#region src/utils/schemas.ts
1078
1136
  function coerceNullishNumberInput(value) {
1079
1137
  if (value === null || value === void 0) return value;
@@ -1196,7 +1254,16 @@ function registerBodyMeasurementTools(server, hevyClient) {
1196
1254
  pageSize: z.coerce.number().int().gte(1).lte(10).default(10)
1197
1255
  };
1198
1256
  server.registerTool("get-body-measurements", {
1199
- description: "Get a paginated list of body measurements for the authenticated user. Returns measurements including weight, body fat, and various circumference measurements.",
1257
+ description: describeTool({
1258
+ summary: "Read-only. Lists dated body measurements for the account.",
1259
+ aliases: [
1260
+ "body stats history",
1261
+ "list weigh-ins",
1262
+ "measurement log"
1263
+ ],
1264
+ useCase: "Use to browse measurement history; use get-body-measurement for one exact date.",
1265
+ importantNotes: "Results are paginated; page starts at 1 and pageSize is limited to 10."
1266
+ }),
1200
1267
  inputSchema: getBodyMeasurementsSchema,
1201
1268
  outputSchema: bodyMeasurementsOutputSchema,
1202
1269
  annotations: readOnlyAnnotations("Get Body Measurements")
@@ -1212,7 +1279,16 @@ function registerBodyMeasurementTools(server, hevyClient) {
1212
1279
  }, "get-body-measurements"));
1213
1280
  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)") };
1214
1281
  server.registerTool("get-body-measurement", {
1215
- description: "Get a single body measurement by date. Returns all measurement fields for the specified date.",
1282
+ description: describeTool({
1283
+ summary: "Read-only. Retrieves the body measurement entry for one date.",
1284
+ aliases: [
1285
+ "get weigh-in",
1286
+ "show body stats",
1287
+ "measurement by date"
1288
+ ],
1289
+ useCase: "Use when the exact measurement date is known; use get-body-measurements to browse dates.",
1290
+ importantNotes: "date must use YYYY-MM-DD; at most one entry exists per date."
1291
+ }),
1216
1292
  inputSchema: getBodyMeasurementSchema,
1217
1293
  outputSchema: bodyMeasurementOutputSchema,
1218
1294
  annotations: readOnlyAnnotations("Get Body Measurement")
@@ -1228,7 +1304,16 @@ function registerBodyMeasurementTools(server, hevyClient) {
1228
1304
  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). Must be unique — returns 409 if an entry already exists for this date."),
1229
1305
  ...bodyMeasurementFieldsSchema
1230
1306
  };
1231
- 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"), withObservability(async (args) => {
1307
+ server.tool("create-body-measurement", describeTool({
1308
+ summary: "Writes to the Hevy account by creating a dated body measurement.",
1309
+ aliases: [
1310
+ "log weigh-in",
1311
+ "add body stats",
1312
+ "record measurements"
1313
+ ],
1314
+ useCase: "Use for a date without an entry; use update-body-measurement when that date already exists.",
1315
+ importantNotes: "date must use YYYY-MM-DD and be unique. Null fields are omitted and cannot clear values; an existing date returns 409."
1316
+ }), createBodyMeasurementSchema, createAnnotations("Create Body Measurement"), withObservability(async (args) => {
1232
1317
  const client = requireClient(hevyClient);
1233
1318
  const { date, ...fields } = args;
1234
1319
  await client.createBodyMeasurement({
@@ -1241,7 +1326,16 @@ function registerBodyMeasurementTools(server, hevyClient) {
1241
1326
  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 to update (YYYY-MM-DD). Must already exist — returns 404 otherwise."),
1242
1327
  ...bodyMeasurementFieldsSchema
1243
1328
  };
1244
- 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"), withObservability(async (args) => {
1329
+ server.tool("update-body-measurement", describeTool({
1330
+ summary: "Mutates the Hevy account by updating a body measurement for a date.",
1331
+ aliases: [
1332
+ "edit weigh-in",
1333
+ "correct body stats",
1334
+ "change measurements"
1335
+ ],
1336
+ useCase: "Use to change fields on an existing date; use create-body-measurement for a new date.",
1337
+ importantNotes: "date must use YYYY-MM-DD and already exist. Provide at least one numeric field; nulls are omitted and cannot clear stored values."
1338
+ }), updateBodyMeasurementSchema, updateAnnotations("Update Body Measurement"), withObservability(async (args) => {
1245
1339
  const client = requireClient(hevyClient);
1246
1340
  const { date, ...fields } = args;
1247
1341
  const payload = buildMeasurementPayload(fields);
@@ -1261,7 +1355,16 @@ function registerFolderTools(server, hevyClient) {
1261
1355
  pageSize: z.coerce.number().int().gte(1).lte(10).default(5)
1262
1356
  };
1263
1357
  server.registerTool("get-routine-folders", {
1264
- description: "Get a paginated list of your routine folders, including both default and custom folders. Useful for organizing and browsing your workout routines.",
1358
+ description: describeTool({
1359
+ summary: "Read-only. Lists default and custom routine folders.",
1360
+ aliases: [
1361
+ "list folders",
1362
+ "browse routine groups",
1363
+ "show plan folders"
1364
+ ],
1365
+ useCase: "Use to browse folder organization or discover folder IDs for routine creation.",
1366
+ importantNotes: "Results are paginated; page starts at 1 and pageSize is limited to 10."
1367
+ }),
1265
1368
  inputSchema: getRoutineFoldersSchema,
1266
1369
  outputSchema: routineFoldersOutputSchema,
1267
1370
  annotations: readOnlyAnnotations("Get Routine Folders")
@@ -1277,7 +1380,16 @@ function registerFolderTools(server, hevyClient) {
1277
1380
  }, "get-routine-folders"));
1278
1381
  const getRoutineFolderSchema = { folderId: z.string().min(1) };
1279
1382
  server.registerTool("get-routine-folder", {
1280
- description: "Get complete details of a specific routine folder by its ID, including name, creation date, and associated routines.",
1383
+ description: describeTool({
1384
+ summary: "Read-only. Retrieves one routine folder's metadata by ID.",
1385
+ aliases: [
1386
+ "show folder",
1387
+ "folder details",
1388
+ "routine folder metadata"
1389
+ ],
1390
+ useCase: "Use for one known folder; use get-routine-folders to browse or discover folder IDs.",
1391
+ importantNotes: "Requires a folderId from get-routine-folders or a prior create response."
1392
+ }),
1281
1393
  inputSchema: getRoutineFolderSchema,
1282
1394
  outputSchema: routineFolderOutputSchema,
1283
1395
  annotations: readOnlyAnnotations("Get Routine Folder")
@@ -1290,7 +1402,16 @@ function registerFolderTools(server, hevyClient) {
1290
1402
  return createStructuredJsonResponse(folder, { routineFolder: folder });
1291
1403
  }, "get-routine-folder"));
1292
1404
  const createRoutineFolderSchema = { name: z.string().min(1) };
1293
- 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"), withObservability(async (args) => {
1405
+ server.tool("create-routine-folder", describeTool({
1406
+ summary: "Writes to the Hevy account by creating a new routine folder.",
1407
+ aliases: [
1408
+ "add folder",
1409
+ "organize routines",
1410
+ "create plan group"
1411
+ ],
1412
+ useCase: "Use to create an organizational destination before assigning new routines to a folderId.",
1413
+ importantNotes: "Requires a non-empty name. Retrying or reusing a name can create duplicate folders."
1414
+ }), createRoutineFolderSchema, createAnnotations("Create Routine Folder"), withObservability(async (args) => {
1294
1415
  const client = requireClient(hevyClient);
1295
1416
  const { name } = args;
1296
1417
  const data = await client.createRoutineFolder({ routine_folder: { title: name } });
@@ -1355,7 +1476,16 @@ function registerRoutineTools(server, hevyClient) {
1355
1476
  pageSize: z.coerce.number().int().gte(1).lte(10).default(5)
1356
1477
  };
1357
1478
  server.registerTool("get-routines", {
1358
- description: "Get a paginated list of your workout routines, including custom and default routines. Useful for browsing or searching your available routines.",
1479
+ description: describeTool({
1480
+ summary: "Read-only. Lists custom and default workout routines.",
1481
+ aliases: [
1482
+ "list routines",
1483
+ "show workout plans",
1484
+ "browse saved routines"
1485
+ ],
1486
+ useCase: "Use to browse routines or discover a routine ID; use get-routine for one known routine.",
1487
+ importantNotes: "Results are paginated; page starts at 1 and pageSize is limited to 10."
1488
+ }),
1359
1489
  inputSchema: getRoutinesSchema,
1360
1490
  outputSchema: routinesOutputSchema,
1361
1491
  annotations: readOnlyAnnotations("Get Routines")
@@ -1371,7 +1501,16 @@ function registerRoutineTools(server, hevyClient) {
1371
1501
  }, "get-routines"));
1372
1502
  const getRoutineSchema = { routineId: z.string().min(1) };
1373
1503
  server.registerTool("get-routine", {
1374
- description: "Get a routine by its ID using the direct endpoint. Returns all details for the specified routine.",
1504
+ description: describeTool({
1505
+ summary: "Read-only. Retrieves one routine and its exercise configuration by ID.",
1506
+ aliases: [
1507
+ "show routine",
1508
+ "fetch workout plan",
1509
+ "routine details"
1510
+ ],
1511
+ useCase: "Use when the routineId is known; use get-routines to browse or discover IDs.",
1512
+ importantNotes: "Requires a routineId from get-routines or a prior create response."
1513
+ }),
1375
1514
  inputSchema: getRoutineSchema,
1376
1515
  outputSchema: routineOutputSchema,
1377
1516
  annotations: readOnlyAnnotations("Get Routine")
@@ -1406,7 +1545,16 @@ function registerRoutineTools(server, hevyClient) {
1406
1545
  }))
1407
1546
  })))
1408
1547
  };
1409
- 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"), withObservability(async (args) => {
1548
+ server.tool("create-routine", describeTool({
1549
+ summary: "Writes to the Hevy account by creating a new workout routine.",
1550
+ aliases: [
1551
+ "add routine",
1552
+ "build workout plan",
1553
+ "save training template"
1554
+ ],
1555
+ useCase: "Use to create a reusable plan; use create-workout to log a completed session.",
1556
+ importantNotes: "Requires exercise template IDs; folderId is optional. Retrying can create duplicates, and non-fixed rep ranges may not display in Hevy apps."
1557
+ }), createRoutineSchema, createAnnotations("Create Routine"), withObservability(async (args) => {
1410
1558
  const client = requireClient(hevyClient);
1411
1559
  const { title, folderId, notes, exercises } = args;
1412
1560
  let usesRepRanges = false;
@@ -1473,7 +1621,16 @@ function registerRoutineTools(server, hevyClient) {
1473
1621
  }))
1474
1622
  })))
1475
1623
  };
1476
- 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"), withObservability(async (args) => {
1624
+ server.tool("update-routine", describeTool({
1625
+ summary: "Mutates the Hevy account by replacing an existing routine's content.",
1626
+ aliases: [
1627
+ "edit routine",
1628
+ "revise workout plan",
1629
+ "replace routine exercises"
1630
+ ],
1631
+ useCase: "Use to change a known routine; use create-routine for a separate new plan.",
1632
+ importantNotes: "Requires routineId and the complete title and exercises payload; omitted exercises are removed. Non-fixed rep ranges may not display in Hevy apps."
1633
+ }), updateRoutineSchema, updateAnnotations("Update Routine"), withObservability(async (args) => {
1477
1634
  const client = requireClient(hevyClient);
1478
1635
  const { routineId, title, notes, exercises } = args;
1479
1636
  let usesRepRanges = false;
@@ -1529,7 +1686,16 @@ function registerTemplateTools(server, hevyClient, options = {}) {
1529
1686
  pageSize: z.coerce.number().int().gte(1).lte(100).default(5)
1530
1687
  };
1531
1688
  server.registerTool("get-exercise-templates", {
1532
- description: "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.",
1689
+ description: describeTool({
1690
+ summary: "Read-only. Lists default and custom exercise templates with equipment and muscle metadata.",
1691
+ aliases: [
1692
+ "browse exercises",
1693
+ "list exercise catalog",
1694
+ "show movements"
1695
+ ],
1696
+ useCase: "Use for page-by-page catalog browsing; use search-exercise-templates for a name lookup across the full catalog.",
1697
+ importantNotes: "Results are paginated; page starts at 1 and pageSize is limited to 100."
1698
+ }),
1533
1699
  inputSchema: getExerciseTemplatesSchema,
1534
1700
  outputSchema: exerciseTemplatesOutputSchema,
1535
1701
  annotations: readOnlyAnnotations("Get Exercise Templates")
@@ -1545,7 +1711,16 @@ function registerTemplateTools(server, hevyClient, options = {}) {
1545
1711
  }, "get-exercise-templates"));
1546
1712
  const getExerciseTemplateSchema = { exerciseTemplateId: z.string().min(1) };
1547
1713
  server.registerTool("get-exercise-template", {
1548
- description: "Get complete details of a specific exercise template by its ID, including name, category, equipment, muscle groups, and notes.",
1714
+ description: describeTool({
1715
+ summary: "Read-only. Retrieves complete metadata for one exercise template by ID.",
1716
+ aliases: [
1717
+ "show exercise details",
1718
+ "fetch movement",
1719
+ "exercise template info"
1720
+ ],
1721
+ useCase: "Use after locating an exact template; use search-exercise-templates when only a name is known.",
1722
+ importantNotes: "Requires an exerciseTemplateId from a template list, search, routine, or workout."
1723
+ }),
1549
1724
  inputSchema: getExerciseTemplateSchema,
1550
1725
  outputSchema: exerciseTemplateOutputSchema,
1551
1726
  annotations: readOnlyAnnotations("Get Exercise Template")
@@ -1563,7 +1738,16 @@ function registerTemplateTools(server, hevyClient, options = {}) {
1563
1738
  endDate: z.string().datetime({ offset: true }).describe("ISO 8601 end date for filtering history").optional()
1564
1739
  };
1565
1740
  server.registerTool("get-exercise-history", {
1566
- description: "Get past sets for a specific exercise template, optionally filtered by start and end dates.",
1741
+ description: describeTool({
1742
+ summary: "Read-only. Retrieves past performed sets for one exercise template.",
1743
+ aliases: [
1744
+ "exercise progress",
1745
+ "past sets",
1746
+ "movement history"
1747
+ ],
1748
+ useCase: "Use to analyze performance for one movement; use get-workouts for complete sessions.",
1749
+ importantNotes: "Requires an exerciseTemplateId. Optional startDate and endDate must be ISO 8601 datetimes with an offset."
1750
+ }),
1567
1751
  inputSchema: getExerciseHistorySchema,
1568
1752
  outputSchema: exerciseHistoryOutputSchema,
1569
1753
  annotations: readOnlyAnnotations("Get Exercise History")
@@ -1584,7 +1768,16 @@ function registerTemplateTools(server, hevyClient, options = {}) {
1584
1768
  muscleGroup: muscleGroupEnum,
1585
1769
  otherMuscles: z.array(muscleGroupEnum).default([])
1586
1770
  };
1587
- server.tool("create-exercise-template", "Create a custom exercise template with title, type, equipment, and muscle groups.", createExerciseTemplateSchema, createAnnotations("Create Exercise Template"), withObservability(async (args) => {
1771
+ server.tool("create-exercise-template", describeTool({
1772
+ summary: "Writes to the Hevy account by creating a custom exercise template.",
1773
+ aliases: [
1774
+ "add custom exercise",
1775
+ "create movement",
1776
+ "define exercise"
1777
+ ],
1778
+ useCase: "Use only when the needed movement is absent; search-exercise-templates should check existing templates first.",
1779
+ importantNotes: "Requires title, exercise type, equipment category, and primary muscle group. Retrying or reusing a title can create duplicates."
1780
+ }), createExerciseTemplateSchema, createAnnotations("Create Exercise Template"), withObservability(async (args) => {
1588
1781
  const client = requireClient(hevyClient);
1589
1782
  const { title, exerciseType, equipmentCategory, muscleGroup, otherMuscles } = args;
1590
1783
  return createJsonResponse({
@@ -1604,7 +1797,16 @@ function registerTemplateTools(server, hevyClient, options = {}) {
1604
1797
  refresh: z.boolean().optional().default(false).describe("Set to true to invalidate the catalog cache and re-fetch all templates from the API")
1605
1798
  };
1606
1799
  server.registerTool("search-exercise-templates", {
1607
- description: "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.",
1800
+ description: describeTool({
1801
+ summary: "Read-only for the Hevy account. Searches the full exercise template catalog by title substring.",
1802
+ aliases: [
1803
+ "find exercise",
1804
+ "look up movement",
1805
+ "search exercise IDs"
1806
+ ],
1807
+ useCase: "Use when a name or partial name is known, especially to discover IDs for workouts and routines; use get-exercise-templates for page browsing.",
1808
+ importantNotes: "Matching is case-insensitive. The catalog is cached locally for 5 minutes; refresh:true re-fetches all pages and changes only local cache state."
1809
+ }),
1608
1810
  inputSchema: searchExerciseTemplatesSchema,
1609
1811
  outputSchema: exerciseTemplatesOutputSchema,
1610
1812
  annotations: readOnlyAnnotations("Search Exercise Templates")
@@ -1641,7 +1843,16 @@ function registerTemplateTools(server, hevyClient, options = {}) {
1641
1843
  //#region src/tools/user.ts
1642
1844
  function registerUserTools(server, hevyClient) {
1643
1845
  server.registerTool("get-user-info", {
1644
- description: "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.",
1846
+ description: describeTool({
1847
+ summary: "Read-only. Returns the authenticated account's user ID, display name, and public profile URL.",
1848
+ aliases: [
1849
+ "who am I",
1850
+ "account info",
1851
+ "verify Hevy user"
1852
+ ],
1853
+ useCase: "Use to confirm which Hevy account is connected before reading or writing account data.",
1854
+ importantNotes: "Accepts no inputs and reports only the account associated with the configured credentials."
1855
+ }),
1645
1856
  inputSchema: {},
1646
1857
  outputSchema: userOutputSchema,
1647
1858
  annotations: readOnlyAnnotations("Get User Info")
@@ -1653,6 +1864,18 @@ function registerUserTools(server, hevyClient) {
1653
1864
  }
1654
1865
  //#endregion
1655
1866
  //#region src/tools/workouts.ts
1867
+ function formatWorkoutEvent(event) {
1868
+ if (event.type === "updated" && "workout" in event) return {
1869
+ type: "updated",
1870
+ workout: formatWorkout(event.workout)
1871
+ };
1872
+ if (event.type === "deleted" && "id" in event) return {
1873
+ type: "deleted",
1874
+ id: event.id,
1875
+ deletedAt: event.deleted_at
1876
+ };
1877
+ throw new Error(`Unsupported workout event type: ${event.type}`);
1878
+ }
1656
1879
  /**
1657
1880
  * Register all workout-related tools with the MCP server
1658
1881
  */
@@ -1662,7 +1885,16 @@ function registerWorkoutTools(server, hevyClient) {
1662
1885
  pageSize: z.coerce.number().int().gte(1).lte(10).default(5)
1663
1886
  };
1664
1887
  server.registerTool("get-workouts", {
1665
- description: "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.",
1888
+ description: describeTool({
1889
+ summary: "Read-only. Lists workouts from newest to oldest with exercise and timing details.",
1890
+ aliases: [
1891
+ "list workout history",
1892
+ "show recent workouts",
1893
+ "browse logs"
1894
+ ],
1895
+ useCase: "Use to browse or page through workout history; use get-workout when a workout ID is already known.",
1896
+ importantNotes: "Results are paginated; page starts at 1 and pageSize is limited to 10."
1897
+ }),
1666
1898
  inputSchema: getWorkoutsSchema,
1667
1899
  outputSchema: workoutsOutputSchema,
1668
1900
  annotations: readOnlyAnnotations("Get Workouts")
@@ -1678,7 +1910,16 @@ function registerWorkoutTools(server, hevyClient) {
1678
1910
  }, "get-workouts"));
1679
1911
  const getWorkoutSchema = { workoutId: z.string().min(1) };
1680
1912
  server.registerTool("get-workout", {
1681
- description: "Get complete details of a specific workout by ID. Returns all workout information including title, description, start/end times, and detailed exercise data.",
1913
+ description: describeTool({
1914
+ summary: "Read-only. Retrieves complete details for one workout by its ID.",
1915
+ aliases: [
1916
+ "show workout",
1917
+ "fetch workout details",
1918
+ "open workout log"
1919
+ ],
1920
+ useCase: "Use after get-workouts identifies the exact workout; do not use for browsing multiple workouts.",
1921
+ importantNotes: "Requires a workoutId discovered from a workout list, event, or prior create response."
1922
+ }),
1682
1923
  inputSchema: getWorkoutSchema,
1683
1924
  outputSchema: workoutOutputSchema,
1684
1925
  annotations: readOnlyAnnotations("Get Workout")
@@ -1691,7 +1932,16 @@ function registerWorkoutTools(server, hevyClient) {
1691
1932
  return createStructuredJsonResponse(workout, { workout });
1692
1933
  }, "get-workout"));
1693
1934
  server.registerTool("get-workout-count", {
1694
- description: "Get the total number of workouts on the account. Useful for pagination or statistics.",
1935
+ description: describeTool({
1936
+ summary: "Read-only. Returns the total workout count for the account.",
1937
+ aliases: [
1938
+ "count workouts",
1939
+ "how many workouts",
1940
+ "workout total"
1941
+ ],
1942
+ useCase: "Use for totals, statistics, or estimating pages; use get-workouts for actual workout records.",
1943
+ importantNotes: "Returns only a count and accepts no paging or date filters."
1944
+ }),
1695
1945
  inputSchema: {},
1696
1946
  outputSchema: workoutCountOutputSchema,
1697
1947
  annotations: readOnlyAnnotations("Get Workout Count")
@@ -1705,7 +1955,16 @@ function registerWorkoutTools(server, hevyClient) {
1705
1955
  since: z.string().default("1970-01-01T00:00:00Z")
1706
1956
  };
1707
1957
  server.registerTool("get-workout-events", {
1708
- description: "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.",
1958
+ description: describeTool({
1959
+ summary: "Read-only. Lists workout update and delete events since a timestamp, newest first.",
1960
+ aliases: [
1961
+ "sync workout changes",
1962
+ "workout change feed",
1963
+ "deleted workouts"
1964
+ ],
1965
+ useCase: "Use to incrementally synchronize a local workout cache; use get-workouts for the current workout list.",
1966
+ importantNotes: "since must be a timestamp string; events are paginated with pageSize at most 10, and the default since value reads from 1970."
1967
+ }),
1709
1968
  inputSchema: getWorkoutEventsSchema,
1710
1969
  outputSchema: workoutEventsOutputSchema,
1711
1970
  annotations: readOnlyAnnotations("Get Workout Events")
@@ -1716,7 +1975,7 @@ function registerWorkoutTools(server, hevyClient) {
1716
1975
  page,
1717
1976
  pageSize,
1718
1977
  since
1719
- }))?.events || [];
1978
+ }))?.events?.map(formatWorkoutEvent) || [];
1720
1979
  if (events.length === 0) return createStructuredEmptyResponse(`No workout events found for the specified parameters since ${since}`, { events: [] });
1721
1980
  return createStructuredJsonResponse(events, { events });
1722
1981
  }, "get-workout-events"));
@@ -1744,7 +2003,16 @@ function registerWorkoutTools(server, hevyClient) {
1744
2003
  }))
1745
2004
  })))
1746
2005
  };
1747
- 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"), withObservability(async (args) => {
2006
+ server.tool("create-workout", describeTool({
2007
+ summary: "Writes to the Hevy account by creating a new workout.",
2008
+ aliases: [
2009
+ "log workout",
2010
+ "add workout",
2011
+ "record training session"
2012
+ ],
2013
+ useCase: "Use to add a completed workout; use update-workout only when modifying an existing workout ID.",
2014
+ importantNotes: "Requires UTC startTime/endTime in YYYY-MM-DDTHH:mm:ssZ form and exercise template IDs. Retrying can create duplicates."
2015
+ }), createWorkoutSchema, createAnnotations("Create Workout"), withObservability(async (args) => {
1748
2016
  const client = requireClient(hevyClient);
1749
2017
  const { title, description, startTime, endTime, isPrivate, exercises } = args;
1750
2018
  const requestBody = { workout: {
@@ -1800,7 +2068,16 @@ function registerWorkoutTools(server, hevyClient) {
1800
2068
  }))
1801
2069
  })))
1802
2070
  };
1803
- 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"), withObservability(async (args) => {
2071
+ server.tool("update-workout", describeTool({
2072
+ summary: "Mutates the Hevy account by replacing an existing workout.",
2073
+ aliases: [
2074
+ "edit workout",
2075
+ "correct workout log",
2076
+ "replace workout details"
2077
+ ],
2078
+ useCase: "Use to revise a known workout; use create-workout for a new training session.",
2079
+ importantNotes: "Requires workoutId plus the complete title, times, privacy, exercises, and sets payload; omitted optional values may be cleared or defaulted."
2080
+ }), updateWorkoutSchema, updateAnnotations("Update Workout"), withObservability(async (args) => {
1804
2081
  const client = requireClient(hevyClient);
1805
2082
  const { workoutId, title, description, startTime, endTime, isPrivate, exercises } = args;
1806
2083
  const requestBody = { workout: {
@@ -1878,6 +2155,81 @@ function assertApiKey(apiKey) {
1878
2155
  }
1879
2156
  }
1880
2157
  //#endregion
2158
+ //#region src/utils/graceful-shutdown.ts
2159
+ const shutdownSignals = ["SIGINT", "SIGTERM"];
2160
+ const FORCED_EXIT_TIMEOUT_MS = 5e3;
2161
+ function flushStdout(stdout = process.stdout) {
2162
+ return new Promise((resolve, reject) => {
2163
+ try {
2164
+ stdout.write("", (error) => {
2165
+ if (error) {
2166
+ reject(error);
2167
+ return;
2168
+ }
2169
+ resolve();
2170
+ });
2171
+ } catch (error) {
2172
+ reject(error);
2173
+ }
2174
+ });
2175
+ }
2176
+ function installGracefulShutdown({ target, process: processLike = process, logError = console.error, flush = flushStdout, forcedExitTimeoutMs = FORCED_EXIT_TIMEOUT_MS, scheduleForcedExit = setTimeout }) {
2177
+ let listenersInstalled = true;
2178
+ let shutdownSettled = false;
2179
+ let shutdownPromise;
2180
+ const cleanup = () => {
2181
+ if (!listenersInstalled || shutdownPromise && !shutdownSettled) return;
2182
+ listenersInstalled = false;
2183
+ for (const signal of shutdownSignals) {
2184
+ const listener = signalListeners.get(signal);
2185
+ if (listener) processLike.removeListener(signal, listener);
2186
+ }
2187
+ };
2188
+ const handleSignal = (signal) => {
2189
+ if (shutdownPromise) return;
2190
+ if (processLike.exitCode == null) processLike.exitCode = 0;
2191
+ scheduleForcedExit(() => {
2192
+ processLike.exit(processLike.exitCode ?? 0);
2193
+ }, forcedExitTimeoutMs).unref();
2194
+ shutdownPromise = (async () => {
2195
+ logError(`Shutting down gracefully after ${signal}`);
2196
+ let shutdownFailed = false;
2197
+ let shutdownError;
2198
+ try {
2199
+ await target.close();
2200
+ } catch (error) {
2201
+ shutdownFailed = true;
2202
+ shutdownError = error;
2203
+ }
2204
+ try {
2205
+ await flush();
2206
+ } catch (error) {
2207
+ if (!shutdownFailed) shutdownError = error;
2208
+ shutdownFailed = true;
2209
+ }
2210
+ try {
2211
+ if (shutdownFailed) {
2212
+ logError(`Graceful shutdown failed: ${shutdownError instanceof Error ? shutdownError.message : "Unknown shutdown error"}`);
2213
+ processLike.exitCode = 1;
2214
+ return;
2215
+ }
2216
+ } finally {
2217
+ shutdownSettled = true;
2218
+ cleanup();
2219
+ }
2220
+ })();
2221
+ };
2222
+ const signalListeners = new Map(shutdownSignals.map((signal) => [signal, () => handleSignal(signal)]));
2223
+ for (const signal of shutdownSignals) {
2224
+ const listener = signalListeners.get(signal);
2225
+ if (listener) processLike.on(signal, listener);
2226
+ }
2227
+ return {
2228
+ cleanup,
2229
+ getShutdownPromise: () => shutdownPromise
2230
+ };
2231
+ }
2232
+ //#endregion
1881
2233
  //#region src/generated/.kubb/fetch.ts
1882
2234
  let _config = {
1883
2235
  baseURL: typeof AXIOS_BASE !== "undefined" ? AXIOS_BASE : void 0,
@@ -1901,13 +2253,13 @@ const mergeConfig = (...configs) => {
1901
2253
  }, {});
1902
2254
  };
1903
2255
  const axiosInstance = axios.create(getConfig());
1904
- const fetch = async (config) => {
2256
+ const fetch$1 = async (config) => {
1905
2257
  return axiosInstance.request(mergeConfig(getConfig(), config)).catch((e) => {
1906
2258
  throw e;
1907
2259
  });
1908
2260
  };
1909
- fetch.getConfig = getConfig;
1910
- fetch.setConfig = setConfig;
2261
+ fetch$1.getConfig = getConfig;
2262
+ fetch$1.setConfig = setConfig;
1911
2263
  //#endregion
1912
2264
  //#region src/generated/client/api/getV1BodyMeasurements.ts
1913
2265
  function getGetV1BodyMeasurementsUrl() {
@@ -1921,7 +2273,7 @@ function getGetV1BodyMeasurementsUrl() {
1921
2273
  * {@link /v1/body_measurements}
1922
2274
  */
1923
2275
  async function getV1BodyMeasurements(headers, params, config = {}) {
1924
- const { client: request = fetch, ...requestConfig } = config;
2276
+ const { client: request = fetch$1, ...requestConfig } = config;
1925
2277
  return (await request({
1926
2278
  method: "GET",
1927
2279
  url: getGetV1BodyMeasurementsUrl().url.toString(),
@@ -1946,7 +2298,7 @@ function getGetV1BodyMeasurementsDateUrl(date) {
1946
2298
  * {@link /v1/body_measurements/:date}
1947
2299
  */
1948
2300
  async function getV1BodyMeasurementsDate(date, headers, config = {}) {
1949
- const { client: request = fetch, ...requestConfig } = config;
2301
+ const { client: request = fetch$1, ...requestConfig } = config;
1950
2302
  return (await request({
1951
2303
  method: "GET",
1952
2304
  url: getGetV1BodyMeasurementsDateUrl(date).url.toString(),
@@ -1970,7 +2322,7 @@ function getGetV1ExerciseHistoryExercisetemplateidUrl(exerciseTemplateId) {
1970
2322
  * {@link /v1/exercise_history/:exerciseTemplateId}
1971
2323
  */
1972
2324
  async function getV1ExerciseHistoryExercisetemplateid(exerciseTemplateId, headers, params, config = {}) {
1973
- const { client: request = fetch, ...requestConfig } = config;
2325
+ const { client: request = fetch$1, ...requestConfig } = config;
1974
2326
  return (await request({
1975
2327
  method: "GET",
1976
2328
  url: getGetV1ExerciseHistoryExercisetemplateidUrl(exerciseTemplateId).url.toString(),
@@ -1995,7 +2347,7 @@ function getGetV1ExerciseTemplatesUrl() {
1995
2347
  * {@link /v1/exercise_templates}
1996
2348
  */
1997
2349
  async function getV1ExerciseTemplates(headers, params, config = {}) {
1998
- const { client: request = fetch, ...requestConfig } = config;
2350
+ const { client: request = fetch$1, ...requestConfig } = config;
1999
2351
  return (await request({
2000
2352
  method: "GET",
2001
2353
  url: getGetV1ExerciseTemplatesUrl().url.toString(),
@@ -2020,7 +2372,7 @@ function getGetV1ExerciseTemplatesExercisetemplateidUrl(exerciseTemplateId) {
2020
2372
  * {@link /v1/exercise_templates/:exerciseTemplateId}
2021
2373
  */
2022
2374
  async function getV1ExerciseTemplatesExercisetemplateid(exerciseTemplateId, headers, config = {}) {
2023
- const { client: request = fetch, ...requestConfig } = config;
2375
+ const { client: request = fetch$1, ...requestConfig } = config;
2024
2376
  return (await request({
2025
2377
  method: "GET",
2026
2378
  url: getGetV1ExerciseTemplatesExercisetemplateidUrl(exerciseTemplateId).url.toString(),
@@ -2044,7 +2396,7 @@ function getGetV1RoutineFoldersUrl() {
2044
2396
  * {@link /v1/routine_folders}
2045
2397
  */
2046
2398
  async function getV1RoutineFolders(headers, params, config = {}) {
2047
- const { client: request = fetch, ...requestConfig } = config;
2399
+ const { client: request = fetch$1, ...requestConfig } = config;
2048
2400
  return (await request({
2049
2401
  method: "GET",
2050
2402
  url: getGetV1RoutineFoldersUrl().url.toString(),
@@ -2069,7 +2421,7 @@ function getGetV1RoutineFoldersFolderidUrl(folderId) {
2069
2421
  * {@link /v1/routine_folders/:folderId}
2070
2422
  */
2071
2423
  async function getV1RoutineFoldersFolderid(folderId, headers, config = {}) {
2072
- const { client: request = fetch, ...requestConfig } = config;
2424
+ const { client: request = fetch$1, ...requestConfig } = config;
2073
2425
  return (await request({
2074
2426
  method: "GET",
2075
2427
  url: getGetV1RoutineFoldersFolderidUrl(folderId).url.toString(),
@@ -2093,7 +2445,7 @@ function getGetV1RoutinesUrl() {
2093
2445
  * {@link /v1/routines}
2094
2446
  */
2095
2447
  async function getV1Routines(headers, params, config = {}) {
2096
- const { client: request = fetch, ...requestConfig } = config;
2448
+ const { client: request = fetch$1, ...requestConfig } = config;
2097
2449
  return (await request({
2098
2450
  method: "GET",
2099
2451
  url: getGetV1RoutinesUrl().url.toString(),
@@ -2118,7 +2470,7 @@ function getGetV1RoutinesRoutineidUrl(routineId) {
2118
2470
  * {@link /v1/routines/:routineId}
2119
2471
  */
2120
2472
  async function getV1RoutinesRoutineid(routineId, headers, config = {}) {
2121
- const { client: request = fetch, ...requestConfig } = config;
2473
+ const { client: request = fetch$1, ...requestConfig } = config;
2122
2474
  return (await request({
2123
2475
  method: "GET",
2124
2476
  url: getGetV1RoutinesRoutineidUrl(routineId).url.toString(),
@@ -2142,7 +2494,7 @@ function getGetV1UserInfoUrl() {
2142
2494
  * {@link /v1/user/info}
2143
2495
  */
2144
2496
  async function getV1UserInfo(headers, config = {}) {
2145
- const { client: request = fetch, ...requestConfig } = config;
2497
+ const { client: request = fetch$1, ...requestConfig } = config;
2146
2498
  return (await request({
2147
2499
  method: "GET",
2148
2500
  url: getGetV1UserInfoUrl().url.toString(),
@@ -2166,7 +2518,7 @@ function getGetV1WorkoutsUrl() {
2166
2518
  * {@link /v1/workouts}
2167
2519
  */
2168
2520
  async function getV1Workouts(headers, params, config = {}) {
2169
- const { client: request = fetch, ...requestConfig } = config;
2521
+ const { client: request = fetch$1, ...requestConfig } = config;
2170
2522
  return (await request({
2171
2523
  method: "GET",
2172
2524
  url: getGetV1WorkoutsUrl().url.toString(),
@@ -2191,7 +2543,7 @@ function getGetV1WorkoutsCountUrl() {
2191
2543
  * {@link /v1/workouts/count}
2192
2544
  */
2193
2545
  async function getV1WorkoutsCount(headers, config = {}) {
2194
- const { client: request = fetch, ...requestConfig } = config;
2546
+ const { client: request = fetch$1, ...requestConfig } = config;
2195
2547
  return (await request({
2196
2548
  method: "GET",
2197
2549
  url: getGetV1WorkoutsCountUrl().url.toString(),
@@ -2216,7 +2568,7 @@ function getGetV1WorkoutsEventsUrl() {
2216
2568
  * {@link /v1/workouts/events}
2217
2569
  */
2218
2570
  async function getV1WorkoutsEvents(headers, params, config = {}) {
2219
- const { client: request = fetch, ...requestConfig } = config;
2571
+ const { client: request = fetch$1, ...requestConfig } = config;
2220
2572
  return (await request({
2221
2573
  method: "GET",
2222
2574
  url: getGetV1WorkoutsEventsUrl().url.toString(),
@@ -2241,7 +2593,7 @@ function getGetV1WorkoutsWorkoutidUrl(workoutId) {
2241
2593
  * {@link /v1/workouts/:workoutId}
2242
2594
  */
2243
2595
  async function getV1WorkoutsWorkoutid(workoutId, headers, config = {}) {
2244
- const { client: request = fetch, ...requestConfig } = config;
2596
+ const { client: request = fetch$1, ...requestConfig } = config;
2245
2597
  return (await request({
2246
2598
  method: "GET",
2247
2599
  url: getGetV1WorkoutsWorkoutidUrl(workoutId).url.toString(),
@@ -2265,7 +2617,7 @@ function getPostV1BodyMeasurementsUrl() {
2265
2617
  * {@link /v1/body_measurements}
2266
2618
  */
2267
2619
  async function postV1BodyMeasurements(data, headers, config = {}) {
2268
- const { client: request = fetch, ...requestConfig } = config;
2620
+ const { client: request = fetch$1, ...requestConfig } = config;
2269
2621
  const requestData = data;
2270
2622
  return (await request({
2271
2623
  method: "POST",
@@ -2291,7 +2643,7 @@ function getPostV1ExerciseTemplatesUrl() {
2291
2643
  * {@link /v1/exercise_templates}
2292
2644
  */
2293
2645
  async function postV1ExerciseTemplates(data, headers, config = {}) {
2294
- const { client: request = fetch, ...requestConfig } = config;
2646
+ const { client: request = fetch$1, ...requestConfig } = config;
2295
2647
  const requestData = data;
2296
2648
  return (await request({
2297
2649
  method: "POST",
@@ -2317,7 +2669,7 @@ function getPostV1RoutineFoldersUrl() {
2317
2669
  * {@link /v1/routine_folders}
2318
2670
  */
2319
2671
  async function postV1RoutineFolders(data, headers, config = {}) {
2320
- const { client: request = fetch, ...requestConfig } = config;
2672
+ const { client: request = fetch$1, ...requestConfig } = config;
2321
2673
  const requestData = data;
2322
2674
  return (await request({
2323
2675
  method: "POST",
@@ -2343,7 +2695,7 @@ function getPostV1RoutinesUrl() {
2343
2695
  * {@link /v1/routines}
2344
2696
  */
2345
2697
  async function postV1Routines(data, headers, config = {}) {
2346
- const { client: request = fetch, ...requestConfig } = config;
2698
+ const { client: request = fetch$1, ...requestConfig } = config;
2347
2699
  const requestData = data;
2348
2700
  return (await request({
2349
2701
  method: "POST",
@@ -2369,7 +2721,7 @@ function getPostV1WorkoutsUrl() {
2369
2721
  * {@link /v1/workouts}
2370
2722
  */
2371
2723
  async function postV1Workouts(data, headers, config = {}) {
2372
- const { client: request = fetch, ...requestConfig } = config;
2724
+ const { client: request = fetch$1, ...requestConfig } = config;
2373
2725
  const requestData = data;
2374
2726
  return (await request({
2375
2727
  method: "POST",
@@ -2395,7 +2747,7 @@ function getPutV1BodyMeasurementsDateUrl(date) {
2395
2747
  * {@link /v1/body_measurements/:date}
2396
2748
  */
2397
2749
  async function putV1BodyMeasurementsDate(date, data, headers, config = {}) {
2398
- const { client: request = fetch, ...requestConfig } = config;
2750
+ const { client: request = fetch$1, ...requestConfig } = config;
2399
2751
  const requestData = data;
2400
2752
  return (await request({
2401
2753
  method: "PUT",
@@ -2421,7 +2773,7 @@ function getPutV1RoutinesRoutineidUrl(routineId) {
2421
2773
  * {@link /v1/routines/:routineId}
2422
2774
  */
2423
2775
  async function putV1RoutinesRoutineid(routineId, data, headers, config = {}) {
2424
- const { client: request = fetch, ...requestConfig } = config;
2776
+ const { client: request = fetch$1, ...requestConfig } = config;
2425
2777
  const requestData = data;
2426
2778
  return (await request({
2427
2779
  method: "PUT",
@@ -2447,7 +2799,7 @@ function getPutV1WorkoutsWorkoutidUrl(workoutId) {
2447
2799
  * {@link /v1/workouts/:workoutId}
2448
2800
  */
2449
2801
  async function putV1WorkoutsWorkoutid(workoutId, data, headers, config = {}) {
2450
- const { client: request = fetch, ...requestConfig } = config;
2802
+ const { client: request = fetch$1, ...requestConfig } = config;
2451
2803
  const requestData = data;
2452
2804
  return (await request({
2453
2805
  method: "PUT",
@@ -2519,6 +2871,15 @@ function getApiTimeoutMs() {
2519
2871
  if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_API_TIMEOUT_MS;
2520
2872
  return Math.trunc(parsed);
2521
2873
  }
2874
+ function normalizeMaxGetRetries(value) {
2875
+ if (value === void 0 || !Number.isFinite(value) || value < 0) return 3;
2876
+ return Math.floor(value);
2877
+ }
2878
+ function normalizeTimeoutMs(value) {
2879
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return getApiTimeoutMs();
2880
+ const normalizedValue = Math.floor(value);
2881
+ return normalizedValue > 0 ? normalizedValue : getApiTimeoutMs();
2882
+ }
2522
2883
  function sleep(ms) {
2523
2884
  return new Promise((resolve) => {
2524
2885
  setTimeout(resolve, ms);
@@ -2606,7 +2967,7 @@ function markRetryExhausted(error, retryCount) {
2606
2967
  if (retryError.code) retryError.hevyRetryOriginalCode = retryError.code;
2607
2968
  retryError.code = HEVY_RETRY_EXHAUSTED_ERROR_CODE;
2608
2969
  }
2609
- async function requestWithRetries(axiosInstance, config, logger) {
2970
+ async function requestWithRetries(axiosInstance, config, maxGetRetries, logger) {
2610
2971
  let retryCount = 0;
2611
2972
  while (true) try {
2612
2973
  return await axiosInstance.request(config);
@@ -2628,7 +2989,7 @@ async function requestWithRetries(axiosInstance, config, logger) {
2628
2989
  });
2629
2990
  throw error;
2630
2991
  }
2631
- if (retryCount >= 3) {
2992
+ if (retryCount >= maxGetRetries) {
2632
2993
  emitClientLog(logger, {
2633
2994
  level: status === 429 ? "warning" : "error",
2634
2995
  logger: "hevy-api",
@@ -2636,7 +2997,7 @@ async function requestWithRetries(axiosInstance, config, logger) {
2636
2997
  message: "Hevy API request failed after retries",
2637
2998
  status,
2638
2999
  attempt: retryCount + 1,
2639
- maxAttempts: 4,
3000
+ maxAttempts: maxGetRetries + 1,
2640
3001
  method,
2641
3002
  endpoint
2642
3003
  }
@@ -2654,7 +3015,7 @@ async function requestWithRetries(axiosInstance, config, logger) {
2654
3015
  message: status === 429 ? "Hevy API rate limit; retrying request" : "Retrying Hevy API request",
2655
3016
  status,
2656
3017
  attempt: retryCount + 1,
2657
- maxAttempts: 4,
3018
+ maxAttempts: maxGetRetries + 1,
2658
3019
  delayMs: retryDelayMs,
2659
3020
  ...status === 429 ? { retryAfterMs: retryAfterMs ?? null } : {},
2660
3021
  method,
@@ -2664,10 +3025,10 @@ async function requestWithRetries(axiosInstance, config, logger) {
2664
3025
  await sleep(retryDelayMs);
2665
3026
  }
2666
3027
  }
2667
- function createResilientClient(axiosInstance, logger) {
3028
+ function createResilientClient(axiosInstance, maxGetRetries, logger) {
2668
3029
  let clientConfig = { baseURL: axiosInstance.defaults.baseURL };
2669
3030
  const resilientClient = (async (config) => {
2670
- return requestWithRetries(axiosInstance, config, logger);
3031
+ return requestWithRetries(axiosInstance, config, maxGetRetries, logger);
2671
3032
  });
2672
3033
  resilientClient.getConfig = () => ({ ...clientConfig });
2673
3034
  resilientClient.setConfig = (config) => {
@@ -2723,9 +3084,11 @@ function finalizeRequestTrace(opts) {
2723
3084
  }
2724
3085
  function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {}) {
2725
3086
  const { logger } = options;
3087
+ const maxGetRetries = normalizeMaxGetRetries(options.maxGetRetries);
3088
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
2726
3089
  const axiosInstance = axios.create({
2727
3090
  baseURL: baseUrl,
2728
- timeout: getApiTimeoutMs(),
3091
+ timeout: timeoutMs,
2729
3092
  headers: { "api-key": apiKey }
2730
3093
  });
2731
3094
  axiosInstance.interceptors.request.use((config) => {
@@ -2733,11 +3096,13 @@ function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {
2733
3096
  const method = (config.method ?? "get").toUpperCase();
2734
3097
  const url = config.url ?? "";
2735
3098
  const endpoint = url.split("?")[0] ?? url;
3099
+ const userId = getCurrentUserId();
2736
3100
  tracedConfig._span = tracer.startSpan(`hevy.api.${method}`, { attributes: {
2737
3101
  "http.method": method,
2738
3102
  "http.url": url,
2739
3103
  "http.base_url": config.baseURL ?? "",
2740
- "hevy.api.endpoint": endpoint
3104
+ "hevy.api.endpoint": endpoint,
3105
+ ...userId ? { "user.id": userId } : {}
2741
3106
  } });
2742
3107
  tracedConfig._startTime = Date.now();
2743
3108
  return config;
@@ -2749,6 +3114,13 @@ function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {
2749
3114
  url: response.config.url,
2750
3115
  _startTime: tracedConfig._startTime
2751
3116
  });
3117
+ const requestContext = getRequestContext(response.config);
3118
+ debugLog("api_response", {
3119
+ method: requestContext.method,
3120
+ endpoint: requestContext.endpoint,
3121
+ durationMs,
3122
+ status: response.status
3123
+ });
2752
3124
  finalizeRequestTrace({
2753
3125
  span: tracedConfig._span,
2754
3126
  statusCode: response.status,
@@ -2764,6 +3136,13 @@ function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {
2764
3136
  url: error.config?.url,
2765
3137
  _startTime: tracedConfig._startTime
2766
3138
  });
3139
+ const requestContext = getRequestContext(error.config ?? {});
3140
+ debugLog("api_response", {
3141
+ method: requestContext.method,
3142
+ endpoint: requestContext.endpoint,
3143
+ durationMs,
3144
+ status: error.response?.status ?? null
3145
+ });
2767
3146
  finalizeRequestTrace({
2768
3147
  span: tracedConfig._span,
2769
3148
  statusCode: error.response?.status ?? 0,
@@ -2775,7 +3154,7 @@ function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {
2775
3154
  throw error;
2776
3155
  });
2777
3156
  const headers = { "api-key": apiKey };
2778
- const client = createResilientClient(axiosInstance, logger);
3157
+ const client = createResilientClient(axiosInstance, maxGetRetries, logger);
2779
3158
  return {
2780
3159
  getWorkouts: (params) => wrapApi(getV1Workouts)(headers, params, { client }),
2781
3160
  getWorkout: (workoutId) => wrapApi(getV1WorkoutsWorkoutid)(workoutId, headers, { client }),
@@ -2832,6 +3211,9 @@ function createMcpClientLogger(server) {
2832
3211
  //#endregion
2833
3212
  //#region src/utils/stdio-observability.ts
2834
3213
  const UTF8_BOM = "";
3214
+ /** Maximum escaped characters included in a malformed stdin shape preview. */
3215
+ const STDIN_PARSE_SHAPE_PREVIEW_MAX_LENGTH = 200;
3216
+ const REDACTED_CONTENT_MARKER = "[REDACTED]";
2835
3217
  /**
2836
3218
  * Adapter boundary around MCP SDK stdio internals.
2837
3219
  *
@@ -2886,6 +3268,62 @@ function getFailureLocation(failurePosition, lineHadLeadingBom) {
2886
3268
  if (failurePosition !== null) return "line_body";
2887
3269
  return "unknown";
2888
3270
  }
3271
+ function createStructuralShapePreview(line) {
3272
+ let shapePreview = "";
3273
+ let inContentRun = false;
3274
+ let inWhitespaceRun = false;
3275
+ const append = (token) => {
3276
+ if (shapePreview.length + token.length > STDIN_PARSE_SHAPE_PREVIEW_MAX_LENGTH) return false;
3277
+ shapePreview += token;
3278
+ return true;
3279
+ };
3280
+ for (const character of line) {
3281
+ if ("{}[]:,\"".includes(character)) {
3282
+ inContentRun = false;
3283
+ inWhitespaceRun = false;
3284
+ if (!append(character === "\"" ? "\\u0022" : character)) return {
3285
+ shapePreview,
3286
+ truncated: true
3287
+ };
3288
+ continue;
3289
+ }
3290
+ if (/\s/u.test(character)) {
3291
+ inContentRun = false;
3292
+ if (!inWhitespaceRun) {
3293
+ if (!append("\\s")) return {
3294
+ shapePreview,
3295
+ truncated: true
3296
+ };
3297
+ inWhitespaceRun = true;
3298
+ }
3299
+ continue;
3300
+ }
3301
+ inWhitespaceRun = false;
3302
+ if (!inContentRun) {
3303
+ if (!append(REDACTED_CONTENT_MARKER)) return {
3304
+ shapePreview,
3305
+ truncated: true
3306
+ };
3307
+ inContentRun = true;
3308
+ }
3309
+ }
3310
+ return {
3311
+ shapePreview,
3312
+ truncated: false
3313
+ };
3314
+ }
3315
+ function getSafeErrorKind(error) {
3316
+ if (error instanceof SyntaxError) return "SyntaxError";
3317
+ if (error instanceof Error) return "Error";
3318
+ return "UnknownError";
3319
+ }
3320
+ function reportStdinParseFailure(error, line, lineByteLength, failureLocation, failurePosition) {
3321
+ try {
3322
+ const errorKind = getSafeErrorKind(error);
3323
+ const { shapePreview, truncated } = createStructuralShapePreview(line);
3324
+ console.error(`Failed to parse MCP stdin message: error_kind=${errorKind} line_bytes=${lineByteLength} failure_location=${failureLocation} failure_position=${failurePosition === null ? "unknown" : failurePosition} shape_preview="${shapePreview}" shape_preview_redacted=true shape_preview_truncated=${truncated}`);
3325
+ } catch {}
3326
+ }
2889
3327
  function deserializeMessageWithObservability(line, chunkSnapshot) {
2890
3328
  const lineHadLeadingBom = line.startsWith(UTF8_BOM);
2891
3329
  const normalizedLine = lineHadLeadingBom ? line.slice(1) : line;
@@ -2929,6 +3367,7 @@ function deserializeMessageWithObservability(line, chunkSnapshot) {
2929
3367
  });
2930
3368
  Sentry.captureException(error);
2931
3369
  });
3370
+ reportStdinParseFailure(error, line, lineByteLength, failureLocation, failurePosition);
2932
3371
  throw error;
2933
3372
  } finally {
2934
3373
  span.end();
@@ -2951,9 +3390,164 @@ function createInstrumentedStdioTransport(transport) {
2951
3390
  return transport;
2952
3391
  }
2953
3392
  //#endregion
3393
+ //#region src/utils/version-check.ts
3394
+ const CACHE_TTL_MS = 1440 * 60 * 1e3;
3395
+ const REQUEST_TIMEOUT_MS = 3e3;
3396
+ const REGISTRY_URL = "https://registry.npmjs.org";
3397
+ const defaultDependencies = {
3398
+ fetch,
3399
+ readFile,
3400
+ mkdir,
3401
+ writeFile,
3402
+ rename,
3403
+ unlink,
3404
+ homedir,
3405
+ randomUUID,
3406
+ pid: process.pid,
3407
+ now: Date.now,
3408
+ env: process.env,
3409
+ writeStderr: (message) => process.stderr.write(message)
3410
+ };
3411
+ const defaultSchedulerDependencies = {
3412
+ setImmediate,
3413
+ checkForUpdate
3414
+ };
3415
+ function getCachePath(dependencies) {
3416
+ return join(dependencies.env.XDG_CACHE_HOME || join(dependencies.homedir(), ".cache"), "hevy-mcp", "update-check.json");
3417
+ }
3418
+ function parseCacheEntry(value, now) {
3419
+ try {
3420
+ const parsed = JSON.parse(value);
3421
+ if (typeof parsed !== "object" || parsed === null || !("checkedAt" in parsed) || !("latestVersion" in parsed)) return;
3422
+ const { checkedAt, latestVersion } = parsed;
3423
+ if (typeof checkedAt !== "number" || !Number.isFinite(checkedAt) || typeof latestVersion !== "string" || !semver.valid(latestVersion)) return;
3424
+ const age = now - checkedAt;
3425
+ if (age < 0 || age >= CACHE_TTL_MS) return;
3426
+ return {
3427
+ checkedAt,
3428
+ latestVersion
3429
+ };
3430
+ } catch {
3431
+ return;
3432
+ }
3433
+ }
3434
+ function parseLatestVersion(value) {
3435
+ if (typeof value !== "object" || value === null || !("dist-tags" in value)) return;
3436
+ const distTags = value["dist-tags"];
3437
+ if (typeof distTags !== "object" || distTags === null || !("latest" in distTags) || typeof distTags.latest !== "string" || !semver.valid(distTags.latest)) return;
3438
+ return distTags.latest;
3439
+ }
3440
+ function shouldNotifyAboutUpdate(currentVersion, latestVersion) {
3441
+ const current = semver.parse(currentVersion);
3442
+ const latest = semver.parse(latestVersion);
3443
+ if (!current || !latest || latest.prerelease.length > 0 || !semver.gt(latest, current)) return false;
3444
+ if (latest.major > current.major) return true;
3445
+ return latest.major === current.major && latest.minor - current.minor > 2;
3446
+ }
3447
+ async function readFreshCachedVersion(cachePath, dependencies) {
3448
+ try {
3449
+ return parseCacheEntry(await dependencies.readFile(cachePath, "utf8"), dependencies.now())?.latestVersion;
3450
+ } catch {
3451
+ return;
3452
+ }
3453
+ }
3454
+ async function fetchLatestVersion(packageName, dependencies) {
3455
+ try {
3456
+ const response = await dependencies.fetch(`${REGISTRY_URL}/${encodeURIComponent(packageName)}`, {
3457
+ headers: { Accept: "application/vnd.npm.install-v1+json" },
3458
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
3459
+ });
3460
+ if (!response.ok) return;
3461
+ return parseLatestVersion(await response.json());
3462
+ } catch {
3463
+ return;
3464
+ }
3465
+ }
3466
+ async function writeCache(cachePath, entry, dependencies) {
3467
+ const temporaryPath = `${cachePath}.${dependencies.pid}.${dependencies.randomUUID()}.tmp`;
3468
+ const payload = `${JSON.stringify(entry)}\n`;
3469
+ try {
3470
+ await dependencies.mkdir(dirname(cachePath), { recursive: true });
3471
+ await dependencies.writeFile(temporaryPath, payload, "utf8");
3472
+ } catch {
3473
+ try {
3474
+ await dependencies.unlink(temporaryPath);
3475
+ } catch {}
3476
+ return;
3477
+ }
3478
+ try {
3479
+ await dependencies.rename(temporaryPath, cachePath);
3480
+ return;
3481
+ } catch {
3482
+ try {
3483
+ await dependencies.writeFile(cachePath, payload, "utf8");
3484
+ } catch {}
3485
+ }
3486
+ try {
3487
+ await dependencies.unlink(temporaryPath);
3488
+ } catch {}
3489
+ }
3490
+ async function checkForUpdate(options, overrides = {}) {
3491
+ try {
3492
+ if (!semver.valid(options.currentVersion)) return;
3493
+ const dependencies = {
3494
+ ...defaultDependencies,
3495
+ ...overrides
3496
+ };
3497
+ const cachePath = getCachePath(dependencies);
3498
+ let latestVersion = await readFreshCachedVersion(cachePath, dependencies);
3499
+ if (!latestVersion) {
3500
+ latestVersion = await fetchLatestVersion(options.packageName, dependencies);
3501
+ if (!latestVersion) return;
3502
+ await writeCache(cachePath, {
3503
+ checkedAt: dependencies.now(),
3504
+ latestVersion
3505
+ }, dependencies);
3506
+ }
3507
+ if (shouldNotifyAboutUpdate(options.currentVersion, latestVersion)) dependencies.writeStderr(`Update available for ${options.packageName}: current ${options.currentVersion}, latest ${latestVersion}. Notices are shown for newer major versions or when more than two minor versions behind. Update using your preferred installation method (for example, npm install -g ${options.packageName}@latest).\n`);
3508
+ } catch {}
3509
+ }
3510
+ function scheduleUpdateCheck(options, overrides = {}) {
3511
+ const dependencies = {
3512
+ ...defaultSchedulerDependencies,
3513
+ ...overrides
3514
+ };
3515
+ dependencies.setImmediate(() => {
3516
+ dependencies.checkForUpdate(options).catch(() => void 0);
3517
+ }).unref();
3518
+ }
3519
+ //#endregion
2954
3520
  //#region src/index.ts
2955
3521
  const name = name$1;
2956
3522
  const version = version$1;
3523
+ const SERVER_INSTRUCTIONS = [
3524
+ [
3525
+ "Hevy MCP connects clients to the authenticated user's Hevy",
3526
+ "workout-tracking data, including workouts, routines, exercise templates,",
3527
+ "routine folders, body measurements, and profile information.",
3528
+ "HEVY_API_KEY must contain a valid Hevy API key."
3529
+ ].join(" "),
3530
+ [
3531
+ "Safety: all get-* and search-* tools are read-only. create-* and",
3532
+ "update-* tools mutate Hevy data. Creates are additive and",
3533
+ "non-idempotent, so repeating one can create duplicates. Updates can",
3534
+ "overwrite existing data. Delete operations are not available."
3535
+ ].join(" "),
3536
+ [
3537
+ "Workflow: search exercise templates first, then use the returned",
3538
+ "template IDs when creating workouts or routines. To create a completed",
3539
+ "workout from a routine, fetch the routine as a plan, then obtain the",
3540
+ "actual completed sets and end time from the user; never invent completion",
3541
+ "data. Use the built-in workflow prompts when they match the task."
3542
+ ].join(" "),
3543
+ ["Pagination: start at page 1 and fetch only the pages needed. Most list", "tools allow pageSize up to 10; get-exercise-templates allows up to 100."].join(" "),
3544
+ [
3545
+ "Rate limits and retries: minimize repeated calls. If Hevy returns HTTP",
3546
+ "429, follow its retry guidance. Transient read requests retry",
3547
+ "automatically, but write requests do not; confirm uncertain write",
3548
+ "outcomes before trying again."
3549
+ ].join(" ")
3550
+ ].join("\n\n");
2957
3551
  const HELP_TEXT = [
2958
3552
  "Usage:",
2959
3553
  " hevy-mcp [options]",
@@ -2965,6 +3559,7 @@ const HELP_TEXT = [
2965
3559
  "",
2966
3560
  "Environment:",
2967
3561
  " HEVY_API_KEY=<api-key> Hevy API key from Hevy app settings",
3562
+ " HEVY_MCP_DEBUG=1 Enable verbose diagnostics on stderr",
2968
3563
  "",
2969
3564
  "Examples:",
2970
3565
  " HEVY_API_KEY=your-key npx hevy-mcp",
@@ -2979,6 +3574,21 @@ function getCliAction(args) {
2979
3574
  return "start";
2980
3575
  }
2981
3576
  const HEVY_API_BASEURL = "https://api.hevyapp.com";
3577
+ const STARTUP_PROBE_TIMEOUT_MS = 5e3;
3578
+ const INVALID_API_KEY_MESSAGE = "HEVY_API_KEY is invalid or expired. Please check your API key in the Hevy app under Settings > API Key.";
3579
+ const API_KEY_VALIDATION_WARNING = "Warning: HEVY_API_KEY could not be validated during startup. Startup will continue; check your network connection and Hevy API availability.";
3580
+ const SAFE_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
3581
+ "EAI_AGAIN",
3582
+ "ECONNABORTED",
3583
+ "ECONNREFUSED",
3584
+ "ECONNRESET",
3585
+ "ENETUNREACH",
3586
+ "ENOTFOUND",
3587
+ "ERR_NETWORK",
3588
+ "ERR_SOCKET_TIMEOUT",
3589
+ "ETIMEDOUT",
3590
+ "HEVY_RETRY_EXHAUSTED"
3591
+ ]);
2982
3592
  const SENTRY_USER_ID_CONTEXT = "hevy-mcp:sentry-user-id:v1";
2983
3593
  function fingerprintApiKey(apiKey) {
2984
3594
  return createHmac("sha256", apiKey).update(SENTRY_USER_ID_CONTEXT).digest("hex").slice(0, 10);
@@ -3007,6 +3617,33 @@ function createToolCountingServer(server) {
3007
3617
  getCount: () => count
3008
3618
  };
3009
3619
  }
3620
+ function getHttpStatus(error) {
3621
+ if (!error || typeof error !== "object" || !("response" in error)) return;
3622
+ const response = error.response;
3623
+ if (!response || typeof response !== "object" || !("status" in response)) return;
3624
+ return typeof response.status === "number" && Number.isInteger(response.status) && response.status >= 100 && response.status <= 599 ? response.status : void 0;
3625
+ }
3626
+ function getSafeValidationDiagnostic(error) {
3627
+ const status = getHttpStatus(error);
3628
+ if (status !== void 0) return `HTTP ${status}`;
3629
+ if (!error || typeof error !== "object" || !("code" in error)) return;
3630
+ const code = error.code;
3631
+ return typeof code === "string" && SAFE_NETWORK_ERROR_CODES.has(code) ? code : void 0;
3632
+ }
3633
+ async function validateApiKey(apiKey) {
3634
+ const startupProbeClient = createClient(apiKey, HEVY_API_BASEURL, {
3635
+ maxGetRetries: 0,
3636
+ timeoutMs: STARTUP_PROBE_TIMEOUT_MS
3637
+ });
3638
+ try {
3639
+ await startupProbeClient.getUserInfo();
3640
+ } catch (error) {
3641
+ const status = getHttpStatus(error);
3642
+ if (status === 401 || status === 403) throw new Error(INVALID_API_KEY_MESSAGE);
3643
+ const diagnostic = getSafeValidationDiagnostic(error);
3644
+ console.error(diagnostic ? `${API_KEY_VALIDATION_WARNING} Diagnostic: ${diagnostic}.` : API_KEY_VALIDATION_WARNING);
3645
+ }
3646
+ }
3010
3647
  function buildServer(apiKey) {
3011
3648
  const userId = fingerprintApiKey(apiKey);
3012
3649
  return tracer.startActiveSpan("mcp.server.build", { attributes: {
@@ -3021,7 +3658,10 @@ function buildServer(apiKey) {
3021
3658
  const baseServer = new McpServer({
3022
3659
  name,
3023
3660
  version
3024
- }, { capabilities: { logging: {} } });
3661
+ }, {
3662
+ capabilities: { logging: {} },
3663
+ instructions: SERVER_INSTRUCTIONS
3664
+ });
3025
3665
  const server = Sentry.wrapMcpServerWithSentry(baseServer);
3026
3666
  const clientLogger = createMcpClientLogger(server);
3027
3667
  const hevyClient = tracer.startActiveSpan("mcp.hevy-client.initialize", (childSpan) => {
@@ -3064,15 +3704,16 @@ function buildServer(apiKey) {
3064
3704
  }
3065
3705
  });
3066
3706
  }
3067
- function createServer({ config }) {
3707
+ async function createServer({ config }) {
3068
3708
  const { apiKey } = serverConfigSchema.parse(config);
3709
+ await validateApiKey(apiKey);
3069
3710
  return buildServer(apiKey);
3070
3711
  }
3071
3712
  async function runServer() {
3072
3713
  const args = process.argv.slice(2);
3073
3714
  const cliAction = getCliAction(args);
3074
3715
  if (cliAction === "version") {
3075
- console.log(version);
3716
+ console.error(`${name} v${version}`);
3076
3717
  return;
3077
3718
  }
3078
3719
  if (cliAction === "help") {
@@ -3084,7 +3725,7 @@ async function runServer() {
3084
3725
  try {
3085
3726
  const apiKey = parseConfig(args, process.env).apiKey;
3086
3727
  assertApiKey(apiKey);
3087
- const server = buildServer(apiKey);
3728
+ const server = await createServer({ config: { apiKey } });
3088
3729
  console.error("Starting MCP server in stdio mode");
3089
3730
  const transport = createInstrumentedStdioTransport(new StdioServerTransport());
3090
3731
  await tracer.startActiveSpan("mcp.server.connect", { attributes: { "mcp.transport": "stdio" } }, async (connectSpan) => {
@@ -3098,6 +3739,11 @@ async function runServer() {
3098
3739
  connectSpan.end();
3099
3740
  }
3100
3741
  });
3742
+ scheduleUpdateCheck({
3743
+ packageName: name$1,
3744
+ currentVersion: version$1
3745
+ });
3746
+ installGracefulShutdown({ target: server });
3101
3747
  span.setStatus({ code: SpanStatusCode.OK });
3102
3748
  } catch (e) {
3103
3749
  span.setStatus({ code: SpanStatusCode.ERROR });
@@ -3110,4 +3756,4 @@ async function runServer() {
3110
3756
  //#endregion
3111
3757
  export { createServer as n, runServer as r, configSchema as t };
3112
3758
 
3113
- //# sourceMappingURL=src-J8OsyJcw.mjs.map
3759
+ //# sourceMappingURL=src-c3mB-R0m.mjs.map