hevy-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/dist/hevy-api.d.ts +147 -0
- package/dist/hevy-api.js +165 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +71 -0
- package/dist/resources.d.ts +8 -0
- package/dist/resources.js +265 -0
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +610 -0
- package/package.json +68 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Resources for Hevy API data
|
|
3
|
+
* Provides read-only access to workout and fitness data as resources
|
|
4
|
+
*/
|
|
5
|
+
// Resource definitions for MCP
|
|
6
|
+
export const resourceDefinitions = [
|
|
7
|
+
{
|
|
8
|
+
uri: 'hevy://workouts',
|
|
9
|
+
name: 'All Workouts',
|
|
10
|
+
description: 'Complete list of all workouts in the Hevy account',
|
|
11
|
+
mimeType: 'application/json',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
uri: 'hevy://routines',
|
|
15
|
+
name: 'All Routines',
|
|
16
|
+
description: 'Complete list of all workout routines',
|
|
17
|
+
mimeType: 'application/json',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
uri: 'hevy://exercise-templates',
|
|
21
|
+
name: 'Exercise Templates',
|
|
22
|
+
description: 'Complete list of available exercise templates',
|
|
23
|
+
mimeType: 'application/json',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
uri: 'hevy://routine-folders',
|
|
27
|
+
name: 'Routine Folders',
|
|
28
|
+
description: 'List of routine organization folders',
|
|
29
|
+
mimeType: 'application/json',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
uri: 'hevy://account-summary',
|
|
33
|
+
name: 'Account Summary',
|
|
34
|
+
description: 'High-level summary of account data and statistics',
|
|
35
|
+
mimeType: 'application/json',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
uri: 'hevy://recent-workouts',
|
|
39
|
+
name: 'Recent Workouts',
|
|
40
|
+
description: 'Last 10 workouts with basic details',
|
|
41
|
+
mimeType: 'application/json',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
uri: 'hevy://exercise-list',
|
|
45
|
+
name: 'Exercise List',
|
|
46
|
+
description: 'Simplified list of exercise names and IDs for reference',
|
|
47
|
+
mimeType: 'text/plain',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
// Resource implementations
|
|
51
|
+
export const resources = {
|
|
52
|
+
'hevy://workouts': async (client) => {
|
|
53
|
+
const workouts = await client.getAllWorkouts();
|
|
54
|
+
return {
|
|
55
|
+
contents: [
|
|
56
|
+
{
|
|
57
|
+
uri: 'hevy://workouts',
|
|
58
|
+
mimeType: 'application/json',
|
|
59
|
+
text: JSON.stringify({
|
|
60
|
+
total_workouts: workouts.length,
|
|
61
|
+
workouts: workouts.map(workout => ({
|
|
62
|
+
id: workout.id,
|
|
63
|
+
title: workout.title,
|
|
64
|
+
description: workout.description,
|
|
65
|
+
start_time: workout.start_time,
|
|
66
|
+
end_time: workout.end_time,
|
|
67
|
+
duration_minutes: workout.end_time ?
|
|
68
|
+
Math.round((new Date(workout.end_time).getTime() - new Date(workout.start_time).getTime()) / 60000) :
|
|
69
|
+
null,
|
|
70
|
+
exercise_count: workout.exercises.length,
|
|
71
|
+
total_sets: workout.exercises.reduce((sum, ex) => sum + ex.sets.length, 0),
|
|
72
|
+
exercises: workout.exercises.map(exercise => ({
|
|
73
|
+
title: exercise.title,
|
|
74
|
+
exercise_template_id: exercise.exercise_template_id,
|
|
75
|
+
sets_count: exercise.sets.length,
|
|
76
|
+
notes: exercise.notes,
|
|
77
|
+
})),
|
|
78
|
+
})),
|
|
79
|
+
}, null, 2),
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
'hevy://routines': async (client) => {
|
|
85
|
+
const routines = await client.getAllRoutines();
|
|
86
|
+
return {
|
|
87
|
+
contents: [
|
|
88
|
+
{
|
|
89
|
+
uri: 'hevy://routines',
|
|
90
|
+
mimeType: 'application/json',
|
|
91
|
+
text: JSON.stringify({
|
|
92
|
+
total_routines: routines.length,
|
|
93
|
+
routines: routines.map(routine => ({
|
|
94
|
+
id: routine.id,
|
|
95
|
+
title: routine.title,
|
|
96
|
+
folder_id: routine.folder_id,
|
|
97
|
+
created_at: routine.created_at,
|
|
98
|
+
updated_at: routine.updated_at,
|
|
99
|
+
exercise_count: routine.exercises.length,
|
|
100
|
+
total_sets: routine.exercises.reduce((sum, ex) => sum + ex.sets.length, 0),
|
|
101
|
+
exercises: routine.exercises.map(exercise => ({
|
|
102
|
+
title: exercise.title,
|
|
103
|
+
exercise_template_id: exercise.exercise_template_id,
|
|
104
|
+
sets_count: exercise.sets.length,
|
|
105
|
+
rest_seconds: exercise.rest_seconds,
|
|
106
|
+
notes: exercise.notes,
|
|
107
|
+
})),
|
|
108
|
+
})),
|
|
109
|
+
}, null, 2),
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
'hevy://exercise-templates': async (client) => {
|
|
115
|
+
const templates = await client.getAllExerciseTemplates();
|
|
116
|
+
return {
|
|
117
|
+
contents: [
|
|
118
|
+
{
|
|
119
|
+
uri: 'hevy://exercise-templates',
|
|
120
|
+
mimeType: 'application/json',
|
|
121
|
+
text: JSON.stringify({
|
|
122
|
+
total_templates: templates.length,
|
|
123
|
+
muscle_groups: [...new Set(templates.map(t => t.primary_muscle_group))].sort(),
|
|
124
|
+
exercise_templates: templates.map(template => ({
|
|
125
|
+
id: template.id,
|
|
126
|
+
title: template.title,
|
|
127
|
+
type: template.type,
|
|
128
|
+
primary_muscle_group: template.primary_muscle_group,
|
|
129
|
+
secondary_muscle_groups: template.secondary_muscle_groups,
|
|
130
|
+
is_custom: template.is_custom,
|
|
131
|
+
})),
|
|
132
|
+
}, null, 2),
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
'hevy://routine-folders': async (client) => {
|
|
138
|
+
const folders = await client.getAllRoutineFolders();
|
|
139
|
+
return {
|
|
140
|
+
contents: [
|
|
141
|
+
{
|
|
142
|
+
uri: 'hevy://routine-folders',
|
|
143
|
+
mimeType: 'application/json',
|
|
144
|
+
text: JSON.stringify({
|
|
145
|
+
total_folders: folders.length,
|
|
146
|
+
routine_folders: folders.map(folder => ({
|
|
147
|
+
id: folder.id,
|
|
148
|
+
title: folder.title,
|
|
149
|
+
index: folder.index,
|
|
150
|
+
created_at: folder.created_at,
|
|
151
|
+
updated_at: folder.updated_at,
|
|
152
|
+
})),
|
|
153
|
+
}, null, 2),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
'hevy://account-summary': async (client) => {
|
|
159
|
+
const [workoutCount, workouts, routines, templates, folders] = await Promise.all([
|
|
160
|
+
client.getWorkoutCount(),
|
|
161
|
+
client.getAllWorkouts(),
|
|
162
|
+
client.getAllRoutines(),
|
|
163
|
+
client.getAllExerciseTemplates(),
|
|
164
|
+
client.getAllRoutineFolders(),
|
|
165
|
+
]);
|
|
166
|
+
// Calculate some basic statistics
|
|
167
|
+
const recentWorkouts = workouts
|
|
168
|
+
.sort((a, b) => new Date(b.start_time).getTime() - new Date(a.start_time).getTime())
|
|
169
|
+
.slice(0, 10);
|
|
170
|
+
const totalSets = workouts.reduce((sum, workout) => sum + workout.exercises.reduce((exSum, ex) => exSum + ex.sets.length, 0), 0);
|
|
171
|
+
const exerciseFrequency = {};
|
|
172
|
+
workouts.forEach(workout => workout.exercises.forEach(exercise => exerciseFrequency[exercise.exercise_template_id] = (exerciseFrequency[exercise.exercise_template_id] || 0) + 1));
|
|
173
|
+
const mostUsedExercises = Object.entries(exerciseFrequency)
|
|
174
|
+
.sort(([, a], [, b]) => b - a)
|
|
175
|
+
.slice(0, 10)
|
|
176
|
+
.map(([exerciseId, count]) => {
|
|
177
|
+
const template = templates.find(t => t.id === exerciseId);
|
|
178
|
+
return {
|
|
179
|
+
exercise_template_id: exerciseId,
|
|
180
|
+
title: template?.title || 'Unknown Exercise',
|
|
181
|
+
usage_count: count,
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
contents: [
|
|
186
|
+
{
|
|
187
|
+
uri: 'hevy://account-summary',
|
|
188
|
+
mimeType: 'application/json',
|
|
189
|
+
text: JSON.stringify({
|
|
190
|
+
account_stats: {
|
|
191
|
+
total_workouts: workoutCount.workout_count,
|
|
192
|
+
total_routines: routines.length,
|
|
193
|
+
total_exercise_templates: templates.length,
|
|
194
|
+
custom_exercises: templates.filter(t => t.is_custom).length,
|
|
195
|
+
routine_folders: folders.length,
|
|
196
|
+
total_sets_completed: totalSets,
|
|
197
|
+
},
|
|
198
|
+
recent_activity: {
|
|
199
|
+
last_workout: recentWorkouts[0] ? {
|
|
200
|
+
id: recentWorkouts[0].id,
|
|
201
|
+
title: recentWorkouts[0].title,
|
|
202
|
+
date: recentWorkouts[0].start_time,
|
|
203
|
+
exercise_count: recentWorkouts[0].exercises.length,
|
|
204
|
+
} : null,
|
|
205
|
+
workouts_last_30_days: workouts.filter(w => {
|
|
206
|
+
const workoutDate = new Date(w.start_time);
|
|
207
|
+
const thirtyDaysAgo = new Date();
|
|
208
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
209
|
+
return workoutDate >= thirtyDaysAgo;
|
|
210
|
+
}).length,
|
|
211
|
+
},
|
|
212
|
+
most_used_exercises: mostUsedExercises,
|
|
213
|
+
muscle_group_distribution: templates.reduce((acc, template) => {
|
|
214
|
+
acc[template.primary_muscle_group] = (acc[template.primary_muscle_group] || 0) + 1;
|
|
215
|
+
return acc;
|
|
216
|
+
}, {}),
|
|
217
|
+
}, null, 2),
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
'hevy://recent-workouts': async (client) => {
|
|
223
|
+
const allWorkouts = await client.getAllWorkouts();
|
|
224
|
+
const recentWorkouts = allWorkouts
|
|
225
|
+
.sort((a, b) => new Date(b.start_time).getTime() - new Date(a.start_time).getTime())
|
|
226
|
+
.slice(0, 10);
|
|
227
|
+
return {
|
|
228
|
+
contents: [
|
|
229
|
+
{
|
|
230
|
+
uri: 'hevy://recent-workouts',
|
|
231
|
+
mimeType: 'application/json',
|
|
232
|
+
text: JSON.stringify({
|
|
233
|
+
recent_workouts: recentWorkouts.map(workout => ({
|
|
234
|
+
id: workout.id,
|
|
235
|
+
title: workout.title,
|
|
236
|
+
date: workout.start_time,
|
|
237
|
+
duration_minutes: workout.end_time ?
|
|
238
|
+
Math.round((new Date(workout.end_time).getTime() - new Date(workout.start_time).getTime()) / 60000) :
|
|
239
|
+
null,
|
|
240
|
+
exercise_count: workout.exercises.length,
|
|
241
|
+
total_sets: workout.exercises.reduce((sum, ex) => sum + ex.sets.length, 0),
|
|
242
|
+
primary_exercises: workout.exercises.slice(0, 3).map(ex => ex.title),
|
|
243
|
+
})),
|
|
244
|
+
}, null, 2),
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
},
|
|
249
|
+
'hevy://exercise-list': async (client) => {
|
|
250
|
+
const templates = await client.getAllExerciseTemplates();
|
|
251
|
+
const exerciseList = templates
|
|
252
|
+
.sort((a, b) => a.title.localeCompare(b.title))
|
|
253
|
+
.map(template => `${template.id} - ${template.title} (${template.primary_muscle_group})`)
|
|
254
|
+
.join('\n');
|
|
255
|
+
return {
|
|
256
|
+
contents: [
|
|
257
|
+
{
|
|
258
|
+
uri: 'hevy://exercise-list',
|
|
259
|
+
mimeType: 'text/plain',
|
|
260
|
+
text: `Hevy Exercise Templates (${templates.length} total)\n\n${exerciseList}`,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
};
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tools for Hevy API interactions
|
|
3
|
+
* Provides tools for managing workouts, routines, and exercise data
|
|
4
|
+
*/
|
|
5
|
+
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { HevyAPIClient } from './hevy-api.js';
|
|
7
|
+
export declare const toolDefinitions: Tool[];
|
|
8
|
+
export declare const tools: Record<string, (client: HevyAPIClient, args: any) => Promise<any>>;
|