hevy-mcp 1.11.10 → 1.12.4
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 +24 -128
- package/dist/index.d.ts +45 -1
- package/dist/index.js +123 -270
- package/dist/index.js.map +1 -1
- package/package.json +9 -10
package/dist/index.js
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
6
|
import "@dotenvx/dotenvx/config";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
7
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
import { z as z42 } from "zod";
|
|
9
11
|
|
|
10
12
|
// package.json
|
|
11
13
|
var name = "hevy-mcp";
|
|
12
|
-
var version = "1.
|
|
14
|
+
var version = "1.12.3";
|
|
13
15
|
|
|
14
16
|
// src/tools/folders.ts
|
|
15
17
|
import { z } from "zod";
|
|
@@ -53,9 +55,9 @@ function determineErrorType(error, message) {
|
|
|
53
55
|
return "UNKNOWN_ERROR" /* UNKNOWN_ERROR */;
|
|
54
56
|
}
|
|
55
57
|
function withErrorHandling(fn, context) {
|
|
56
|
-
return (async (...
|
|
58
|
+
return (async (...args) => {
|
|
57
59
|
try {
|
|
58
|
-
return await fn(...
|
|
60
|
+
return await fn(...args);
|
|
59
61
|
} catch (error) {
|
|
60
62
|
return createErrorResponse(error, context);
|
|
61
63
|
}
|
|
@@ -188,8 +190,8 @@ function createEmptyResponse(message = "No data found") {
|
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
// src/tools/folders.ts
|
|
191
|
-
function registerFolderTools(
|
|
192
|
-
|
|
193
|
+
function registerFolderTools(server, hevyClient) {
|
|
194
|
+
server.tool(
|
|
193
195
|
"get-routine-folders",
|
|
194
196
|
"Get a paginated list of your routine folders, including both default and custom folders. Useful for organizing and browsing your workout routines.",
|
|
195
197
|
{
|
|
@@ -198,12 +200,12 @@ function registerFolderTools(server2, hevyClient2) {
|
|
|
198
200
|
},
|
|
199
201
|
withErrorHandling(
|
|
200
202
|
async ({ page, pageSize }) => {
|
|
201
|
-
if (!
|
|
203
|
+
if (!hevyClient) {
|
|
202
204
|
throw new Error(
|
|
203
205
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
204
206
|
);
|
|
205
207
|
}
|
|
206
|
-
const data = await
|
|
208
|
+
const data = await hevyClient.getRoutineFolders({
|
|
207
209
|
page,
|
|
208
210
|
pageSize
|
|
209
211
|
});
|
|
@@ -220,19 +222,19 @@ function registerFolderTools(server2, hevyClient2) {
|
|
|
220
222
|
"get-routine-folders"
|
|
221
223
|
)
|
|
222
224
|
);
|
|
223
|
-
|
|
225
|
+
server.tool(
|
|
224
226
|
"get-routine-folder",
|
|
225
227
|
"Get complete details of a specific routine folder by its ID, including name, creation date, and associated routines.",
|
|
226
228
|
{
|
|
227
229
|
folderId: z.string().min(1)
|
|
228
230
|
},
|
|
229
231
|
withErrorHandling(async ({ folderId }) => {
|
|
230
|
-
if (!
|
|
232
|
+
if (!hevyClient) {
|
|
231
233
|
throw new Error(
|
|
232
234
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
233
235
|
);
|
|
234
236
|
}
|
|
235
|
-
const data = await
|
|
237
|
+
const data = await hevyClient.getRoutineFolder(folderId);
|
|
236
238
|
if (!data) {
|
|
237
239
|
return createEmptyResponse(
|
|
238
240
|
`Routine folder with ID ${folderId} not found`
|
|
@@ -242,19 +244,19 @@ function registerFolderTools(server2, hevyClient2) {
|
|
|
242
244
|
return createJsonResponse(folder);
|
|
243
245
|
}, "get-routine-folder")
|
|
244
246
|
);
|
|
245
|
-
|
|
247
|
+
server.tool(
|
|
246
248
|
"create-routine-folder",
|
|
247
249
|
"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.",
|
|
248
250
|
{
|
|
249
251
|
name: z.string().min(1)
|
|
250
252
|
},
|
|
251
253
|
withErrorHandling(async ({ name: name2 }) => {
|
|
252
|
-
if (!
|
|
254
|
+
if (!hevyClient) {
|
|
253
255
|
throw new Error(
|
|
254
256
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
255
257
|
);
|
|
256
258
|
}
|
|
257
|
-
const data = await
|
|
259
|
+
const data = await hevyClient.createRoutineFolder({
|
|
258
260
|
routine_folder: {
|
|
259
261
|
title: name2
|
|
260
262
|
}
|
|
@@ -275,22 +277,22 @@ function registerFolderTools(server2, hevyClient2) {
|
|
|
275
277
|
|
|
276
278
|
// src/tools/routines.ts
|
|
277
279
|
import { z as z2 } from "zod";
|
|
278
|
-
function registerRoutineTools(
|
|
279
|
-
|
|
280
|
+
function registerRoutineTools(server, hevyClient) {
|
|
281
|
+
server.tool(
|
|
280
282
|
"get-routines",
|
|
281
283
|
"Get a paginated list of your workout routines, including custom and default routines. Useful for browsing or searching your available routines.",
|
|
282
284
|
{
|
|
283
285
|
page: z2.coerce.number().int().gte(1).default(1),
|
|
284
286
|
pageSize: z2.coerce.number().int().gte(1).lte(10).default(5)
|
|
285
287
|
},
|
|
286
|
-
withErrorHandling(async (
|
|
287
|
-
if (!
|
|
288
|
+
withErrorHandling(async (args) => {
|
|
289
|
+
if (!hevyClient) {
|
|
288
290
|
throw new Error(
|
|
289
291
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
290
292
|
);
|
|
291
293
|
}
|
|
292
|
-
const { page, pageSize } =
|
|
293
|
-
const data = await
|
|
294
|
+
const { page, pageSize } = args;
|
|
295
|
+
const data = await hevyClient.getRoutines({
|
|
294
296
|
page,
|
|
295
297
|
pageSize
|
|
296
298
|
});
|
|
@@ -303,19 +305,19 @@ function registerRoutineTools(server2, hevyClient2) {
|
|
|
303
305
|
return createJsonResponse(routines);
|
|
304
306
|
}, "get-routines")
|
|
305
307
|
);
|
|
306
|
-
|
|
308
|
+
server.tool(
|
|
307
309
|
"get-routine",
|
|
308
310
|
"Get a routine by its ID using the direct endpoint. Returns all details for the specified routine.",
|
|
309
311
|
{
|
|
310
312
|
routineId: z2.string().min(1)
|
|
311
313
|
},
|
|
312
314
|
withErrorHandling(async ({ routineId }) => {
|
|
313
|
-
if (!
|
|
315
|
+
if (!hevyClient) {
|
|
314
316
|
throw new Error(
|
|
315
317
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
316
318
|
);
|
|
317
319
|
}
|
|
318
|
-
const data = await
|
|
320
|
+
const data = await hevyClient.getRoutineById(String(routineId));
|
|
319
321
|
if (!data || !data.routine) {
|
|
320
322
|
return createEmptyResponse(`Routine with ID ${routineId} not found`);
|
|
321
323
|
}
|
|
@@ -323,7 +325,7 @@ function registerRoutineTools(server2, hevyClient2) {
|
|
|
323
325
|
return createJsonResponse(routine);
|
|
324
326
|
}, "get-routine")
|
|
325
327
|
);
|
|
326
|
-
|
|
328
|
+
server.tool(
|
|
327
329
|
"create-routine",
|
|
328
330
|
"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.",
|
|
329
331
|
{
|
|
@@ -349,14 +351,14 @@ function registerRoutineTools(server2, hevyClient2) {
|
|
|
349
351
|
})
|
|
350
352
|
)
|
|
351
353
|
},
|
|
352
|
-
withErrorHandling(async (
|
|
353
|
-
if (!
|
|
354
|
+
withErrorHandling(async (args) => {
|
|
355
|
+
if (!hevyClient) {
|
|
354
356
|
throw new Error(
|
|
355
357
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
356
358
|
);
|
|
357
359
|
}
|
|
358
|
-
const { title, folderId, notes, exercises } =
|
|
359
|
-
const data = await
|
|
360
|
+
const { title, folderId, notes, exercises } = args;
|
|
361
|
+
const data = await hevyClient.createRoutine({
|
|
360
362
|
routine: {
|
|
361
363
|
title,
|
|
362
364
|
folder_id: folderId ?? null,
|
|
@@ -393,7 +395,7 @@ function registerRoutineTools(server2, hevyClient2) {
|
|
|
393
395
|
});
|
|
394
396
|
}, "create-routine")
|
|
395
397
|
);
|
|
396
|
-
|
|
398
|
+
server.tool(
|
|
397
399
|
"update-routine",
|
|
398
400
|
"Update an existing routine by ID. You can modify the title, notes, and exercise configurations. Returns the updated routine with all changes applied.",
|
|
399
401
|
{
|
|
@@ -419,14 +421,14 @@ function registerRoutineTools(server2, hevyClient2) {
|
|
|
419
421
|
})
|
|
420
422
|
)
|
|
421
423
|
},
|
|
422
|
-
withErrorHandling(async (
|
|
423
|
-
if (!
|
|
424
|
+
withErrorHandling(async (args) => {
|
|
425
|
+
if (!hevyClient) {
|
|
424
426
|
throw new Error(
|
|
425
427
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
426
428
|
);
|
|
427
429
|
}
|
|
428
|
-
const { routineId, title, notes, exercises } =
|
|
429
|
-
const data = await
|
|
430
|
+
const { routineId, title, notes, exercises } = args;
|
|
431
|
+
const data = await hevyClient.updateRoutine(routineId, {
|
|
430
432
|
routine: {
|
|
431
433
|
title,
|
|
432
434
|
notes: notes ?? null,
|
|
@@ -466,8 +468,8 @@ function registerRoutineTools(server2, hevyClient2) {
|
|
|
466
468
|
|
|
467
469
|
// src/tools/templates.ts
|
|
468
470
|
import { z as z3 } from "zod";
|
|
469
|
-
function registerTemplateTools(
|
|
470
|
-
|
|
471
|
+
function registerTemplateTools(server, hevyClient) {
|
|
472
|
+
server.tool(
|
|
471
473
|
"get-exercise-templates",
|
|
472
474
|
"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.",
|
|
473
475
|
{
|
|
@@ -476,12 +478,12 @@ function registerTemplateTools(server2, hevyClient2) {
|
|
|
476
478
|
},
|
|
477
479
|
withErrorHandling(
|
|
478
480
|
async ({ page, pageSize }) => {
|
|
479
|
-
if (!
|
|
481
|
+
if (!hevyClient) {
|
|
480
482
|
throw new Error(
|
|
481
483
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
482
484
|
);
|
|
483
485
|
}
|
|
484
|
-
const data = await
|
|
486
|
+
const data = await hevyClient.getExerciseTemplates({
|
|
485
487
|
page,
|
|
486
488
|
pageSize
|
|
487
489
|
});
|
|
@@ -498,7 +500,7 @@ function registerTemplateTools(server2, hevyClient2) {
|
|
|
498
500
|
"get-exercise-templates"
|
|
499
501
|
)
|
|
500
502
|
);
|
|
501
|
-
|
|
503
|
+
server.tool(
|
|
502
504
|
"get-exercise-template",
|
|
503
505
|
"Get complete details of a specific exercise template by its ID, including name, category, equipment, muscle groups, and notes.",
|
|
504
506
|
{
|
|
@@ -506,12 +508,12 @@ function registerTemplateTools(server2, hevyClient2) {
|
|
|
506
508
|
},
|
|
507
509
|
withErrorHandling(
|
|
508
510
|
async ({ exerciseTemplateId }) => {
|
|
509
|
-
if (!
|
|
511
|
+
if (!hevyClient) {
|
|
510
512
|
throw new Error(
|
|
511
513
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
512
514
|
);
|
|
513
515
|
}
|
|
514
|
-
const data = await
|
|
516
|
+
const data = await hevyClient.getExerciseTemplate(exerciseTemplateId);
|
|
515
517
|
if (!data) {
|
|
516
518
|
return createEmptyResponse(
|
|
517
519
|
`Exercise template with ID ${exerciseTemplateId} not found`
|
|
@@ -994,18 +996,18 @@ var webhookUrlSchema = z40.string().url().refine(
|
|
|
994
996
|
message: "Webhook URL cannot be localhost or loopback address"
|
|
995
997
|
}
|
|
996
998
|
);
|
|
997
|
-
function registerWebhookTools(
|
|
998
|
-
|
|
999
|
+
function registerWebhookTools(server, hevyClient) {
|
|
1000
|
+
server.tool(
|
|
999
1001
|
"get-webhook-subscription",
|
|
1000
1002
|
"Get the current webhook subscription for this account. Returns the webhook URL and auth token if a subscription exists.",
|
|
1001
1003
|
{},
|
|
1002
1004
|
withErrorHandling(async () => {
|
|
1003
|
-
if (!
|
|
1005
|
+
if (!hevyClient) {
|
|
1004
1006
|
throw new Error(
|
|
1005
1007
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
1006
1008
|
);
|
|
1007
1009
|
}
|
|
1008
|
-
const data = await
|
|
1010
|
+
const data = await hevyClient.getWebhookSubscription();
|
|
1009
1011
|
if (!data) {
|
|
1010
1012
|
return createEmptyResponse(
|
|
1011
1013
|
"No webhook subscription found for this account"
|
|
@@ -1014,7 +1016,7 @@ function registerWebhookTools(server2, hevyClient2) {
|
|
|
1014
1016
|
return createJsonResponse(data);
|
|
1015
1017
|
}, "get-webhook-subscription")
|
|
1016
1018
|
);
|
|
1017
|
-
|
|
1019
|
+
server.tool(
|
|
1018
1020
|
"create-webhook-subscription",
|
|
1019
1021
|
"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.",
|
|
1020
1022
|
{
|
|
@@ -1026,7 +1028,7 @@ function registerWebhookTools(server2, hevyClient2) {
|
|
|
1026
1028
|
)
|
|
1027
1029
|
},
|
|
1028
1030
|
withErrorHandling(async ({ url, authToken }) => {
|
|
1029
|
-
if (!
|
|
1031
|
+
if (!hevyClient) {
|
|
1030
1032
|
throw new Error(
|
|
1031
1033
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
1032
1034
|
);
|
|
@@ -1035,7 +1037,7 @@ function registerWebhookTools(server2, hevyClient2) {
|
|
|
1035
1037
|
url,
|
|
1036
1038
|
authToken
|
|
1037
1039
|
});
|
|
1038
|
-
const data = await
|
|
1040
|
+
const data = await hevyClient.createWebhookSubscription(requestBody);
|
|
1039
1041
|
if (!data) {
|
|
1040
1042
|
return createEmptyResponse(
|
|
1041
1043
|
"Failed to create webhook subscription - please check your URL and try again"
|
|
@@ -1044,17 +1046,17 @@ function registerWebhookTools(server2, hevyClient2) {
|
|
|
1044
1046
|
return createJsonResponse(data);
|
|
1045
1047
|
}, "create-webhook-subscription")
|
|
1046
1048
|
);
|
|
1047
|
-
|
|
1049
|
+
server.tool(
|
|
1048
1050
|
"delete-webhook-subscription",
|
|
1049
1051
|
"Delete the current webhook subscription for this account. This will stop all webhook notifications.",
|
|
1050
1052
|
{},
|
|
1051
1053
|
withErrorHandling(async () => {
|
|
1052
|
-
if (!
|
|
1054
|
+
if (!hevyClient) {
|
|
1053
1055
|
throw new Error(
|
|
1054
1056
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
1055
1057
|
);
|
|
1056
1058
|
}
|
|
1057
|
-
const data = await
|
|
1059
|
+
const data = await hevyClient.deleteWebhookSubscription();
|
|
1058
1060
|
if (!data) {
|
|
1059
1061
|
return createEmptyResponse(
|
|
1060
1062
|
"Failed to delete webhook subscription - no subscription may exist or there was a server error"
|
|
@@ -1067,8 +1069,8 @@ function registerWebhookTools(server2, hevyClient2) {
|
|
|
1067
1069
|
|
|
1068
1070
|
// src/tools/workouts.ts
|
|
1069
1071
|
import { z as z41 } from "zod";
|
|
1070
|
-
function registerWorkoutTools(
|
|
1071
|
-
|
|
1072
|
+
function registerWorkoutTools(server, hevyClient) {
|
|
1073
|
+
server.tool(
|
|
1072
1074
|
"get-workouts",
|
|
1073
1075
|
"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.",
|
|
1074
1076
|
{
|
|
@@ -1076,12 +1078,12 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
1076
1078
|
pageSize: z41.coerce.number().int().gte(1).lte(10).default(5)
|
|
1077
1079
|
},
|
|
1078
1080
|
withErrorHandling(async ({ page, pageSize }) => {
|
|
1079
|
-
if (!
|
|
1081
|
+
if (!hevyClient) {
|
|
1080
1082
|
throw new Error(
|
|
1081
1083
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
1082
1084
|
);
|
|
1083
1085
|
}
|
|
1084
|
-
const data = await
|
|
1086
|
+
const data = await hevyClient.getWorkouts({
|
|
1085
1087
|
page,
|
|
1086
1088
|
pageSize
|
|
1087
1089
|
});
|
|
@@ -1094,19 +1096,19 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
1094
1096
|
return createJsonResponse(workouts);
|
|
1095
1097
|
}, "get-workouts")
|
|
1096
1098
|
);
|
|
1097
|
-
|
|
1099
|
+
server.tool(
|
|
1098
1100
|
"get-workout",
|
|
1099
1101
|
"Get complete details of a specific workout by ID. Returns all workout information including title, description, start/end times, and detailed exercise data.",
|
|
1100
1102
|
{
|
|
1101
1103
|
workoutId: z41.string().min(1)
|
|
1102
1104
|
},
|
|
1103
1105
|
withErrorHandling(async ({ workoutId }) => {
|
|
1104
|
-
if (!
|
|
1106
|
+
if (!hevyClient) {
|
|
1105
1107
|
throw new Error(
|
|
1106
1108
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
1107
1109
|
);
|
|
1108
1110
|
}
|
|
1109
|
-
const data = await
|
|
1111
|
+
const data = await hevyClient.getWorkout(workoutId);
|
|
1110
1112
|
if (!data) {
|
|
1111
1113
|
return createEmptyResponse(`Workout with ID ${workoutId} not found`);
|
|
1112
1114
|
}
|
|
@@ -1114,22 +1116,22 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
1114
1116
|
return createJsonResponse(workout);
|
|
1115
1117
|
}, "get-workout")
|
|
1116
1118
|
);
|
|
1117
|
-
|
|
1119
|
+
server.tool(
|
|
1118
1120
|
"get-workout-count",
|
|
1119
1121
|
"Get the total number of workouts on the account. Useful for pagination or statistics.",
|
|
1120
1122
|
{},
|
|
1121
1123
|
withErrorHandling(async () => {
|
|
1122
|
-
if (!
|
|
1124
|
+
if (!hevyClient) {
|
|
1123
1125
|
throw new Error(
|
|
1124
1126
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
1125
1127
|
);
|
|
1126
1128
|
}
|
|
1127
|
-
const data = await
|
|
1129
|
+
const data = await hevyClient.getWorkoutCount();
|
|
1128
1130
|
const count = data ? data.workoutCount || 0 : 0;
|
|
1129
1131
|
return createJsonResponse({ count });
|
|
1130
1132
|
}, "get-workout-count")
|
|
1131
1133
|
);
|
|
1132
|
-
|
|
1134
|
+
server.tool(
|
|
1133
1135
|
"get-workout-events",
|
|
1134
1136
|
"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.",
|
|
1135
1137
|
{
|
|
@@ -1138,12 +1140,12 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
1138
1140
|
since: z41.string().default("1970-01-01T00:00:00Z")
|
|
1139
1141
|
},
|
|
1140
1142
|
withErrorHandling(async ({ page, pageSize, since }) => {
|
|
1141
|
-
if (!
|
|
1143
|
+
if (!hevyClient) {
|
|
1142
1144
|
throw new Error(
|
|
1143
1145
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
1144
1146
|
);
|
|
1145
1147
|
}
|
|
1146
|
-
const data = await
|
|
1148
|
+
const data = await hevyClient.getWorkoutEvents({
|
|
1147
1149
|
page,
|
|
1148
1150
|
pageSize,
|
|
1149
1151
|
since
|
|
@@ -1157,7 +1159,7 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
1157
1159
|
return createJsonResponse(events);
|
|
1158
1160
|
}, "get-workout-events")
|
|
1159
1161
|
);
|
|
1160
|
-
|
|
1162
|
+
server.tool(
|
|
1161
1163
|
"create-workout",
|
|
1162
1164
|
"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.",
|
|
1163
1165
|
{
|
|
@@ -1194,7 +1196,7 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
1194
1196
|
isPrivate,
|
|
1195
1197
|
exercises
|
|
1196
1198
|
}) => {
|
|
1197
|
-
if (!
|
|
1199
|
+
if (!hevyClient) {
|
|
1198
1200
|
throw new Error(
|
|
1199
1201
|
"API client not initialized. Please provide HEVY_API_KEY."
|
|
1200
1202
|
);
|
|
@@ -1222,7 +1224,7 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
1222
1224
|
}))
|
|
1223
1225
|
}
|
|
1224
1226
|
};
|
|
1225
|
-
const data = await
|
|
1227
|
+
const data = await hevyClient.createWorkout(requestBody);
|
|
1226
1228
|
if (!data) {
|
|
1227
1229
|
return createEmptyResponse(
|
|
1228
1230
|
"Failed to create workout: Server returned no data"
|
|
@@ -1237,7 +1239,7 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
1237
1239
|
"create-workout"
|
|
1238
1240
|
)
|
|
1239
1241
|
);
|
|
1240
|
-
|
|
1242
|
+
server.tool(
|
|
1241
1243
|
"update-workout",
|
|
1242
1244
|
"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.",
|
|
1243
1245
|
{
|
|
@@ -1299,7 +1301,7 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
1299
1301
|
}))
|
|
1300
1302
|
}
|
|
1301
1303
|
};
|
|
1302
|
-
const data = await
|
|
1304
|
+
const data = await hevyClient.updateWorkout(workoutId, requestBody);
|
|
1303
1305
|
if (!data) {
|
|
1304
1306
|
return createEmptyResponse(
|
|
1305
1307
|
`Failed to update workout with ID ${workoutId}`
|
|
@@ -1337,24 +1339,14 @@ function parseConfig(argv, env) {
|
|
|
1337
1339
|
if (!apiKey) {
|
|
1338
1340
|
apiKey = env.HEVY_API_KEY || "";
|
|
1339
1341
|
}
|
|
1340
|
-
const transportMode = argv.includes("--http") || env.MCP_TRANSPORT === "http" ? "http" : "stdio";
|
|
1341
|
-
const httpPort = Number.parseInt(env.MCP_HTTP_PORT || "3000", 10);
|
|
1342
|
-
const httpHost = env.MCP_HTTP_HOST || "127.0.0.1";
|
|
1343
|
-
const enableDnsRebindingProtection = env.MCP_DNS_REBINDING_PROTECTION === "true";
|
|
1344
|
-
const allowedHosts = env.MCP_ALLOWED_HOSTS?.split(",").map((h) => h.trim()).filter(Boolean) || ["127.0.0.1"];
|
|
1345
1342
|
return {
|
|
1346
|
-
apiKey
|
|
1347
|
-
transportMode,
|
|
1348
|
-
httpHost,
|
|
1349
|
-
httpPort,
|
|
1350
|
-
enableDnsRebindingProtection,
|
|
1351
|
-
allowedHosts
|
|
1343
|
+
apiKey
|
|
1352
1344
|
};
|
|
1353
1345
|
}
|
|
1354
1346
|
function assertApiKey(apiKey) {
|
|
1355
1347
|
if (!apiKey) {
|
|
1356
1348
|
console.error(
|
|
1357
|
-
"Hevy API key is required. Provide via HEVY_API_KEY
|
|
1349
|
+
"Hevy API key is required. Provide it via the HEVY_API_KEY environment variable or the --hevy-api-key=YOUR_KEY command argument."
|
|
1358
1350
|
);
|
|
1359
1351
|
process.exit(1);
|
|
1360
1352
|
}
|
|
@@ -1637,202 +1629,63 @@ function createClient2(apiKey, baseUrl) {
|
|
|
1637
1629
|
return createClient(apiKey, baseUrl);
|
|
1638
1630
|
}
|
|
1639
1631
|
|
|
1640
|
-
// src/utils/httpServer.ts
|
|
1641
|
-
import { randomUUID } from "crypto";
|
|
1642
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1643
|
-
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
1644
|
-
import express from "express";
|
|
1645
|
-
var transports = /* @__PURE__ */ new Map();
|
|
1646
|
-
var SESSION_TIMEOUT = 30 * 60 * 1e3;
|
|
1647
|
-
setInterval(
|
|
1648
|
-
() => {
|
|
1649
|
-
const now = Date.now();
|
|
1650
|
-
for (const [sessionId, session] of transports) {
|
|
1651
|
-
if (now - session.lastActivity > SESSION_TIMEOUT) {
|
|
1652
|
-
console.log(`Cleaning up abandoned session: ${sessionId}`);
|
|
1653
|
-
session.transport.close?.();
|
|
1654
|
-
transports.delete(sessionId);
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
},
|
|
1658
|
-
5 * 60 * 1e3
|
|
1659
|
-
);
|
|
1660
|
-
function createHttpServer(server2, options) {
|
|
1661
|
-
const app = express();
|
|
1662
|
-
const port = options?.port || 3e3;
|
|
1663
|
-
const host = options?.host || "127.0.0.1";
|
|
1664
|
-
app.use(express.json());
|
|
1665
|
-
app.use("/mcp", (req, res, next) => {
|
|
1666
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1667
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
1668
|
-
res.setHeader(
|
|
1669
|
-
"Access-Control-Allow-Headers",
|
|
1670
|
-
"Content-Type, mcp-session-id"
|
|
1671
|
-
);
|
|
1672
|
-
res.setHeader(
|
|
1673
|
-
"Access-Control-Expose-Headers",
|
|
1674
|
-
"mcp-session-id, mcp-protocol-version"
|
|
1675
|
-
);
|
|
1676
|
-
if (req.method === "OPTIONS") {
|
|
1677
|
-
res.status(204).end();
|
|
1678
|
-
return;
|
|
1679
|
-
}
|
|
1680
|
-
next();
|
|
1681
|
-
});
|
|
1682
|
-
app.post("/mcp", async (req, res) => {
|
|
1683
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
1684
|
-
let transport;
|
|
1685
|
-
if (sessionId && transports.has(sessionId)) {
|
|
1686
|
-
const existingSession = transports.get(sessionId);
|
|
1687
|
-
if (!existingSession) {
|
|
1688
|
-
res.status(400).json({
|
|
1689
|
-
jsonrpc: "2.0",
|
|
1690
|
-
error: {
|
|
1691
|
-
code: -32e3,
|
|
1692
|
-
message: "Bad Request: Session lookup failed"
|
|
1693
|
-
},
|
|
1694
|
-
id: null
|
|
1695
|
-
});
|
|
1696
|
-
return;
|
|
1697
|
-
}
|
|
1698
|
-
transport = existingSession.transport;
|
|
1699
|
-
existingSession.lastActivity = Date.now();
|
|
1700
|
-
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
1701
|
-
const queryApiKey = req.query.HEVY_API_KEY;
|
|
1702
|
-
if (queryApiKey && options?.onFirstRequestApiKey) {
|
|
1703
|
-
options.onFirstRequestApiKey(queryApiKey);
|
|
1704
|
-
}
|
|
1705
|
-
transport = new StreamableHTTPServerTransport({
|
|
1706
|
-
sessionIdGenerator: () => randomUUID(),
|
|
1707
|
-
onsessioninitialized: (sessionId2) => {
|
|
1708
|
-
transports.set(sessionId2, { transport, lastActivity: Date.now() });
|
|
1709
|
-
},
|
|
1710
|
-
// DNS rebinding protection configuration
|
|
1711
|
-
enableDnsRebindingProtection: options?.enableDnsRebindingProtection ?? false,
|
|
1712
|
-
allowedHosts: options?.allowedHosts || ["127.0.0.1"]
|
|
1713
|
-
});
|
|
1714
|
-
transport.onclose = () => {
|
|
1715
|
-
if (transport.sessionId) {
|
|
1716
|
-
transports.delete(transport.sessionId);
|
|
1717
|
-
}
|
|
1718
|
-
};
|
|
1719
|
-
await server2.connect(transport);
|
|
1720
|
-
} else {
|
|
1721
|
-
res.status(400).json({
|
|
1722
|
-
jsonrpc: "2.0",
|
|
1723
|
-
error: {
|
|
1724
|
-
code: -32e3,
|
|
1725
|
-
message: "Bad Request: No valid session ID provided"
|
|
1726
|
-
},
|
|
1727
|
-
id: null
|
|
1728
|
-
});
|
|
1729
|
-
return;
|
|
1730
|
-
}
|
|
1731
|
-
await transport.handleRequest(req, res, req.body);
|
|
1732
|
-
});
|
|
1733
|
-
const handleSessionRequest = async (req, res) => {
|
|
1734
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
1735
|
-
if (!sessionId || !transports.has(sessionId)) {
|
|
1736
|
-
res.status(400).send("Invalid or missing session ID");
|
|
1737
|
-
return;
|
|
1738
|
-
}
|
|
1739
|
-
const transport = transports.get(sessionId)?.transport;
|
|
1740
|
-
if (transport) {
|
|
1741
|
-
await transport.handleRequest(req, res);
|
|
1742
|
-
}
|
|
1743
|
-
};
|
|
1744
|
-
app.get("/mcp", handleSessionRequest);
|
|
1745
|
-
app.delete("/mcp", handleSessionRequest);
|
|
1746
|
-
app.get("/health", (_req, res) => {
|
|
1747
|
-
res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1748
|
-
});
|
|
1749
|
-
const startServer = () => {
|
|
1750
|
-
return new Promise((resolve, reject) => {
|
|
1751
|
-
const httpServer = app.listen(port, host, () => {
|
|
1752
|
-
console.log(`MCP HTTP server listening on http://${host}:${port}`);
|
|
1753
|
-
console.log(`MCP endpoint: http://${host}:${port}/mcp`);
|
|
1754
|
-
console.log(`Health check: http://${host}:${port}/health`);
|
|
1755
|
-
resolve();
|
|
1756
|
-
});
|
|
1757
|
-
httpServer.on("error", (error) => {
|
|
1758
|
-
reject(error);
|
|
1759
|
-
});
|
|
1760
|
-
});
|
|
1761
|
-
};
|
|
1762
|
-
return {
|
|
1763
|
-
app,
|
|
1764
|
-
startServer,
|
|
1765
|
-
getActiveSessionsCount: () => transports.size,
|
|
1766
|
-
closeAllSessions: () => {
|
|
1767
|
-
for (const session of transports.values()) {
|
|
1768
|
-
session.transport.close?.();
|
|
1769
|
-
}
|
|
1770
|
-
transports.clear();
|
|
1771
|
-
}
|
|
1772
|
-
};
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
1632
|
// src/index.ts
|
|
1776
1633
|
var HEVY_API_BASEURL = "https://api.hevyapp.com";
|
|
1777
|
-
var
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1634
|
+
var serverConfigSchema = z42.object({
|
|
1635
|
+
apiKey: z42.string().min(1, "Hevy API key is required").describe("Your Hevy API key (available in the Hevy app settings).")
|
|
1636
|
+
});
|
|
1637
|
+
var configSchema = serverConfigSchema;
|
|
1638
|
+
function buildServer(apiKey) {
|
|
1639
|
+
const server = new McpServer({
|
|
1640
|
+
name,
|
|
1641
|
+
version
|
|
1642
|
+
});
|
|
1643
|
+
const hevyClient = createClient2(apiKey, HEVY_API_BASEURL);
|
|
1644
|
+
console.log("Hevy client initialized with API key");
|
|
1645
|
+
registerWorkoutTools(server, hevyClient);
|
|
1646
|
+
registerRoutineTools(server, hevyClient);
|
|
1647
|
+
registerTemplateTools(server, hevyClient);
|
|
1648
|
+
registerFolderTools(server, hevyClient);
|
|
1649
|
+
registerWebhookTools(server, hevyClient);
|
|
1650
|
+
return server;
|
|
1790
1651
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
} else {
|
|
1796
|
-
console.log(
|
|
1797
|
-
"Starting in HTTP mode without API key. Waiting for API key via query parameter on first request."
|
|
1798
|
-
);
|
|
1652
|
+
function createServer({ config }) {
|
|
1653
|
+
const { apiKey } = serverConfigSchema.parse(config);
|
|
1654
|
+
const server = buildServer(apiKey);
|
|
1655
|
+
return server.server;
|
|
1799
1656
|
}
|
|
1800
|
-
registerWorkoutTools(server, hevyClient);
|
|
1801
|
-
registerRoutineTools(server, hevyClient);
|
|
1802
|
-
registerTemplateTools(server, hevyClient);
|
|
1803
|
-
registerFolderTools(server, hevyClient);
|
|
1804
|
-
registerWebhookTools(server, hevyClient);
|
|
1805
1657
|
async function runServer() {
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
initializeClient(apiKey);
|
|
1819
|
-
registerWorkoutTools(server, hevyClient);
|
|
1820
|
-
registerRoutineTools(server, hevyClient);
|
|
1821
|
-
registerTemplateTools(server, hevyClient);
|
|
1822
|
-
registerFolderTools(server, hevyClient);
|
|
1823
|
-
registerWebhookTools(server, hevyClient);
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
});
|
|
1827
|
-
await httpServer.startServer();
|
|
1828
|
-
} else {
|
|
1829
|
-
console.log("Starting MCP server in stdio mode");
|
|
1830
|
-
const transport = new StdioServerTransport();
|
|
1831
|
-
await server.connect(transport);
|
|
1658
|
+
const args = process.argv.slice(2);
|
|
1659
|
+
const cfg = parseConfig(args, process.env);
|
|
1660
|
+
const apiKey = cfg.apiKey;
|
|
1661
|
+
assertApiKey(apiKey);
|
|
1662
|
+
const server = buildServer(apiKey);
|
|
1663
|
+
console.log("Starting MCP server in stdio mode");
|
|
1664
|
+
const transport = new StdioServerTransport();
|
|
1665
|
+
await server.connect(transport);
|
|
1666
|
+
}
|
|
1667
|
+
var isDirectExecution = (() => {
|
|
1668
|
+
if (typeof process === "undefined" || !Array.isArray(process.argv)) {
|
|
1669
|
+
return false;
|
|
1832
1670
|
}
|
|
1671
|
+
if (typeof import.meta === "undefined" || !import.meta?.url) {
|
|
1672
|
+
return false;
|
|
1673
|
+
}
|
|
1674
|
+
try {
|
|
1675
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
1676
|
+
return process.argv[1] === modulePath;
|
|
1677
|
+
} catch {
|
|
1678
|
+
return false;
|
|
1679
|
+
}
|
|
1680
|
+
})();
|
|
1681
|
+
if (isDirectExecution) {
|
|
1682
|
+
runServer().catch((error) => {
|
|
1683
|
+
console.error("Fatal error in main():", error);
|
|
1684
|
+
process.exit(1);
|
|
1685
|
+
});
|
|
1833
1686
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
}
|
|
1687
|
+
export {
|
|
1688
|
+
configSchema,
|
|
1689
|
+
createServer as default
|
|
1690
|
+
};
|
|
1838
1691
|
//# sourceMappingURL=index.js.map
|