hevy-mcp 1.17.2 → 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.
- package/README.md +15 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.mjs +14 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +54 -0
- package/dist/index.mjs +6 -0
- package/dist/src-BqxJlq7j.mjs +1440 -0
- package/dist/src-BqxJlq7j.mjs.map +1 -0
- package/package.json +10 -9
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -1458
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts +0 -55
- package/dist/index.js +0 -1463
- package/dist/index.js.map +0 -1
|
@@ -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
|