hevy-mcp 1.17.3 → 1.18.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.
@@ -0,0 +1,1440 @@
1
+ #!/usr/bin/env node
2
+ // Generated with tsdown
3
+ // https://tsdown.dev
4
+ ;{try{(function(){var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a4412ce9-ddb4-48ee-be98-0cda1fedb287",e._sentryDebugIdIdentifier="sentry-dbid-a4412ce9-ddb4-48ee-be98-0cda1fedb287");})();}catch(e){}};!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"1.17.3"};}catch(e){}}();import dotenvx from "@dotenvx/dotenvx";
5
+ import * as Sentry from "@sentry/node";
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { z } from "zod";
9
+ import axios from "axios";
10
+ import fetch from "@kubb/plugin-client/clients/axios";
11
+
12
+ //#region src/utils/error-handler.ts
13
+ /**
14
+ * Specific error types for better categorization
15
+ */
16
+ let ErrorType = /* @__PURE__ */ function(ErrorType$1) {
17
+ ErrorType$1["API_ERROR"] = "API_ERROR";
18
+ ErrorType$1["VALIDATION_ERROR"] = "VALIDATION_ERROR";
19
+ ErrorType$1["NOT_FOUND"] = "NOT_FOUND";
20
+ ErrorType$1["NETWORK_ERROR"] = "NETWORK_ERROR";
21
+ ErrorType$1["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
22
+ return ErrorType$1;
23
+ }({});
24
+ /**
25
+ * Create a standardized error response for MCP tools
26
+ *
27
+ * @param error - The error object or message
28
+ * @param context - Optional context information about where the error occurred
29
+ * @returns A formatted MCP tool response with error information
30
+ */
31
+ function createErrorResponse(error, context) {
32
+ const errorMessage = error instanceof Error ? error.message : String(error);
33
+ const errorCode = error instanceof Error && "code" in error ? error.code : void 0;
34
+ const errorType = determineErrorType(error, errorMessage);
35
+ if (errorCode) console.debug(`Error code: ${errorCode}`);
36
+ const formattedMessage = `${context ? `[${context}] ` : ""}Error: ${errorMessage}`;
37
+ console.error(`${formattedMessage} (Type: ${errorType})`, error);
38
+ return {
39
+ content: [{
40
+ type: "text",
41
+ text: formattedMessage
42
+ }],
43
+ isError: true
44
+ };
45
+ }
46
+ /**
47
+ * Determine the type of error based on error characteristics
48
+ */
49
+ function determineErrorType(error, message) {
50
+ const messageLower = message.toLowerCase();
51
+ const nameLower = error instanceof Error ? error.name.toLowerCase() : "";
52
+ if (nameLower.includes("network") || messageLower.includes("network") || nameLower.includes("fetch") || messageLower.includes("fetch") || nameLower.includes("timeout") || messageLower.includes("timeout")) return ErrorType.NETWORK_ERROR;
53
+ if (nameLower.includes("validation") || messageLower.includes("validation") || messageLower.includes("invalid") || messageLower.includes("required")) return ErrorType.VALIDATION_ERROR;
54
+ if (messageLower.includes("not found") || messageLower.includes("404") || messageLower.includes("does not exist")) return ErrorType.NOT_FOUND;
55
+ if (nameLower.includes("api") || messageLower.includes("api") || messageLower.includes("server error") || messageLower.includes("500")) return ErrorType.API_ERROR;
56
+ return ErrorType.UNKNOWN_ERROR;
57
+ }
58
+ /**
59
+ * Wrap an async function with standardized error handling
60
+ *
61
+ * This function preserves the parameter types of the wrapped function while
62
+ * providing error handling. The returned function accepts Record<string, unknown>
63
+ * (as required by MCP SDK) but internally casts to the original parameter type.
64
+ *
65
+ * @param fn - The async function to wrap
66
+ * @param context - Context information for error messages
67
+ * @returns A function that catches errors and returns standardized error responses
68
+ */
69
+ function withErrorHandling(fn, context) {
70
+ return async (args) => {
71
+ try {
72
+ return await fn(args);
73
+ } catch (error) {
74
+ return createErrorResponse(error, context);
75
+ }
76
+ };
77
+ }
78
+
79
+ //#endregion
80
+ //#region src/utils/formatters.ts
81
+ /**
82
+ * Format a workout object for consistent presentation
83
+ *
84
+ * @param workout - The workout object from the API
85
+ * @returns A formatted workout object with standardized properties
86
+ */
87
+ function formatWorkout(workout) {
88
+ return {
89
+ id: workout.id,
90
+ title: workout.title,
91
+ description: workout.description,
92
+ startTime: workout.start_time,
93
+ endTime: workout.end_time,
94
+ createdAt: workout.created_at,
95
+ updatedAt: workout.updated_at,
96
+ duration: calculateDuration(workout.start_time, workout.end_time),
97
+ exercises: workout.exercises?.map((exercise) => {
98
+ return {
99
+ index: exercise.index,
100
+ name: exercise.title,
101
+ exerciseTemplateId: exercise.exercise_template_id,
102
+ notes: exercise.notes,
103
+ supersetsId: exercise.supersets_id,
104
+ sets: exercise.sets?.map((set) => ({
105
+ index: set.index,
106
+ type: set.type,
107
+ weight: set.weight_kg,
108
+ reps: set.reps,
109
+ distance: set.distance_meters,
110
+ duration: set.duration_seconds,
111
+ rpe: set.rpe,
112
+ customMetric: set.custom_metric
113
+ }))
114
+ };
115
+ })
116
+ };
117
+ }
118
+ /**
119
+ * Format a routine object for consistent presentation
120
+ *
121
+ * @param routine - The routine object from the API
122
+ * @returns A formatted routine object with standardized properties
123
+ */
124
+ function formatRoutine(routine) {
125
+ return {
126
+ id: routine.id,
127
+ title: routine.title,
128
+ folderId: routine.folder_id,
129
+ createdAt: routine.created_at,
130
+ updatedAt: routine.updated_at,
131
+ exercises: routine.exercises?.map((exercise) => {
132
+ return {
133
+ name: exercise.title,
134
+ index: exercise.index,
135
+ exerciseTemplateId: exercise.exercise_template_id,
136
+ notes: exercise.notes,
137
+ supersetId: exercise.supersets_id,
138
+ restSeconds: exercise.rest_seconds,
139
+ sets: exercise.sets?.map((set) => ({
140
+ index: set.index,
141
+ type: set.type,
142
+ weight: set.weight_kg,
143
+ reps: set.reps,
144
+ ...set.rep_range !== void 0 && { repRange: set.rep_range },
145
+ distance: set.distance_meters,
146
+ duration: set.duration_seconds,
147
+ ...set.rpe !== void 0 && { rpe: set.rpe },
148
+ customMetric: set.custom_metric
149
+ }))
150
+ };
151
+ })
152
+ };
153
+ }
154
+ /**
155
+ * Format a routine folder object for consistent presentation
156
+ *
157
+ * @param folder - The routine folder object from the API
158
+ * @returns A formatted routine folder object with standardized properties
159
+ */
160
+ function formatRoutineFolder(folder) {
161
+ return {
162
+ id: folder.id,
163
+ title: folder.title,
164
+ createdAt: folder.created_at,
165
+ updatedAt: folder.updated_at
166
+ };
167
+ }
168
+ /**
169
+ * Calculate duration between two ISO timestamp strings
170
+ *
171
+ * @param startTime - The start time as ISO string or timestamp
172
+ * @param endTime - The end time as ISO string or timestamp
173
+ * @returns A formatted duration string (e.g. "1h 30m 45s") or "Unknown duration" if inputs are invalid
174
+ */
175
+ function calculateDuration(startTime, endTime) {
176
+ if (!startTime || !endTime) return "Unknown duration";
177
+ try {
178
+ const start = new Date(startTime);
179
+ const end = new Date(endTime);
180
+ if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return "Unknown duration";
181
+ const durationMs = end.getTime() - start.getTime();
182
+ if (durationMs < 0) return "Invalid duration (end time before start time)";
183
+ return `${Math.floor(durationMs / (1e3 * 60 * 60))}h ${Math.floor(durationMs % (1e3 * 60 * 60) / (1e3 * 60))}m ${Math.floor(durationMs % (1e3 * 60) / 1e3)}s`;
184
+ } catch (error) {
185
+ console.error("Error calculating duration:", error);
186
+ return "Unknown duration";
187
+ }
188
+ }
189
+ /**
190
+ * Format an exercise template object for consistent presentation
191
+ *
192
+ * @param template - The exercise template object from the API
193
+ * @returns A formatted exercise template object with standardized properties
194
+ */
195
+ function formatExerciseTemplate(template) {
196
+ return {
197
+ id: template.id,
198
+ title: template.title,
199
+ type: template.type,
200
+ primaryMuscleGroup: template.primary_muscle_group,
201
+ secondaryMuscleGroups: template.secondary_muscle_groups,
202
+ isCustom: template.is_custom
203
+ };
204
+ }
205
+ function formatExerciseHistoryEntry(entry) {
206
+ return {
207
+ workoutId: entry.workout_id,
208
+ workoutTitle: entry.workout_title,
209
+ workoutStartTime: entry.workout_start_time,
210
+ workoutEndTime: entry.workout_end_time,
211
+ exerciseTemplateId: entry.exercise_template_id,
212
+ weight: entry.weight_kg,
213
+ reps: entry.reps,
214
+ distance: entry.distance_meters,
215
+ duration: entry.duration_seconds,
216
+ rpe: entry.rpe,
217
+ customMetric: entry.custom_metric,
218
+ setType: entry.set_type
219
+ };
220
+ }
221
+
222
+ //#endregion
223
+ //#region src/utils/response-formatter.ts
224
+ /**
225
+ * Create a standardized success response with JSON data
226
+ *
227
+ * @param data - The data to include in the response
228
+ * @param options - Formatting options
229
+ * @returns A formatted MCP tool response with the data as JSON
230
+ */
231
+ function createJsonResponse(data, options = {
232
+ pretty: true,
233
+ indent: 2
234
+ }) {
235
+ return { content: [{
236
+ type: "text",
237
+ text: options.pretty ? JSON.stringify(data, null, options.indent) : JSON.stringify(data)
238
+ }] };
239
+ }
240
+ /**
241
+ * Create a standardized success response for empty or null results
242
+ *
243
+ * @param message - Optional message to include (default: "No data found")
244
+ * @returns A formatted MCP tool response for empty results
245
+ */
246
+ function createEmptyResponse(message = "No data found") {
247
+ return { content: [{
248
+ type: "text",
249
+ text: message
250
+ }] };
251
+ }
252
+
253
+ //#endregion
254
+ //#region src/tools/folders.ts
255
+ /**
256
+ * Register all routine folder-related tools with the MCP server
257
+ */
258
+ function registerFolderTools(server, hevyClient) {
259
+ const getRoutineFoldersSchema = {
260
+ page: z.coerce.number().int().gte(1).default(1),
261
+ pageSize: z.coerce.number().int().gte(1).lte(10).default(5)
262
+ };
263
+ 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, withErrorHandling(async (args) => {
264
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
265
+ const { page, pageSize } = args;
266
+ const folders = (await hevyClient.getRoutineFolders({
267
+ page,
268
+ pageSize
269
+ }))?.routine_folders?.map((folder) => formatRoutineFolder(folder)) || [];
270
+ if (folders.length === 0) return createEmptyResponse("No routine folders found for the specified parameters");
271
+ return createJsonResponse(folders);
272
+ }, "get-routine-folders"));
273
+ const getRoutineFolderSchema = { folderId: z.string().min(1) };
274
+ server.tool("get-routine-folder", "Get complete details of a specific routine folder by its ID, including name, creation date, and associated routines.", getRoutineFolderSchema, withErrorHandling(async (args) => {
275
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
276
+ const { folderId } = args;
277
+ const data = await hevyClient.getRoutineFolder(folderId);
278
+ if (!data) return createEmptyResponse(`Routine folder with ID ${folderId} not found`);
279
+ return createJsonResponse(formatRoutineFolder(data));
280
+ }, "get-routine-folder"));
281
+ const createRoutineFolderSchema = { name: z.string().min(1) };
282
+ 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, withErrorHandling(async (args) => {
283
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
284
+ const { name: name$1 } = args;
285
+ const data = await hevyClient.createRoutineFolder({ routine_folder: { title: name$1 } });
286
+ if (!data) return createEmptyResponse("Failed to create routine folder: Server returned no data");
287
+ return createJsonResponse(formatRoutineFolder(data), {
288
+ pretty: true,
289
+ indent: 2
290
+ });
291
+ }, "create-routine-folder"));
292
+ }
293
+
294
+ //#endregion
295
+ //#region src/tools/routines.ts
296
+ /**
297
+ * Register all routine-related tools with the MCP server
298
+ */
299
+ function registerRoutineTools(server, hevyClient) {
300
+ const getRoutinesSchema = {
301
+ page: z.coerce.number().int().gte(1).default(1),
302
+ pageSize: z.coerce.number().int().gte(1).lte(10).default(5)
303
+ };
304
+ 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, withErrorHandling(async (args) => {
305
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
306
+ const { page, pageSize } = args;
307
+ const routines = (await hevyClient.getRoutines({
308
+ page,
309
+ pageSize
310
+ }))?.routines?.map((routine) => formatRoutine(routine)) || [];
311
+ if (routines.length === 0) return createEmptyResponse("No routines found for the specified parameters");
312
+ return createJsonResponse(routines);
313
+ }, "get-routines"));
314
+ const getRoutineSchema = { routineId: z.string().min(1) };
315
+ server.tool("get-routine", "Get a routine by its ID using the direct endpoint. Returns all details for the specified routine.", getRoutineSchema, withErrorHandling(async (args) => {
316
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
317
+ const { routineId } = args;
318
+ const data = await hevyClient.getRoutineById(String(routineId));
319
+ if (!data || !data.routine) return createEmptyResponse(`Routine with ID ${routineId} not found`);
320
+ return createJsonResponse(formatRoutine(data.routine));
321
+ }, "get-routine"));
322
+ const createRoutineSchema = {
323
+ title: z.string().min(1),
324
+ folderId: z.coerce.number().nullable().optional(),
325
+ notes: z.string().optional(),
326
+ exercises: z.array(z.object({
327
+ exerciseTemplateId: z.string().min(1),
328
+ supersetId: z.coerce.number().nullable().optional(),
329
+ restSeconds: z.coerce.number().int().min(0).optional(),
330
+ notes: z.string().optional(),
331
+ sets: z.array(z.object({
332
+ type: z.enum([
333
+ "warmup",
334
+ "normal",
335
+ "failure",
336
+ "dropset"
337
+ ]).default("normal"),
338
+ weight: z.coerce.number().optional(),
339
+ weightKg: z.coerce.number().optional(),
340
+ reps: z.coerce.number().int().optional(),
341
+ distance: z.coerce.number().int().optional(),
342
+ distanceMeters: z.coerce.number().int().optional(),
343
+ duration: z.coerce.number().int().optional(),
344
+ durationSeconds: z.coerce.number().int().optional(),
345
+ customMetric: z.coerce.number().optional(),
346
+ repRange: z.object({
347
+ start: z.coerce.number().int().optional(),
348
+ end: z.coerce.number().int().optional()
349
+ }).optional()
350
+ }))
351
+ }))
352
+ };
353
+ 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, withErrorHandling(async (args) => {
354
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
355
+ const { title, folderId, notes, exercises } = args;
356
+ const data = await hevyClient.createRoutine({ routine: {
357
+ title,
358
+ folder_id: folderId ?? null,
359
+ notes: notes ?? "",
360
+ exercises: exercises.map((exercise) => ({
361
+ exercise_template_id: exercise.exerciseTemplateId,
362
+ superset_id: exercise.supersetId ?? null,
363
+ rest_seconds: exercise.restSeconds ?? null,
364
+ notes: exercise.notes ?? null,
365
+ sets: exercise.sets.map((set) => ({
366
+ type: set.type,
367
+ weight_kg: set.weight ?? set.weightKg ?? null,
368
+ reps: set.reps ?? null,
369
+ distance_meters: set.distance ?? set.distanceMeters ?? null,
370
+ duration_seconds: set.duration ?? set.durationSeconds ?? null,
371
+ custom_metric: set.customMetric ?? null,
372
+ rep_range: set.repRange ? {
373
+ start: set.repRange.start ?? null,
374
+ end: set.repRange.end ?? null
375
+ } : null
376
+ }))
377
+ }))
378
+ } });
379
+ if (!data) return createEmptyResponse("Failed to create routine: Server returned no data");
380
+ return createJsonResponse(formatRoutine(data), {
381
+ pretty: true,
382
+ indent: 2
383
+ });
384
+ }, "create-routine"));
385
+ const updateRoutineSchema = {
386
+ routineId: z.string().min(1),
387
+ title: z.string().min(1),
388
+ notes: z.string().optional(),
389
+ exercises: z.array(z.object({
390
+ exerciseTemplateId: z.string().min(1),
391
+ supersetId: z.coerce.number().nullable().optional(),
392
+ restSeconds: z.coerce.number().int().min(0).optional(),
393
+ notes: z.string().optional(),
394
+ sets: z.array(z.object({
395
+ type: z.enum([
396
+ "warmup",
397
+ "normal",
398
+ "failure",
399
+ "dropset"
400
+ ]).default("normal"),
401
+ weight: z.coerce.number().optional(),
402
+ weightKg: z.coerce.number().optional(),
403
+ reps: z.coerce.number().int().optional(),
404
+ distance: z.coerce.number().int().optional(),
405
+ distanceMeters: z.coerce.number().int().optional(),
406
+ duration: z.coerce.number().int().optional(),
407
+ durationSeconds: z.coerce.number().int().optional(),
408
+ customMetric: z.coerce.number().optional(),
409
+ repRange: z.object({
410
+ start: z.coerce.number().int().optional(),
411
+ end: z.coerce.number().int().optional()
412
+ }).optional()
413
+ }))
414
+ }))
415
+ };
416
+ 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, withErrorHandling(async (args) => {
417
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
418
+ const { routineId, title, notes, exercises } = args;
419
+ const data = await hevyClient.updateRoutine(routineId, { routine: {
420
+ title,
421
+ notes: notes ?? null,
422
+ exercises: exercises.map((exercise) => ({
423
+ exercise_template_id: exercise.exerciseTemplateId,
424
+ superset_id: exercise.supersetId ?? null,
425
+ rest_seconds: exercise.restSeconds ?? null,
426
+ notes: exercise.notes ?? null,
427
+ sets: exercise.sets.map((set) => ({
428
+ type: set.type,
429
+ weight_kg: set.weight ?? set.weightKg ?? null,
430
+ reps: set.reps ?? null,
431
+ distance_meters: set.distance ?? set.distanceMeters ?? null,
432
+ duration_seconds: set.duration ?? set.durationSeconds ?? null,
433
+ custom_metric: set.customMetric ?? null,
434
+ rep_range: set.repRange ? {
435
+ start: set.repRange.start ?? null,
436
+ end: set.repRange.end ?? null
437
+ } : null
438
+ }))
439
+ }))
440
+ } });
441
+ if (!data) return createEmptyResponse(`Failed to update routine with ID ${routineId}`);
442
+ return createJsonResponse(formatRoutine(data), {
443
+ pretty: true,
444
+ indent: 2
445
+ });
446
+ }, "update-routine"));
447
+ }
448
+
449
+ //#endregion
450
+ //#region src/tools/templates.ts
451
+ /**
452
+ * Register all exercise template-related tools with the MCP server
453
+ */
454
+ function registerTemplateTools(server, hevyClient) {
455
+ const getExerciseTemplatesSchema = {
456
+ page: z.coerce.number().int().gte(1).default(1),
457
+ pageSize: z.coerce.number().int().gte(1).lte(100).default(5)
458
+ };
459
+ 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, withErrorHandling(async (args) => {
460
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
461
+ const { page, pageSize } = args;
462
+ const templates = (await hevyClient.getExerciseTemplates({
463
+ page,
464
+ pageSize
465
+ }))?.exercise_templates?.map((template) => formatExerciseTemplate(template)) || [];
466
+ if (templates.length === 0) return createEmptyResponse("No exercise templates found for the specified parameters");
467
+ return createJsonResponse(templates);
468
+ }, "get-exercise-templates"));
469
+ const getExerciseTemplateSchema = { exerciseTemplateId: z.string().min(1) };
470
+ 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, withErrorHandling(async (args) => {
471
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
472
+ const { exerciseTemplateId } = args;
473
+ const data = await hevyClient.getExerciseTemplate(exerciseTemplateId);
474
+ if (!data) return createEmptyResponse(`Exercise template with ID ${exerciseTemplateId} not found`);
475
+ return createJsonResponse(formatExerciseTemplate(data));
476
+ }, "get-exercise-template"));
477
+ const getExerciseHistorySchema = {
478
+ exerciseTemplateId: z.string().min(1),
479
+ startDate: z.string().datetime({ offset: true }).describe("ISO 8601 start date for filtering history").optional(),
480
+ endDate: z.string().datetime({ offset: true }).describe("ISO 8601 end date for filtering history").optional()
481
+ };
482
+ server.tool("get-exercise-history", "Get past sets for a specific exercise template, optionally filtered by start and end dates.", getExerciseHistorySchema, withErrorHandling(async (args) => {
483
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
484
+ const { exerciseTemplateId, startDate, endDate } = args;
485
+ const history = (await hevyClient.getExerciseHistory(exerciseTemplateId, {
486
+ ...startDate ? { start_date: startDate } : {},
487
+ ...endDate ? { end_date: endDate } : {}
488
+ }))?.exercise_history?.map((entry) => formatExerciseHistoryEntry(entry)) || [];
489
+ if (history.length === 0) return createEmptyResponse(`No exercise history found for template ${exerciseTemplateId}`);
490
+ return createJsonResponse(history);
491
+ }, "get-exercise-history"));
492
+ const createExerciseTemplateSchema = {
493
+ title: z.string().min(1),
494
+ exerciseType: z.enum([
495
+ "weight_reps",
496
+ "reps_only",
497
+ "bodyweight_reps",
498
+ "bodyweight_assisted_reps",
499
+ "duration",
500
+ "weight_duration",
501
+ "distance_duration",
502
+ "short_distance_weight"
503
+ ]),
504
+ equipmentCategory: z.enum([
505
+ "none",
506
+ "barbell",
507
+ "dumbbell",
508
+ "kettlebell",
509
+ "machine",
510
+ "plate",
511
+ "resistance_band",
512
+ "suspension",
513
+ "other"
514
+ ]),
515
+ muscleGroup: z.enum([
516
+ "abdominals",
517
+ "shoulders",
518
+ "biceps",
519
+ "triceps",
520
+ "forearms",
521
+ "quadriceps",
522
+ "hamstrings",
523
+ "calves",
524
+ "glutes",
525
+ "abductors",
526
+ "adductors",
527
+ "lats",
528
+ "upper_back",
529
+ "traps",
530
+ "lower_back",
531
+ "chest",
532
+ "cardio",
533
+ "neck",
534
+ "full_body",
535
+ "other"
536
+ ]),
537
+ otherMuscles: z.array(z.enum([
538
+ "abdominals",
539
+ "shoulders",
540
+ "biceps",
541
+ "triceps",
542
+ "forearms",
543
+ "quadriceps",
544
+ "hamstrings",
545
+ "calves",
546
+ "glutes",
547
+ "abductors",
548
+ "adductors",
549
+ "lats",
550
+ "upper_back",
551
+ "traps",
552
+ "lower_back",
553
+ "chest",
554
+ "cardio",
555
+ "neck",
556
+ "full_body",
557
+ "other"
558
+ ])).default([])
559
+ };
560
+ server.tool("create-exercise-template", "Create a custom exercise template with title, type, equipment, and muscle groups.", createExerciseTemplateSchema, withErrorHandling(async (args) => {
561
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
562
+ const { title, exerciseType, equipmentCategory, muscleGroup, otherMuscles } = args;
563
+ return createJsonResponse({
564
+ id: (await hevyClient.createExerciseTemplate({ exercise: {
565
+ title,
566
+ exercise_type: exerciseType,
567
+ equipment_category: equipmentCategory,
568
+ muscle_group: muscleGroup,
569
+ other_muscles: otherMuscles
570
+ } }))?.id,
571
+ message: "Exercise template created successfully"
572
+ });
573
+ }, "create-exercise-template"));
574
+ }
575
+
576
+ //#endregion
577
+ //#region src/tools/webhooks.ts
578
+ const webhookUrlSchema = z.string().url().refine((url) => {
579
+ try {
580
+ const parsed = new URL(url);
581
+ return parsed.protocol === "https:" || parsed.protocol === "http:";
582
+ } catch {
583
+ return false;
584
+ }
585
+ }, { message: "Webhook URL must be a valid HTTP or HTTPS URL" }).refine((url) => {
586
+ try {
587
+ const parsed = new URL(url);
588
+ return parsed.hostname !== "localhost" && !parsed.hostname.startsWith("127.");
589
+ } catch {
590
+ return false;
591
+ }
592
+ }, { message: "Webhook URL cannot be localhost or loopback address" });
593
+ function registerWebhookTools(server, hevyClient) {
594
+ server.tool("get-webhook-subscription", "Get the current webhook subscription for this account. Returns the webhook URL and auth token if a subscription exists.", {}, withErrorHandling(async (_args) => {
595
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
596
+ if (!hevyClient.getWebhookSubscription) throw new Error("Webhook subscription API not available. Please regenerate the client from the updated OpenAPI spec.");
597
+ const data = await hevyClient.getWebhookSubscription();
598
+ if (!data) return createEmptyResponse("No webhook subscription found for this account");
599
+ return createJsonResponse(data);
600
+ }, "get-webhook-subscription"));
601
+ const createWebhookSubscriptionSchema = {
602
+ url: webhookUrlSchema.describe("The webhook URL that will receive POST requests when workouts are created"),
603
+ authToken: z.string().optional().describe("Optional auth token that will be sent as Authorization header in webhook requests")
604
+ };
605
+ server.tool("create-webhook-subscription", "Create a new webhook subscription for this account. The webhook will receive POST requests when workouts are created. Your endpoint must respond with 200 OK within 5 seconds.", createWebhookSubscriptionSchema, withErrorHandling(async (args) => {
606
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
607
+ const { url, authToken } = args;
608
+ if (!hevyClient.createWebhookSubscription) throw new Error("Webhook subscription API not available. Please regenerate the client from the updated OpenAPI spec.");
609
+ const data = await hevyClient.createWebhookSubscription({ webhook: {
610
+ url,
611
+ authToken: authToken || null
612
+ } });
613
+ if (!data) return createEmptyResponse("Failed to create webhook subscription - please check your URL and try again");
614
+ return createJsonResponse(data);
615
+ }, "create-webhook-subscription"));
616
+ server.tool("delete-webhook-subscription", "Delete the current webhook subscription for this account. This will stop all webhook notifications.", {}, withErrorHandling(async (_args) => {
617
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
618
+ if (!hevyClient.deleteWebhookSubscription) throw new Error("Webhook subscription API not available. Please regenerate the client from the updated OpenAPI spec.");
619
+ const data = await hevyClient.deleteWebhookSubscription();
620
+ if (!data) return createEmptyResponse("Failed to delete webhook subscription - no subscription may exist or there was a server error");
621
+ return createJsonResponse(data);
622
+ }, "delete-webhook-subscription"));
623
+ }
624
+
625
+ //#endregion
626
+ //#region src/tools/workouts.ts
627
+ /**
628
+ * Register all workout-related tools with the MCP server
629
+ */
630
+ function registerWorkoutTools(server, hevyClient) {
631
+ const getWorkoutsSchema = {
632
+ page: z.coerce.number().gte(1).default(1),
633
+ pageSize: z.coerce.number().int().gte(1).lte(10).default(5)
634
+ };
635
+ 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, withErrorHandling(async (args) => {
636
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
637
+ const { page, pageSize } = args;
638
+ const workouts = (await hevyClient.getWorkouts({
639
+ page,
640
+ pageSize
641
+ }))?.workouts?.map((workout) => formatWorkout(workout)) || [];
642
+ if (workouts.length === 0) return createEmptyResponse("No workouts found for the specified parameters");
643
+ return createJsonResponse(workouts);
644
+ }, "get-workouts"));
645
+ const getWorkoutSchema = { workoutId: z.string().min(1) };
646
+ 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, withErrorHandling(async (args) => {
647
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
648
+ const { workoutId } = args;
649
+ const data = await hevyClient.getWorkout(workoutId);
650
+ if (!data) return createEmptyResponse(`Workout with ID ${workoutId} not found`);
651
+ return createJsonResponse(formatWorkout(data));
652
+ }, "get-workout"));
653
+ server.tool("get-workout-count", "Get the total number of workouts on the account. Useful for pagination or statistics.", {}, withErrorHandling(async () => {
654
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
655
+ const data = await hevyClient.getWorkoutCount();
656
+ return createJsonResponse({ count: data ? data.workoutCount || 0 : 0 });
657
+ }, "get-workout-count"));
658
+ const getWorkoutEventsSchema = {
659
+ page: z.coerce.number().int().gte(1).default(1),
660
+ pageSize: z.coerce.number().int().gte(1).lte(10).default(5),
661
+ since: z.string().default("1970-01-01T00:00:00Z")
662
+ };
663
+ 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, withErrorHandling(async (args) => {
664
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
665
+ const { page, pageSize, since } = args;
666
+ const events = (await hevyClient.getWorkoutEvents({
667
+ page,
668
+ pageSize,
669
+ since
670
+ }))?.events || [];
671
+ if (events.length === 0) return createEmptyResponse(`No workout events found for the specified parameters since ${since}`);
672
+ return createJsonResponse(events);
673
+ }, "get-workout-events"));
674
+ const createWorkoutSchema = {
675
+ title: z.string().min(1),
676
+ description: z.string().optional().nullable(),
677
+ startTime: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/),
678
+ endTime: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/),
679
+ routineId: z.string().optional().nullable(),
680
+ isPrivate: z.boolean().default(false),
681
+ exercises: z.array(z.object({
682
+ exerciseTemplateId: z.string().min(1),
683
+ supersetId: z.coerce.number().nullable().optional(),
684
+ notes: z.string().optional().nullable(),
685
+ sets: z.array(z.object({
686
+ type: z.enum([
687
+ "warmup",
688
+ "normal",
689
+ "failure",
690
+ "dropset"
691
+ ]).default("normal"),
692
+ weight: z.coerce.number().optional().nullable(),
693
+ weightKg: z.coerce.number().optional().nullable(),
694
+ reps: z.coerce.number().int().optional().nullable(),
695
+ distance: z.coerce.number().int().optional().nullable(),
696
+ distanceMeters: z.coerce.number().int().optional().nullable(),
697
+ duration: z.coerce.number().int().optional().nullable(),
698
+ durationSeconds: z.coerce.number().int().optional().nullable(),
699
+ rpe: z.coerce.number().optional().nullable(),
700
+ customMetric: z.coerce.number().optional().nullable()
701
+ }))
702
+ }))
703
+ };
704
+ 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, withErrorHandling(async (args) => {
705
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
706
+ const { title, description, startTime, endTime, isPrivate, exercises } = args;
707
+ const requestBody = { workout: {
708
+ title,
709
+ description: description || null,
710
+ start_time: startTime,
711
+ end_time: endTime,
712
+ routine_id: args.routineId ?? null,
713
+ is_private: isPrivate,
714
+ exercises: exercises.map((exercise) => ({
715
+ exercise_template_id: exercise.exerciseTemplateId,
716
+ superset_id: exercise.supersetId ?? null,
717
+ notes: exercise.notes ?? null,
718
+ sets: exercise.sets.map((set) => ({
719
+ type: set.type,
720
+ weight_kg: set.weight ?? set.weightKg ?? null,
721
+ reps: set.reps ?? null,
722
+ distance_meters: set.distance ?? set.distanceMeters ?? null,
723
+ duration_seconds: set.duration ?? set.durationSeconds ?? null,
724
+ rpe: set.rpe ?? null,
725
+ custom_metric: set.customMetric ?? null
726
+ }))
727
+ }))
728
+ } };
729
+ const data = await hevyClient.createWorkout(requestBody);
730
+ if (!data) return createEmptyResponse("Failed to create workout: Server returned no data");
731
+ return createJsonResponse(formatWorkout(data), {
732
+ pretty: true,
733
+ indent: 2
734
+ });
735
+ }, "create-workout"));
736
+ const updateWorkoutSchema = {
737
+ workoutId: z.string().min(1),
738
+ title: z.string().min(1),
739
+ description: z.string().optional().nullable(),
740
+ startTime: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/),
741
+ endTime: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/),
742
+ routineId: z.string().optional().nullable(),
743
+ isPrivate: z.boolean().default(false),
744
+ exercises: z.array(z.object({
745
+ exerciseTemplateId: z.string().min(1),
746
+ supersetId: z.coerce.number().nullable().optional(),
747
+ notes: z.string().optional().nullable(),
748
+ sets: z.array(z.object({
749
+ type: z.enum([
750
+ "warmup",
751
+ "normal",
752
+ "failure",
753
+ "dropset"
754
+ ]).default("normal"),
755
+ weight: z.coerce.number().optional().nullable(),
756
+ weightKg: z.coerce.number().optional().nullable(),
757
+ reps: z.coerce.number().int().optional().nullable(),
758
+ distance: z.coerce.number().int().optional().nullable(),
759
+ distanceMeters: z.coerce.number().int().optional().nullable(),
760
+ duration: z.coerce.number().int().optional().nullable(),
761
+ durationSeconds: z.coerce.number().int().optional().nullable(),
762
+ rpe: z.coerce.number().optional().nullable(),
763
+ customMetric: z.coerce.number().optional().nullable()
764
+ }))
765
+ }))
766
+ };
767
+ 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, withErrorHandling(async (args) => {
768
+ if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
769
+ const { workoutId, title, description, startTime, endTime, routineId, isPrivate, exercises } = args;
770
+ const requestBody = { workout: {
771
+ title,
772
+ description: description || null,
773
+ start_time: startTime,
774
+ end_time: endTime,
775
+ routine_id: routineId ?? null,
776
+ is_private: isPrivate,
777
+ exercises: exercises.map((exercise) => ({
778
+ exercise_template_id: exercise.exerciseTemplateId,
779
+ superset_id: exercise.supersetId ?? null,
780
+ notes: exercise.notes ?? null,
781
+ sets: exercise.sets.map((set) => ({
782
+ type: set.type,
783
+ weight_kg: set.weight ?? set.weightKg ?? null,
784
+ reps: set.reps ?? null,
785
+ distance_meters: set.distance ?? set.distanceMeters ?? null,
786
+ duration_seconds: set.duration ?? set.durationSeconds ?? null,
787
+ rpe: set.rpe ?? null,
788
+ custom_metric: set.customMetric ?? null
789
+ }))
790
+ }))
791
+ } };
792
+ const data = await hevyClient.updateWorkout(workoutId, requestBody);
793
+ if (!data) return createEmptyResponse(`Failed to update workout with ID ${workoutId}`);
794
+ return createJsonResponse(formatWorkout(data), {
795
+ pretty: true,
796
+ indent: 2
797
+ });
798
+ }, "update-workout-operation"));
799
+ }
800
+
801
+ //#endregion
802
+ //#region src/utils/config.ts
803
+ /**
804
+ * Parse CLI arguments and environment to derive configuration.
805
+ * Priority order for API key: CLI flag forms > environment variable.
806
+ * Supported CLI arg forms:
807
+ * --hevy-api-key=KEY
808
+ * --hevyApiKey=KEY
809
+ * hevy-api-key=KEY (bare, e.g. when passed after npm start -- )
810
+ */
811
+ function parseConfig(argv, env) {
812
+ let apiKey = "";
813
+ const apiKeyArgPatterns = [
814
+ /^--hevy-api-key=(.+)$/i,
815
+ /^--hevyApiKey=(.+)$/i,
816
+ /^hevy-api-key=(.+)$/i
817
+ ];
818
+ for (const raw of argv) {
819
+ for (const pattern of apiKeyArgPatterns) {
820
+ const m = raw.match(pattern);
821
+ if (m) {
822
+ apiKey = m[1];
823
+ break;
824
+ }
825
+ }
826
+ if (apiKey) break;
827
+ }
828
+ if (!apiKey) apiKey = env.HEVY_API_KEY || "";
829
+ return { apiKey };
830
+ }
831
+ function assertApiKey(apiKey) {
832
+ if (!apiKey) {
833
+ console.error("Hevy API key is required. Provide it via the HEVY_API_KEY environment variable or the --hevy-api-key=YOUR_KEY command argument.");
834
+ process.exit(1);
835
+ }
836
+ }
837
+
838
+ //#endregion
839
+ //#region src/generated/client/api/getV1ExerciseHistoryExercisetemplateid.ts
840
+ /**
841
+ * Generated by Kubb (https://kubb.dev/).
842
+ * Do not edit manually.
843
+ */
844
+ function getGetV1ExerciseHistoryExercisetemplateidUrl(exerciseTemplateId) {
845
+ return {
846
+ method: "GET",
847
+ url: `/v1/exercise_history/${exerciseTemplateId}`
848
+ };
849
+ }
850
+ /**
851
+ * @summary Get exercise history for a specific exercise template
852
+ * {@link /v1/exercise_history/:exerciseTemplateId}
853
+ */
854
+ async function getV1ExerciseHistoryExercisetemplateid(exerciseTemplateId, headers, params, config = {}) {
855
+ const { client: request = fetch, ...requestConfig } = config;
856
+ return (await request({
857
+ method: "GET",
858
+ url: getGetV1ExerciseHistoryExercisetemplateidUrl(exerciseTemplateId).url.toString(),
859
+ params,
860
+ ...requestConfig,
861
+ headers: {
862
+ ...headers,
863
+ ...requestConfig.headers
864
+ }
865
+ })).data;
866
+ }
867
+
868
+ //#endregion
869
+ //#region src/generated/client/api/getV1ExerciseTemplates.ts
870
+ /**
871
+ * Generated by Kubb (https://kubb.dev/).
872
+ * Do not edit manually.
873
+ */
874
+ function getGetV1ExerciseTemplatesUrl() {
875
+ return {
876
+ method: "GET",
877
+ url: `/v1/exercise_templates`
878
+ };
879
+ }
880
+ /**
881
+ * @summary Get a paginated list of exercise templates available on the account.
882
+ * {@link /v1/exercise_templates}
883
+ */
884
+ async function getV1ExerciseTemplates(headers, params, config = {}) {
885
+ const { client: request = fetch, ...requestConfig } = config;
886
+ return (await request({
887
+ method: "GET",
888
+ url: getGetV1ExerciseTemplatesUrl().url.toString(),
889
+ params,
890
+ ...requestConfig,
891
+ headers: {
892
+ ...headers,
893
+ ...requestConfig.headers
894
+ }
895
+ })).data;
896
+ }
897
+
898
+ //#endregion
899
+ //#region src/generated/client/api/getV1ExerciseTemplatesExercisetemplateid.ts
900
+ /**
901
+ * Generated by Kubb (https://kubb.dev/).
902
+ * Do not edit manually.
903
+ */
904
+ function getGetV1ExerciseTemplatesExercisetemplateidUrl(exerciseTemplateId) {
905
+ return {
906
+ method: "GET",
907
+ url: `/v1/exercise_templates/${exerciseTemplateId}`
908
+ };
909
+ }
910
+ /**
911
+ * @summary Get a single exercise template by id.
912
+ * {@link /v1/exercise_templates/:exerciseTemplateId}
913
+ */
914
+ async function getV1ExerciseTemplatesExercisetemplateid(exerciseTemplateId, headers, config = {}) {
915
+ const { client: request = fetch, ...requestConfig } = config;
916
+ return (await request({
917
+ method: "GET",
918
+ url: getGetV1ExerciseTemplatesExercisetemplateidUrl(exerciseTemplateId).url.toString(),
919
+ ...requestConfig,
920
+ headers: {
921
+ ...headers,
922
+ ...requestConfig.headers
923
+ }
924
+ })).data;
925
+ }
926
+
927
+ //#endregion
928
+ //#region src/generated/client/api/getV1RoutineFolders.ts
929
+ /**
930
+ * Generated by Kubb (https://kubb.dev/).
931
+ * Do not edit manually.
932
+ */
933
+ function getGetV1RoutineFoldersUrl() {
934
+ return {
935
+ method: "GET",
936
+ url: `/v1/routine_folders`
937
+ };
938
+ }
939
+ /**
940
+ * @summary Get a paginated list of routine folders available on the account.
941
+ * {@link /v1/routine_folders}
942
+ */
943
+ async function getV1RoutineFolders(headers, params, config = {}) {
944
+ const { client: request = fetch, ...requestConfig } = config;
945
+ return (await request({
946
+ method: "GET",
947
+ url: getGetV1RoutineFoldersUrl().url.toString(),
948
+ params,
949
+ ...requestConfig,
950
+ headers: {
951
+ ...headers,
952
+ ...requestConfig.headers
953
+ }
954
+ })).data;
955
+ }
956
+
957
+ //#endregion
958
+ //#region src/generated/client/api/getV1RoutineFoldersFolderid.ts
959
+ /**
960
+ * Generated by Kubb (https://kubb.dev/).
961
+ * Do not edit manually.
962
+ */
963
+ function getGetV1RoutineFoldersFolderidUrl(folderId) {
964
+ return {
965
+ method: "GET",
966
+ url: `/v1/routine_folders/${folderId}`
967
+ };
968
+ }
969
+ /**
970
+ * @summary Get a single routine folder by id.
971
+ * {@link /v1/routine_folders/:folderId}
972
+ */
973
+ async function getV1RoutineFoldersFolderid(folderId, headers, config = {}) {
974
+ const { client: request = fetch, ...requestConfig } = config;
975
+ return (await request({
976
+ method: "GET",
977
+ url: getGetV1RoutineFoldersFolderidUrl(folderId).url.toString(),
978
+ ...requestConfig,
979
+ headers: {
980
+ ...headers,
981
+ ...requestConfig.headers
982
+ }
983
+ })).data;
984
+ }
985
+
986
+ //#endregion
987
+ //#region src/generated/client/api/getV1Routines.ts
988
+ /**
989
+ * Generated by Kubb (https://kubb.dev/).
990
+ * Do not edit manually.
991
+ */
992
+ function getGetV1RoutinesUrl() {
993
+ return {
994
+ method: "GET",
995
+ url: `/v1/routines`
996
+ };
997
+ }
998
+ /**
999
+ * @summary Get a paginated list of routines
1000
+ * {@link /v1/routines}
1001
+ */
1002
+ async function getV1Routines(headers, params, config = {}) {
1003
+ const { client: request = fetch, ...requestConfig } = config;
1004
+ return (await request({
1005
+ method: "GET",
1006
+ url: getGetV1RoutinesUrl().url.toString(),
1007
+ params,
1008
+ ...requestConfig,
1009
+ headers: {
1010
+ ...headers,
1011
+ ...requestConfig.headers
1012
+ }
1013
+ })).data;
1014
+ }
1015
+
1016
+ //#endregion
1017
+ //#region src/generated/client/api/getV1RoutinesRoutineid.ts
1018
+ /**
1019
+ * Generated by Kubb (https://kubb.dev/).
1020
+ * Do not edit manually.
1021
+ */
1022
+ function getGetV1RoutinesRoutineidUrl(routineId) {
1023
+ return {
1024
+ method: "GET",
1025
+ url: `/v1/routines/${routineId}`
1026
+ };
1027
+ }
1028
+ /**
1029
+ * @summary Get a routine by its Id
1030
+ * {@link /v1/routines/:routineId}
1031
+ */
1032
+ async function getV1RoutinesRoutineid(routineId, headers, config = {}) {
1033
+ const { client: request = fetch, ...requestConfig } = config;
1034
+ return (await request({
1035
+ method: "GET",
1036
+ url: getGetV1RoutinesRoutineidUrl(routineId).url.toString(),
1037
+ ...requestConfig,
1038
+ headers: {
1039
+ ...headers,
1040
+ ...requestConfig.headers
1041
+ }
1042
+ })).data;
1043
+ }
1044
+
1045
+ //#endregion
1046
+ //#region src/generated/client/api/getV1Workouts.ts
1047
+ /**
1048
+ * Generated by Kubb (https://kubb.dev/).
1049
+ * Do not edit manually.
1050
+ */
1051
+ function getGetV1WorkoutsUrl() {
1052
+ return {
1053
+ method: "GET",
1054
+ url: `/v1/workouts`
1055
+ };
1056
+ }
1057
+ /**
1058
+ * @summary Get a paginated list of workouts
1059
+ * {@link /v1/workouts}
1060
+ */
1061
+ async function getV1Workouts(headers, params, config = {}) {
1062
+ const { client: request = fetch, ...requestConfig } = config;
1063
+ return (await request({
1064
+ method: "GET",
1065
+ url: getGetV1WorkoutsUrl().url.toString(),
1066
+ params,
1067
+ ...requestConfig,
1068
+ headers: {
1069
+ ...headers,
1070
+ ...requestConfig.headers
1071
+ }
1072
+ })).data;
1073
+ }
1074
+
1075
+ //#endregion
1076
+ //#region src/generated/client/api/getV1WorkoutsCount.ts
1077
+ /**
1078
+ * Generated by Kubb (https://kubb.dev/).
1079
+ * Do not edit manually.
1080
+ */
1081
+ function getGetV1WorkoutsCountUrl() {
1082
+ return {
1083
+ method: "GET",
1084
+ url: `/v1/workouts/count`
1085
+ };
1086
+ }
1087
+ /**
1088
+ * @summary Get the total number of workouts on the account
1089
+ * {@link /v1/workouts/count}
1090
+ */
1091
+ async function getV1WorkoutsCount(headers, config = {}) {
1092
+ const { client: request = fetch, ...requestConfig } = config;
1093
+ return (await request({
1094
+ method: "GET",
1095
+ url: getGetV1WorkoutsCountUrl().url.toString(),
1096
+ ...requestConfig,
1097
+ headers: {
1098
+ ...headers,
1099
+ ...requestConfig.headers
1100
+ }
1101
+ })).data;
1102
+ }
1103
+
1104
+ //#endregion
1105
+ //#region src/generated/client/api/getV1WorkoutsEvents.ts
1106
+ /**
1107
+ * Generated by Kubb (https://kubb.dev/).
1108
+ * Do not edit manually.
1109
+ */
1110
+ function getGetV1WorkoutsEventsUrl() {
1111
+ return {
1112
+ method: "GET",
1113
+ url: `/v1/workouts/events`
1114
+ };
1115
+ }
1116
+ /**
1117
+ * @description Returns a paginated array of workout events, indicating updates or deletions.
1118
+ * @summary 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.
1119
+ * {@link /v1/workouts/events}
1120
+ */
1121
+ async function getV1WorkoutsEvents(headers, params, config = {}) {
1122
+ const { client: request = fetch, ...requestConfig } = config;
1123
+ return (await request({
1124
+ method: "GET",
1125
+ url: getGetV1WorkoutsEventsUrl().url.toString(),
1126
+ params,
1127
+ ...requestConfig,
1128
+ headers: {
1129
+ ...headers,
1130
+ ...requestConfig.headers
1131
+ }
1132
+ })).data;
1133
+ }
1134
+
1135
+ //#endregion
1136
+ //#region src/generated/client/api/getV1WorkoutsWorkoutid.ts
1137
+ /**
1138
+ * Generated by Kubb (https://kubb.dev/).
1139
+ * Do not edit manually.
1140
+ */
1141
+ function getGetV1WorkoutsWorkoutidUrl(workoutId) {
1142
+ return {
1143
+ method: "GET",
1144
+ url: `/v1/workouts/${workoutId}`
1145
+ };
1146
+ }
1147
+ /**
1148
+ * @summary Get a single workout’s complete details by the workoutId
1149
+ * {@link /v1/workouts/:workoutId}
1150
+ */
1151
+ async function getV1WorkoutsWorkoutid(workoutId, headers, config = {}) {
1152
+ const { client: request = fetch, ...requestConfig } = config;
1153
+ return (await request({
1154
+ method: "GET",
1155
+ url: getGetV1WorkoutsWorkoutidUrl(workoutId).url.toString(),
1156
+ ...requestConfig,
1157
+ headers: {
1158
+ ...headers,
1159
+ ...requestConfig.headers
1160
+ }
1161
+ })).data;
1162
+ }
1163
+
1164
+ //#endregion
1165
+ //#region src/generated/client/api/postV1ExerciseTemplates.ts
1166
+ /**
1167
+ * Generated by Kubb (https://kubb.dev/).
1168
+ * Do not edit manually.
1169
+ */
1170
+ function getPostV1ExerciseTemplatesUrl() {
1171
+ return {
1172
+ method: "POST",
1173
+ url: `/v1/exercise_templates`
1174
+ };
1175
+ }
1176
+ /**
1177
+ * @summary Create a new custom exercise template.
1178
+ * {@link /v1/exercise_templates}
1179
+ */
1180
+ async function postV1ExerciseTemplates(headers, data, config = {}) {
1181
+ const { client: request = fetch, ...requestConfig } = config;
1182
+ const requestData = data;
1183
+ return (await request({
1184
+ method: "POST",
1185
+ url: getPostV1ExerciseTemplatesUrl().url.toString(),
1186
+ data: requestData,
1187
+ ...requestConfig,
1188
+ headers: {
1189
+ ...headers,
1190
+ ...requestConfig.headers
1191
+ }
1192
+ })).data;
1193
+ }
1194
+
1195
+ //#endregion
1196
+ //#region src/generated/client/api/postV1RoutineFolders.ts
1197
+ /**
1198
+ * Generated by Kubb (https://kubb.dev/).
1199
+ * Do not edit manually.
1200
+ */
1201
+ function getPostV1RoutineFoldersUrl() {
1202
+ return {
1203
+ method: "POST",
1204
+ url: `/v1/routine_folders`
1205
+ };
1206
+ }
1207
+ /**
1208
+ * @summary Create a new routine folder. The folder will be created at index 0, and all other folders will have their indexes incremented.
1209
+ * {@link /v1/routine_folders}
1210
+ */
1211
+ async function postV1RoutineFolders(headers, data, config = {}) {
1212
+ const { client: request = fetch, ...requestConfig } = config;
1213
+ const requestData = data;
1214
+ return (await request({
1215
+ method: "POST",
1216
+ url: getPostV1RoutineFoldersUrl().url.toString(),
1217
+ data: requestData,
1218
+ ...requestConfig,
1219
+ headers: {
1220
+ ...headers,
1221
+ ...requestConfig.headers
1222
+ }
1223
+ })).data;
1224
+ }
1225
+
1226
+ //#endregion
1227
+ //#region src/generated/client/api/postV1Routines.ts
1228
+ /**
1229
+ * Generated by Kubb (https://kubb.dev/).
1230
+ * Do not edit manually.
1231
+ */
1232
+ function getPostV1RoutinesUrl() {
1233
+ return {
1234
+ method: "POST",
1235
+ url: `/v1/routines`
1236
+ };
1237
+ }
1238
+ /**
1239
+ * @summary Create a new routine
1240
+ * {@link /v1/routines}
1241
+ */
1242
+ async function postV1Routines(headers, data, config = {}) {
1243
+ const { client: request = fetch, ...requestConfig } = config;
1244
+ const requestData = data;
1245
+ return (await request({
1246
+ method: "POST",
1247
+ url: getPostV1RoutinesUrl().url.toString(),
1248
+ data: requestData,
1249
+ ...requestConfig,
1250
+ headers: {
1251
+ ...headers,
1252
+ ...requestConfig.headers
1253
+ }
1254
+ })).data;
1255
+ }
1256
+
1257
+ //#endregion
1258
+ //#region src/generated/client/api/postV1Workouts.ts
1259
+ /**
1260
+ * Generated by Kubb (https://kubb.dev/).
1261
+ * Do not edit manually.
1262
+ */
1263
+ function getPostV1WorkoutsUrl() {
1264
+ return {
1265
+ method: "POST",
1266
+ url: `/v1/workouts`
1267
+ };
1268
+ }
1269
+ /**
1270
+ * @summary Create a new workout
1271
+ * {@link /v1/workouts}
1272
+ */
1273
+ async function postV1Workouts(headers, data, config = {}) {
1274
+ const { client: request = fetch, ...requestConfig } = config;
1275
+ const requestData = data;
1276
+ return (await request({
1277
+ method: "POST",
1278
+ url: getPostV1WorkoutsUrl().url.toString(),
1279
+ data: requestData,
1280
+ ...requestConfig,
1281
+ headers: {
1282
+ ...headers,
1283
+ ...requestConfig.headers
1284
+ }
1285
+ })).data;
1286
+ }
1287
+
1288
+ //#endregion
1289
+ //#region src/generated/client/api/putV1RoutinesRoutineid.ts
1290
+ /**
1291
+ * Generated by Kubb (https://kubb.dev/).
1292
+ * Do not edit manually.
1293
+ */
1294
+ function getPutV1RoutinesRoutineidUrl(routineId) {
1295
+ return {
1296
+ method: "PUT",
1297
+ url: `/v1/routines/${routineId}`
1298
+ };
1299
+ }
1300
+ /**
1301
+ * @summary Update an existing routine
1302
+ * {@link /v1/routines/:routineId}
1303
+ */
1304
+ async function putV1RoutinesRoutineid(routineId, headers, data, config = {}) {
1305
+ const { client: request = fetch, ...requestConfig } = config;
1306
+ const requestData = data;
1307
+ return (await request({
1308
+ method: "PUT",
1309
+ url: getPutV1RoutinesRoutineidUrl(routineId).url.toString(),
1310
+ data: requestData,
1311
+ ...requestConfig,
1312
+ headers: {
1313
+ ...headers,
1314
+ ...requestConfig.headers
1315
+ }
1316
+ })).data;
1317
+ }
1318
+
1319
+ //#endregion
1320
+ //#region src/generated/client/api/putV1WorkoutsWorkoutid.ts
1321
+ /**
1322
+ * Generated by Kubb (https://kubb.dev/).
1323
+ * Do not edit manually.
1324
+ */
1325
+ function getPutV1WorkoutsWorkoutidUrl(workoutId) {
1326
+ return {
1327
+ method: "PUT",
1328
+ url: `/v1/workouts/${workoutId}`
1329
+ };
1330
+ }
1331
+ /**
1332
+ * @summary Update an existing workout
1333
+ * {@link /v1/workouts/:workoutId}
1334
+ */
1335
+ async function putV1WorkoutsWorkoutid(workoutId, headers, data, config = {}) {
1336
+ const { client: request = fetch, ...requestConfig } = config;
1337
+ const requestData = data;
1338
+ return (await request({
1339
+ method: "PUT",
1340
+ url: getPutV1WorkoutsWorkoutidUrl(workoutId).url.toString(),
1341
+ data: requestData,
1342
+ ...requestConfig,
1343
+ headers: {
1344
+ ...headers,
1345
+ ...requestConfig.headers
1346
+ }
1347
+ })).data;
1348
+ }
1349
+
1350
+ //#endregion
1351
+ //#region src/utils/hevyClientKubb.ts
1352
+ function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com") {
1353
+ const axiosInstance = axios.create({
1354
+ baseURL: baseUrl,
1355
+ headers: { "api-key": apiKey }
1356
+ });
1357
+ const headers = { "api-key": apiKey };
1358
+ const client = axiosInstance;
1359
+ return {
1360
+ getWorkouts: (params) => getV1Workouts(headers, params, { client }),
1361
+ getWorkout: (workoutId) => getV1WorkoutsWorkoutid(workoutId, headers, { client }),
1362
+ createWorkout: (data) => postV1Workouts(headers, data, { client }),
1363
+ updateWorkout: (workoutId, data) => putV1WorkoutsWorkoutid(workoutId, headers, data, { client }),
1364
+ getWorkoutCount: () => getV1WorkoutsCount(headers, { client }),
1365
+ getWorkoutEvents: (params) => getV1WorkoutsEvents(headers, params, { client }),
1366
+ getRoutines: (params) => getV1Routines(headers, params, { client }),
1367
+ getRoutineById: (routineId) => getV1RoutinesRoutineid(routineId, headers, { client }),
1368
+ createRoutine: (data) => postV1Routines(headers, data, { client }),
1369
+ updateRoutine: (routineId, data) => putV1RoutinesRoutineid(routineId, headers, data, { client }),
1370
+ getExerciseTemplates: (params) => getV1ExerciseTemplates(headers, params, { client }),
1371
+ getExerciseTemplate: (templateId) => getV1ExerciseTemplatesExercisetemplateid(templateId, headers, { client }),
1372
+ getExerciseHistory: (exerciseTemplateId, params) => getV1ExerciseHistoryExercisetemplateid(exerciseTemplateId, headers, params, { client }),
1373
+ createExerciseTemplate: (data) => postV1ExerciseTemplates(headers, data, { client }),
1374
+ getRoutineFolders: (params) => getV1RoutineFolders(headers, params, { client }),
1375
+ createRoutineFolder: (data) => postV1RoutineFolders(headers, data, { client }),
1376
+ getRoutineFolder: (folderId) => getV1RoutineFoldersFolderid(folderId, headers, { client }),
1377
+ getWebhookSubscription: async () => {
1378
+ throw new Error("Webhook subscription API not available. Please regenerate the client from the updated OpenAPI spec.");
1379
+ },
1380
+ createWebhookSubscription: async (_data) => {
1381
+ throw new Error("Webhook subscription API not available. Please regenerate the client from the updated OpenAPI spec.");
1382
+ },
1383
+ deleteWebhookSubscription: async () => {
1384
+ throw new Error("Webhook subscription API not available. Please regenerate the client from the updated OpenAPI spec.");
1385
+ }
1386
+ };
1387
+ }
1388
+
1389
+ //#endregion
1390
+ //#region src/utils/hevyClient.ts
1391
+ function createClient(apiKey, baseUrl) {
1392
+ return createClient$1(apiKey, baseUrl);
1393
+ }
1394
+
1395
+ //#endregion
1396
+ //#region src/index.ts
1397
+ const name = "hevy-mcp";
1398
+ const version = "1.17.3";
1399
+ dotenvx.config({ quiet: true });
1400
+ const sentryConfig = {
1401
+ dsn: "https://ce696d8333b507acbf5203eb877bce0f@o4508975499575296.ingest.de.sentry.io/4509049671647312",
1402
+ release: process.env.SENTRY_RELEASE ?? `${name}@${version}`,
1403
+ tracesSampleRate: 1,
1404
+ sendDefaultPii: false
1405
+ };
1406
+ Sentry.init(sentryConfig);
1407
+ const HEVY_API_BASEURL = "https://api.hevyapp.com";
1408
+ const serverConfigSchema = z.object({ apiKey: z.string().min(1, "Hevy API key is required").describe("Your Hevy API key (available in the Hevy app settings).") });
1409
+ const configSchema = serverConfigSchema;
1410
+ function buildServer(apiKey) {
1411
+ const baseServer = new McpServer({
1412
+ name,
1413
+ version
1414
+ });
1415
+ const server = Sentry.wrapMcpServerWithSentry(baseServer);
1416
+ const hevyClient = createClient(apiKey, HEVY_API_BASEURL);
1417
+ console.error("Hevy client initialized with API key");
1418
+ registerWorkoutTools(server, hevyClient);
1419
+ registerRoutineTools(server, hevyClient);
1420
+ registerTemplateTools(server, hevyClient);
1421
+ registerFolderTools(server, hevyClient);
1422
+ registerWebhookTools(server, hevyClient);
1423
+ return server;
1424
+ }
1425
+ function createServer({ config }) {
1426
+ const { apiKey } = serverConfigSchema.parse(config);
1427
+ return buildServer(apiKey).server;
1428
+ }
1429
+ async function runServer() {
1430
+ const apiKey = parseConfig(process.argv.slice(2), process.env).apiKey;
1431
+ assertApiKey(apiKey);
1432
+ const server = buildServer(apiKey);
1433
+ console.error("Starting MCP server in stdio mode");
1434
+ const transport = new StdioServerTransport();
1435
+ await server.connect(transport);
1436
+ }
1437
+
1438
+ //#endregion
1439
+ export { createServer as n, runServer as r, configSchema as t };
1440
+ //# sourceMappingURL=src-BqxJlq7j.mjs.map