hevy-mcp 1.2.1 → 1.3.1
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 +2 -0
- package/dist/index.js +148 -196
- package/dist/index.js.map +1 -1
- package/package.json +14 -13
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
[](https://mseep.ai/app/chrisdoc-hevy-mcp)
|
|
2
|
+
|
|
1
3
|
# hevy-mcp: Model Context Protocol Server for Hevy Fitness API
|
|
2
4
|
|
|
3
5
|
[](https://opensource.org/licenses/MIT)
|
package/dist/index.js
CHANGED
|
@@ -72,13 +72,24 @@ function formatRoutineFolder(folder) {
|
|
|
72
72
|
}
|
|
73
73
|
function calculateDuration(startTime, endTime) {
|
|
74
74
|
if (!startTime || !endTime) return "Unknown duration";
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
try {
|
|
76
|
+
const start = new Date(startTime);
|
|
77
|
+
const end = new Date(endTime);
|
|
78
|
+
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
|
|
79
|
+
return "Unknown duration";
|
|
80
|
+
}
|
|
81
|
+
const durationMs = end.getTime() - start.getTime();
|
|
82
|
+
if (durationMs < 0) {
|
|
83
|
+
return "Invalid duration (end time before start time)";
|
|
84
|
+
}
|
|
85
|
+
const hours = Math.floor(durationMs / (1e3 * 60 * 60));
|
|
86
|
+
const minutes = Math.floor(durationMs % (1e3 * 60 * 60) / (1e3 * 60));
|
|
87
|
+
const seconds = Math.floor(durationMs % (1e3 * 60) / 1e3);
|
|
88
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error("Error calculating duration:", error);
|
|
91
|
+
return "Unknown duration";
|
|
92
|
+
}
|
|
82
93
|
}
|
|
83
94
|
function formatExerciseTemplate(template) {
|
|
84
95
|
return {
|
|
@@ -569,6 +580,61 @@ function registerTemplateTools(server2, hevyClient2) {
|
|
|
569
580
|
|
|
570
581
|
// src/tools/workouts.ts
|
|
571
582
|
import { z as z4 } from "zod";
|
|
583
|
+
|
|
584
|
+
// src/utils/error-handler.ts
|
|
585
|
+
function createErrorResponse(error, context) {
|
|
586
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
587
|
+
const errorCode = error instanceof Error && "code" in error ? error.code : void 0;
|
|
588
|
+
if (errorCode) {
|
|
589
|
+
console.debug(`Error code: ${errorCode}`);
|
|
590
|
+
}
|
|
591
|
+
const contextPrefix = context ? `[${context}] ` : "";
|
|
592
|
+
const formattedMessage = `${contextPrefix}Error: ${errorMessage}`;
|
|
593
|
+
console.error(formattedMessage, error);
|
|
594
|
+
return {
|
|
595
|
+
content: [
|
|
596
|
+
{
|
|
597
|
+
type: "text",
|
|
598
|
+
text: formattedMessage
|
|
599
|
+
}
|
|
600
|
+
],
|
|
601
|
+
isError: true
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function withErrorHandling(fn, context) {
|
|
605
|
+
return async (...args) => {
|
|
606
|
+
try {
|
|
607
|
+
return await fn(...args);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
return createErrorResponse(error, context);
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/utils/response-formatter.ts
|
|
615
|
+
function createJsonResponse(data, options = { pretty: true, indent: 2 }) {
|
|
616
|
+
const jsonString = options.pretty ? JSON.stringify(data, null, options.indent) : JSON.stringify(data);
|
|
617
|
+
return {
|
|
618
|
+
content: [
|
|
619
|
+
{
|
|
620
|
+
type: "text",
|
|
621
|
+
text: jsonString
|
|
622
|
+
}
|
|
623
|
+
]
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function createEmptyResponse(message = "No data found") {
|
|
627
|
+
return {
|
|
628
|
+
content: [
|
|
629
|
+
{
|
|
630
|
+
type: "text",
|
|
631
|
+
text: message
|
|
632
|
+
}
|
|
633
|
+
]
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/tools/workouts.ts
|
|
572
638
|
function registerWorkoutTools(server2, hevyClient2) {
|
|
573
639
|
server2.tool(
|
|
574
640
|
"get-workouts",
|
|
@@ -577,36 +643,19 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
577
643
|
page: z4.coerce.number().gte(1).default(1),
|
|
578
644
|
pageSize: z4.coerce.number().int().gte(1).lte(10).default(5)
|
|
579
645
|
},
|
|
580
|
-
async ({ page, pageSize }
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
return
|
|
590
|
-
content: [
|
|
591
|
-
{
|
|
592
|
-
type: "text",
|
|
593
|
-
text: JSON.stringify(workouts, null, 2)
|
|
594
|
-
}
|
|
595
|
-
]
|
|
596
|
-
};
|
|
597
|
-
} catch (error) {
|
|
598
|
-
console.error("Error fetching workouts:", error);
|
|
599
|
-
return {
|
|
600
|
-
content: [
|
|
601
|
-
{
|
|
602
|
-
type: "text",
|
|
603
|
-
text: `Error fetching workouts: ${error instanceof Error ? error.message : String(error)}`
|
|
604
|
-
}
|
|
605
|
-
],
|
|
606
|
-
isError: true
|
|
607
|
-
};
|
|
646
|
+
withErrorHandling(async ({ page, pageSize }) => {
|
|
647
|
+
const data = await hevyClient2.v1.workouts.get({
|
|
648
|
+
queryParameters: {
|
|
649
|
+
page,
|
|
650
|
+
pageSize
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
const workouts = data?.workouts?.map((workout) => formatWorkout(workout)) || [];
|
|
654
|
+
if (workouts.length === 0) {
|
|
655
|
+
return createEmptyResponse("No workouts found for the specified parameters");
|
|
608
656
|
}
|
|
609
|
-
|
|
657
|
+
return createJsonResponse(workouts);
|
|
658
|
+
}, "get-workouts")
|
|
610
659
|
);
|
|
611
660
|
server2.tool(
|
|
612
661
|
"get-workout",
|
|
@@ -614,70 +663,24 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
614
663
|
{
|
|
615
664
|
workoutId: z4.string().min(1)
|
|
616
665
|
},
|
|
617
|
-
async ({ workoutId }
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
return {
|
|
622
|
-
content: [
|
|
623
|
-
{
|
|
624
|
-
type: "text",
|
|
625
|
-
text: `Workout with ID ${workoutId} not found`
|
|
626
|
-
}
|
|
627
|
-
]
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
const workout = formatWorkout(data);
|
|
631
|
-
return {
|
|
632
|
-
content: [
|
|
633
|
-
{
|
|
634
|
-
type: "text",
|
|
635
|
-
text: JSON.stringify(workout, null, 2)
|
|
636
|
-
}
|
|
637
|
-
]
|
|
638
|
-
};
|
|
639
|
-
} catch (error) {
|
|
640
|
-
console.error(`Error fetching workout ${workoutId}:`, error);
|
|
641
|
-
return {
|
|
642
|
-
content: [
|
|
643
|
-
{
|
|
644
|
-
type: "text",
|
|
645
|
-
text: `Error fetching workout: ${error instanceof Error ? error.message : String(error)}`
|
|
646
|
-
}
|
|
647
|
-
],
|
|
648
|
-
isError: true
|
|
649
|
-
};
|
|
666
|
+
withErrorHandling(async ({ workoutId }) => {
|
|
667
|
+
const data = await hevyClient2.v1.workouts.byWorkoutId(workoutId).get();
|
|
668
|
+
if (!data) {
|
|
669
|
+
return createEmptyResponse(`Workout with ID ${workoutId} not found`);
|
|
650
670
|
}
|
|
651
|
-
|
|
671
|
+
const workout = formatWorkout(data);
|
|
672
|
+
return createJsonResponse(workout);
|
|
673
|
+
}, "get-workout")
|
|
652
674
|
);
|
|
653
675
|
server2.tool(
|
|
654
676
|
"get-workout-count",
|
|
655
677
|
"Get the total number of workouts on the account. Useful for pagination or statistics.",
|
|
656
678
|
{},
|
|
657
|
-
async (
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
{
|
|
663
|
-
type: "text",
|
|
664
|
-
text: `Total workouts: ${data ? data.count || 0 : 0}`
|
|
665
|
-
}
|
|
666
|
-
]
|
|
667
|
-
};
|
|
668
|
-
} catch (error) {
|
|
669
|
-
console.error("Error fetching workout count:", error);
|
|
670
|
-
return {
|
|
671
|
-
content: [
|
|
672
|
-
{
|
|
673
|
-
type: "text",
|
|
674
|
-
text: `Error fetching workout count: ${error instanceof Error ? error.message : String(error)}`
|
|
675
|
-
}
|
|
676
|
-
],
|
|
677
|
-
isError: true
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
}
|
|
679
|
+
withErrorHandling(async () => {
|
|
680
|
+
const data = await hevyClient2.v1.workouts.count.get();
|
|
681
|
+
const count = data ? data.workoutCount || 0 : 0;
|
|
682
|
+
return createJsonResponse({ count });
|
|
683
|
+
}, "get-workout-count")
|
|
681
684
|
);
|
|
682
685
|
server2.tool(
|
|
683
686
|
"get-workout-events",
|
|
@@ -687,36 +690,20 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
687
690
|
pageSize: z4.coerce.number().int().gte(1).lte(10).default(5),
|
|
688
691
|
since: z4.string().default("1970-01-01T00:00:00Z")
|
|
689
692
|
},
|
|
690
|
-
async ({ page, pageSize, since }
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
{
|
|
702
|
-
type: "text",
|
|
703
|
-
text: JSON.stringify(data?.events || [], null, 2)
|
|
704
|
-
}
|
|
705
|
-
]
|
|
706
|
-
};
|
|
707
|
-
} catch (error) {
|
|
708
|
-
console.error("Error fetching workout events:", error);
|
|
709
|
-
return {
|
|
710
|
-
content: [
|
|
711
|
-
{
|
|
712
|
-
type: "text",
|
|
713
|
-
text: `Error fetching workout events: ${error instanceof Error ? error.message : String(error)}`
|
|
714
|
-
}
|
|
715
|
-
],
|
|
716
|
-
isError: true
|
|
717
|
-
};
|
|
693
|
+
withErrorHandling(async ({ page, pageSize, since }) => {
|
|
694
|
+
const data = await hevyClient2.v1.workouts.events.get({
|
|
695
|
+
queryParameters: {
|
|
696
|
+
page,
|
|
697
|
+
pageSize,
|
|
698
|
+
since
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
const events = data?.events || [];
|
|
702
|
+
if (events.length === 0) {
|
|
703
|
+
return createEmptyResponse(`No workout events found for the specified parameters since ${since}`);
|
|
718
704
|
}
|
|
719
|
-
|
|
705
|
+
return createJsonResponse(events);
|
|
706
|
+
}, "get-workout-events")
|
|
720
707
|
);
|
|
721
708
|
server2.tool(
|
|
722
709
|
"create-workout",
|
|
@@ -746,8 +733,15 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
746
733
|
})
|
|
747
734
|
)
|
|
748
735
|
},
|
|
749
|
-
|
|
750
|
-
|
|
736
|
+
withErrorHandling(
|
|
737
|
+
async ({
|
|
738
|
+
title,
|
|
739
|
+
description,
|
|
740
|
+
startTime,
|
|
741
|
+
endTime,
|
|
742
|
+
isPrivate,
|
|
743
|
+
exercises
|
|
744
|
+
}) => {
|
|
751
745
|
const requestBody = {
|
|
752
746
|
workout: {
|
|
753
747
|
title,
|
|
@@ -773,38 +767,16 @@ function registerWorkoutTools(server2, hevyClient2) {
|
|
|
773
767
|
};
|
|
774
768
|
const data = await hevyClient2.v1.workouts.post(requestBody);
|
|
775
769
|
if (!data) {
|
|
776
|
-
return
|
|
777
|
-
content: [
|
|
778
|
-
{
|
|
779
|
-
type: "text",
|
|
780
|
-
text: "Failed to create workout"
|
|
781
|
-
}
|
|
782
|
-
]
|
|
783
|
-
};
|
|
770
|
+
return createEmptyResponse("Failed to create workout: Server returned no data");
|
|
784
771
|
}
|
|
785
772
|
const workout = formatWorkout(data);
|
|
786
|
-
return {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
]
|
|
794
|
-
};
|
|
795
|
-
} catch (error) {
|
|
796
|
-
console.error("Error creating workout:", error);
|
|
797
|
-
return {
|
|
798
|
-
content: [
|
|
799
|
-
{
|
|
800
|
-
type: "text",
|
|
801
|
-
text: `Error creating workout: ${error instanceof Error ? error.message : String(error)}`
|
|
802
|
-
}
|
|
803
|
-
],
|
|
804
|
-
isError: true
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
}
|
|
773
|
+
return createJsonResponse(workout, {
|
|
774
|
+
pretty: true,
|
|
775
|
+
indent: 2
|
|
776
|
+
});
|
|
777
|
+
},
|
|
778
|
+
"create-workout"
|
|
779
|
+
)
|
|
808
780
|
);
|
|
809
781
|
server2.tool(
|
|
810
782
|
"update-workout",
|
|
@@ -835,16 +807,16 @@ ${JSON.stringify(workout, null, 2)}`
|
|
|
835
807
|
})
|
|
836
808
|
)
|
|
837
809
|
},
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
810
|
+
withErrorHandling(
|
|
811
|
+
async ({
|
|
812
|
+
workoutId,
|
|
813
|
+
title,
|
|
814
|
+
description,
|
|
815
|
+
startTime,
|
|
816
|
+
endTime,
|
|
817
|
+
isPrivate,
|
|
818
|
+
exercises
|
|
819
|
+
}) => {
|
|
848
820
|
const requestBody = {
|
|
849
821
|
workout: {
|
|
850
822
|
title,
|
|
@@ -870,38 +842,18 @@ ${JSON.stringify(workout, null, 2)}`
|
|
|
870
842
|
};
|
|
871
843
|
const data = await hevyClient2.v1.workouts.byWorkoutId(workoutId).put(requestBody);
|
|
872
844
|
if (!data) {
|
|
873
|
-
return
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
type: "text",
|
|
877
|
-
text: `Failed to update workout with ID ${workoutId}`
|
|
878
|
-
}
|
|
879
|
-
]
|
|
880
|
-
};
|
|
845
|
+
return createEmptyResponse(
|
|
846
|
+
`Failed to update workout with ID ${workoutId}`
|
|
847
|
+
);
|
|
881
848
|
}
|
|
882
849
|
const workout = formatWorkout(data);
|
|
883
|
-
return {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
]
|
|
891
|
-
};
|
|
892
|
-
} catch (error) {
|
|
893
|
-
console.error(`Error updating workout ${workoutId}:`, error);
|
|
894
|
-
return {
|
|
895
|
-
content: [
|
|
896
|
-
{
|
|
897
|
-
type: "text",
|
|
898
|
-
text: `Error updating workout: ${error instanceof Error ? error.message : String(error)}`
|
|
899
|
-
}
|
|
900
|
-
],
|
|
901
|
-
isError: true
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
}
|
|
850
|
+
return createJsonResponse(workout, {
|
|
851
|
+
pretty: true,
|
|
852
|
+
indent: 2
|
|
853
|
+
});
|
|
854
|
+
},
|
|
855
|
+
"update-workout-operation"
|
|
856
|
+
)
|
|
905
857
|
);
|
|
906
858
|
}
|
|
907
859
|
|
|
@@ -2136,7 +2088,7 @@ function createClient(apiKey2, baseUrl) {
|
|
|
2136
2088
|
|
|
2137
2089
|
// package.json
|
|
2138
2090
|
var name = "hevy-mcp";
|
|
2139
|
-
var version = "1.
|
|
2091
|
+
var version = "1.4.0";
|
|
2140
2092
|
|
|
2141
2093
|
// src/index.ts
|
|
2142
2094
|
var HEVY_API_BASEURL = "https://api.hevyapp.com";
|