intervals-icu-mcp-server 0.1.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 +80 -0
- package/dist/client.d.ts +362 -0
- package/dist/client.js +236 -0
- package/dist/format.d.ts +5 -0
- package/dist/format.js +92 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +613 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { getActivities, getActivityDetails, getActivityIntervals, getWellness, getEvents, getEventById, createEvent, updateEvent, deleteEvent, getAthleteProfile, getAthleteZones, getAthleteSummary, getWellnessDay, updateWellness, getActivityStreams, getActivityPowerCurves, listWorkouts, createWorkout, bulkCreateEvents, bulkDeleteEvents, } from "./client.js";
|
|
7
|
+
import { formatActivity, formatWellness, formatEvent, formatIntervalRow, } from "./format.js";
|
|
8
|
+
function getConfig(args) {
|
|
9
|
+
const apiKey = args["api_key"] ?? process.env.API_KEY ?? "";
|
|
10
|
+
const athleteId = args["athlete_id"] ??
|
|
11
|
+
process.env.ATHLETE_ID ??
|
|
12
|
+
"";
|
|
13
|
+
if (!apiKey)
|
|
14
|
+
throw new Error("No API key provided. Set API_KEY in .env or pass api_key.");
|
|
15
|
+
if (!athleteId)
|
|
16
|
+
throw new Error("No athlete ID provided. Set ATHLETE_ID in .env or pass athlete_id.");
|
|
17
|
+
return { apiKey, athleteId };
|
|
18
|
+
}
|
|
19
|
+
function today() {
|
|
20
|
+
return new Date().toISOString().slice(0, 10);
|
|
21
|
+
}
|
|
22
|
+
function daysAgo(n) {
|
|
23
|
+
const d = new Date();
|
|
24
|
+
d.setDate(d.getDate() - n);
|
|
25
|
+
return d.toISOString().slice(0, 10);
|
|
26
|
+
}
|
|
27
|
+
function daysFromNow(n) {
|
|
28
|
+
const d = new Date();
|
|
29
|
+
d.setDate(d.getDate() + n);
|
|
30
|
+
return d.toISOString().slice(0, 10);
|
|
31
|
+
}
|
|
32
|
+
const TOOLS = [
|
|
33
|
+
{
|
|
34
|
+
name: "get_activities",
|
|
35
|
+
description: "Get a list of activities for an athlete from Intervals.icu. Returns activity summaries including power, HR, distance, and training metrics.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
40
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
41
|
+
start_date: { type: "string", description: "Start date YYYY-MM-DD (default: 30 days ago)" },
|
|
42
|
+
end_date: { type: "string", description: "End date YYYY-MM-DD (default: today)" },
|
|
43
|
+
limit: { type: "number", description: "Max activities to return (default: 10)" },
|
|
44
|
+
include_unnamed: { type: "boolean", description: "Include unnamed activities (default: false)" },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "get_activity_details",
|
|
50
|
+
description: "Get detailed metrics for a specific activity by ID, including power zones, HR zones, and full metrics.",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
required: ["activity_id"],
|
|
54
|
+
properties: {
|
|
55
|
+
activity_id: { type: "string", description: "The activity ID" },
|
|
56
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
57
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "get_activity_intervals",
|
|
63
|
+
description: "Get interval breakdown for a specific activity: power, HR, cadence, speed, elevation per interval and group.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
required: ["activity_id"],
|
|
67
|
+
properties: {
|
|
68
|
+
activity_id: { type: "string", description: "The activity ID" },
|
|
69
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
70
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "get_wellness_data",
|
|
76
|
+
description: "Get wellness data (weight, HRV, sleep, readiness, CTL/ATL, subjective metrics) for a date range.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
81
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
82
|
+
start_date: { type: "string", description: "Start date YYYY-MM-DD (default: 30 days ago)" },
|
|
83
|
+
end_date: { type: "string", description: "End date YYYY-MM-DD (default: today)" },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "get_events",
|
|
89
|
+
description: "Get calendar events (workouts, races) for an athlete in a date range.",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
94
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
95
|
+
start_date: { type: "string", description: "Start date YYYY-MM-DD (default: today)" },
|
|
96
|
+
end_date: { type: "string", description: "End date YYYY-MM-DD (default: 30 days from now)" },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "get_event_by_id",
|
|
102
|
+
description: "Get full details for a specific calendar event by ID.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
required: ["event_id"],
|
|
106
|
+
properties: {
|
|
107
|
+
event_id: { type: "string", description: "The event ID" },
|
|
108
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
109
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "create_event",
|
|
115
|
+
description: "Create a new calendar event (workout, note, race) on a specific date in Intervals.icu.",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
required: ["start_date_local"],
|
|
119
|
+
properties: {
|
|
120
|
+
start_date_local: { type: "string", description: "Date/time in ISO 8601 format, e.g. 2024-06-01T08:00:00" },
|
|
121
|
+
name: { type: "string", description: "Event name" },
|
|
122
|
+
description: { type: "string", description: "Event description / workout notes" },
|
|
123
|
+
category: { type: "string", description: "Event category: WORKOUT, NOTE, RACE_A, RACE_B, RACE_C (race priority A/B/C)" },
|
|
124
|
+
type: { type: "string", description: "Activity type: Ride, Run, Swim, WeightTraining, etc." },
|
|
125
|
+
race: { type: "boolean", description: "Mark as a race event (default: false)" },
|
|
126
|
+
distance: { type: "number", description: "Distance in meters (e.g. 100000 for 100km)" },
|
|
127
|
+
sub_type: { type: "string", description: "Race category/sub-type (e.g. A, B, C)" },
|
|
128
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
129
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "update_event",
|
|
135
|
+
description: "Update an existing calendar event. Pass only the fields you want to change.",
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: "object",
|
|
138
|
+
required: ["event_id"],
|
|
139
|
+
properties: {
|
|
140
|
+
event_id: { type: "string", description: "The event ID to update" },
|
|
141
|
+
start_date_local: { type: "string", description: "New date/time (moves the event), e.g. 2024-06-05T08:00:00" },
|
|
142
|
+
name: { type: "string", description: "New event name" },
|
|
143
|
+
description: { type: "string", description: "New description" },
|
|
144
|
+
category: { type: "string", description: "New category" },
|
|
145
|
+
type: { type: "string", description: "New activity type" },
|
|
146
|
+
race: { type: "boolean", description: "Toggle race flag" },
|
|
147
|
+
distance: { type: "number", description: "Distance in meters (e.g. 100000 for 100km)" },
|
|
148
|
+
sub_type: { type: "string", description: "Race category/sub-type (e.g. A, B, C)" },
|
|
149
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
150
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "delete_event",
|
|
156
|
+
description: "Delete a calendar event from Intervals.icu by ID. This action cannot be undone.",
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: "object",
|
|
159
|
+
required: ["event_id"],
|
|
160
|
+
properties: {
|
|
161
|
+
event_id: { type: "string", description: "The event ID to delete" },
|
|
162
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
163
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "get_athlete_profile",
|
|
169
|
+
description: "Get athlete profile including FTP, LTHR, weight, VO2max and other settings.",
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
174
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "get_athlete_zones",
|
|
180
|
+
description: "Get power, heart rate, and pace training zones for the athlete.",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
185
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "get_athlete_summary",
|
|
191
|
+
description: "Get current fitness snapshot: CTL (fitness), ATL (fatigue), TSB (form), and ramp rate.",
|
|
192
|
+
inputSchema: {
|
|
193
|
+
type: "object",
|
|
194
|
+
properties: {
|
|
195
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
196
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "get_wellness_day",
|
|
202
|
+
description: "Get wellness data for a specific single date.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: "object",
|
|
205
|
+
required: ["date"],
|
|
206
|
+
properties: {
|
|
207
|
+
date: { type: "string", description: "Date in YYYY-MM-DD format" },
|
|
208
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
209
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: "update_wellness",
|
|
215
|
+
description: "Log or update wellness data for a specific date (weight, HRV, sleep, subjective metrics, etc.).",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: "object",
|
|
218
|
+
required: ["date"],
|
|
219
|
+
properties: {
|
|
220
|
+
date: { type: "string", description: "Date in YYYY-MM-DD format" },
|
|
221
|
+
weight: { type: "number", description: "Body weight in kg" },
|
|
222
|
+
restingHR: { type: "number", description: "Resting heart rate in bpm" },
|
|
223
|
+
hrv: { type: "number", description: "HRV score" },
|
|
224
|
+
hrvSDNN: { type: "number", description: "HRV SDNN value" },
|
|
225
|
+
sleepSecs: { type: "number", description: "Sleep duration in seconds" },
|
|
226
|
+
sleepScore: { type: "number", description: "Sleep score 0-100" },
|
|
227
|
+
sleepQuality: { type: "number", description: "Sleep quality 1-10" },
|
|
228
|
+
readiness: { type: "number", description: "Readiness score 1-10" },
|
|
229
|
+
soreness: { type: "number", description: "Muscle soreness 1-10" },
|
|
230
|
+
fatigue: { type: "number", description: "Fatigue level 1-10" },
|
|
231
|
+
stress: { type: "number", description: "Stress level 1-10" },
|
|
232
|
+
mood: { type: "number", description: "Mood 1-10" },
|
|
233
|
+
motivation: { type: "number", description: "Motivation 1-10" },
|
|
234
|
+
steps: { type: "number", description: "Step count" },
|
|
235
|
+
kcalConsumed: { type: "number", description: "Calories consumed" },
|
|
236
|
+
spO2: { type: "number", description: "Blood oxygen %" },
|
|
237
|
+
comments: { type: "string", description: "Free-text notes" },
|
|
238
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
239
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: "get_activity_streams",
|
|
245
|
+
description: "Get raw time-series data streams for an activity (watts, HR, cadence, speed, altitude, etc.) — one value per second.",
|
|
246
|
+
inputSchema: {
|
|
247
|
+
type: "object",
|
|
248
|
+
required: ["activity_id"],
|
|
249
|
+
properties: {
|
|
250
|
+
activity_id: { type: "string", description: "The activity ID" },
|
|
251
|
+
types: {
|
|
252
|
+
type: "array",
|
|
253
|
+
items: { type: "string" },
|
|
254
|
+
description: "Stream types to fetch, e.g. [\"watts\",\"heartrate\",\"cadence\",\"velocity_smooth\",\"altitude\"]. Omit for all.",
|
|
255
|
+
},
|
|
256
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
257
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: "get_activity_power_curves",
|
|
263
|
+
description: "Get best power output for standard durations (5s, 1min, 5min, 20min, 60min, etc.) for an activity — the power curve / critical power profile.",
|
|
264
|
+
inputSchema: {
|
|
265
|
+
type: "object",
|
|
266
|
+
required: ["activity_id"],
|
|
267
|
+
properties: {
|
|
268
|
+
activity_id: { type: "string", description: "The activity ID" },
|
|
269
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
270
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: "list_workouts",
|
|
276
|
+
description: "List all workouts in the athlete's workout library.",
|
|
277
|
+
inputSchema: {
|
|
278
|
+
type: "object",
|
|
279
|
+
properties: {
|
|
280
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
281
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: "create_workout",
|
|
287
|
+
description: "Create a new structured workout in the athlete's workout library.",
|
|
288
|
+
inputSchema: {
|
|
289
|
+
type: "object",
|
|
290
|
+
properties: {
|
|
291
|
+
name: { type: "string", description: "Workout name" },
|
|
292
|
+
description: { type: "string", description: "Workout description" },
|
|
293
|
+
type: { type: "string", description: "Activity type: Ride, Run, Swim, etc." },
|
|
294
|
+
folder_id: { type: "number", description: "Folder ID to save workout into (required by API)" },
|
|
295
|
+
workout_doc: { type: "object", description: "Structured workout definition (Intervals.icu workout_doc format)" },
|
|
296
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
297
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: "bulk_create_events",
|
|
303
|
+
description: "Create multiple calendar events at once.",
|
|
304
|
+
inputSchema: {
|
|
305
|
+
type: "object",
|
|
306
|
+
required: ["events"],
|
|
307
|
+
properties: {
|
|
308
|
+
events: {
|
|
309
|
+
type: "array",
|
|
310
|
+
description: "Array of event objects to create",
|
|
311
|
+
items: {
|
|
312
|
+
type: "object",
|
|
313
|
+
properties: {
|
|
314
|
+
start_date_local: { type: "string", description: "ISO 8601 date/time" },
|
|
315
|
+
name: { type: "string" },
|
|
316
|
+
description: { type: "string" },
|
|
317
|
+
category: { type: "string" },
|
|
318
|
+
type: { type: "string" },
|
|
319
|
+
race: { type: "boolean" },
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
324
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "bulk_delete_events",
|
|
330
|
+
description: "Delete multiple calendar events at once by their IDs.",
|
|
331
|
+
inputSchema: {
|
|
332
|
+
type: "object",
|
|
333
|
+
required: ["event_ids"],
|
|
334
|
+
properties: {
|
|
335
|
+
event_ids: {
|
|
336
|
+
type: "array",
|
|
337
|
+
items: { type: "string" },
|
|
338
|
+
description: "Array of event IDs to delete",
|
|
339
|
+
},
|
|
340
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
341
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
];
|
|
346
|
+
const server = new Server({ name: "intervals-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
347
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
348
|
+
tools: TOOLS.map((t) => ({ ...t, inputSchema: t.inputSchema })),
|
|
349
|
+
}));
|
|
350
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
351
|
+
const { name, arguments: args = {} } = request.params;
|
|
352
|
+
try {
|
|
353
|
+
switch (name) {
|
|
354
|
+
case "get_activities": {
|
|
355
|
+
const config = getConfig(args);
|
|
356
|
+
const startDate = args["start_date"] ?? daysAgo(30);
|
|
357
|
+
const endDate = args["end_date"] ?? today();
|
|
358
|
+
const limit = args["limit"] ?? 10;
|
|
359
|
+
const includeUnnamed = args["include_unnamed"] ?? false;
|
|
360
|
+
let activities = await getActivities(config, { startDate, endDate, limit: limit * 3 });
|
|
361
|
+
if (!includeUnnamed) {
|
|
362
|
+
activities = activities.filter((a) => a.name && a.name !== "Unnamed");
|
|
363
|
+
}
|
|
364
|
+
activities = activities.slice(0, limit);
|
|
365
|
+
if (activities.length === 0) {
|
|
366
|
+
return {
|
|
367
|
+
content: [
|
|
368
|
+
{
|
|
369
|
+
type: "text",
|
|
370
|
+
text: includeUnnamed
|
|
371
|
+
? "No activities found in the specified date range."
|
|
372
|
+
: "No named activities found. Try with include_unnamed: true.",
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const text = activities.map(formatActivity).join("\n\n---\n\n");
|
|
378
|
+
return { content: [{ type: "text", text }] };
|
|
379
|
+
}
|
|
380
|
+
case "get_activity_details": {
|
|
381
|
+
const activityId = z.string().parse(args["activity_id"]);
|
|
382
|
+
const config = getConfig(args);
|
|
383
|
+
const activity = await getActivityDetails(config, activityId);
|
|
384
|
+
return { content: [{ type: "text", text: formatActivity(activity) }] };
|
|
385
|
+
}
|
|
386
|
+
case "get_activity_intervals": {
|
|
387
|
+
const activityId = z.string().parse(args["activity_id"]);
|
|
388
|
+
const config = getConfig(args);
|
|
389
|
+
const data = await getActivityIntervals(config, activityId);
|
|
390
|
+
const lines = [`Intervals for activity ${activityId}`, ""];
|
|
391
|
+
const intervals = data["icu_intervals"];
|
|
392
|
+
const groups = data["icu_groups"];
|
|
393
|
+
if (intervals?.length) {
|
|
394
|
+
lines.push("## Individual Intervals", "");
|
|
395
|
+
intervals.forEach((iv, i) => lines.push(formatIntervalRow(iv, i + 1), ""));
|
|
396
|
+
}
|
|
397
|
+
if (groups?.length) {
|
|
398
|
+
lines.push("## Groups", "");
|
|
399
|
+
groups.forEach((g, i) => lines.push(formatIntervalRow(g, i + 1), ""));
|
|
400
|
+
}
|
|
401
|
+
if (!intervals?.length && !groups?.length) {
|
|
402
|
+
lines.push("No interval data found for this activity.");
|
|
403
|
+
}
|
|
404
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
405
|
+
}
|
|
406
|
+
case "get_wellness_data": {
|
|
407
|
+
const config = getConfig(args);
|
|
408
|
+
const startDate = args["start_date"] ?? daysAgo(30);
|
|
409
|
+
const endDate = args["end_date"] ?? today();
|
|
410
|
+
const wellnessMap = await getWellness(config, { startDate, endDate });
|
|
411
|
+
const entries = Object.entries(wellnessMap).sort(([a], [b]) => a.localeCompare(b));
|
|
412
|
+
if (entries.length === 0) {
|
|
413
|
+
return { content: [{ type: "text", text: "No wellness data found for the specified date range." }] };
|
|
414
|
+
}
|
|
415
|
+
const text = entries.map(([date, w]) => formatWellness(date, w)).join("\n\n---\n\n");
|
|
416
|
+
return { content: [{ type: "text", text }] };
|
|
417
|
+
}
|
|
418
|
+
case "get_events": {
|
|
419
|
+
const config = getConfig(args);
|
|
420
|
+
const startDate = args["start_date"] ?? today();
|
|
421
|
+
const endDate = args["end_date"] ?? daysFromNow(30);
|
|
422
|
+
const events = await getEvents(config, { startDate, endDate });
|
|
423
|
+
if (events.length === 0) {
|
|
424
|
+
return { content: [{ type: "text", text: "No events found in the specified date range." }] };
|
|
425
|
+
}
|
|
426
|
+
const text = events.map(formatEvent).join("\n\n---\n\n");
|
|
427
|
+
return { content: [{ type: "text", text }] };
|
|
428
|
+
}
|
|
429
|
+
case "get_event_by_id": {
|
|
430
|
+
const eventId = z.string().parse(args["event_id"]);
|
|
431
|
+
const config = getConfig(args);
|
|
432
|
+
const event = await getEventById(config, eventId);
|
|
433
|
+
return { content: [{ type: "text", text: formatEvent(event) }] };
|
|
434
|
+
}
|
|
435
|
+
case "create_event": {
|
|
436
|
+
const config = getConfig(args);
|
|
437
|
+
const event = await createEvent(config, {
|
|
438
|
+
name: args["name"],
|
|
439
|
+
description: args["description"],
|
|
440
|
+
start_date_local: z.string().parse(args["start_date_local"]),
|
|
441
|
+
category: args["category"],
|
|
442
|
+
type: args["type"],
|
|
443
|
+
race: args["race"],
|
|
444
|
+
distance: args["distance"],
|
|
445
|
+
sub_type: args["sub_type"],
|
|
446
|
+
});
|
|
447
|
+
return { content: [{ type: "text", text: `Created event:\n\n${formatEvent(event)}` }] };
|
|
448
|
+
}
|
|
449
|
+
case "update_event": {
|
|
450
|
+
const eventId = z.string().parse(args["event_id"]);
|
|
451
|
+
const config = getConfig(args);
|
|
452
|
+
const input = {};
|
|
453
|
+
for (const key of ["name", "description", "start_date_local", "category", "type", "race", "distance", "sub_type"]) {
|
|
454
|
+
if (args[key] !== undefined)
|
|
455
|
+
input[key] = args[key];
|
|
456
|
+
}
|
|
457
|
+
const event = await updateEvent(config, eventId, input);
|
|
458
|
+
return { content: [{ type: "text", text: `Updated event:\n\n${formatEvent(event)}` }] };
|
|
459
|
+
}
|
|
460
|
+
case "delete_event": {
|
|
461
|
+
const eventId = z.string().parse(args["event_id"]);
|
|
462
|
+
const config = getConfig(args);
|
|
463
|
+
await deleteEvent(config, eventId);
|
|
464
|
+
return { content: [{ type: "text", text: `Event ${eventId} deleted.` }] };
|
|
465
|
+
}
|
|
466
|
+
case "get_athlete_profile": {
|
|
467
|
+
const config = getConfig(args);
|
|
468
|
+
const athlete = await getAthleteProfile(config);
|
|
469
|
+
const lines = [
|
|
470
|
+
`**${athlete.name ?? "Athlete"}** (${athlete.id ?? config.athleteId})`,
|
|
471
|
+
athlete.email ? `Email: ${athlete.email}` : "",
|
|
472
|
+
`Sex: ${athlete.sex ?? "N/A"} | DOB: ${athlete.dob ?? "N/A"} | City: ${athlete.city ?? "N/A"}, ${athlete.country ?? "N/A"}`,
|
|
473
|
+
"",
|
|
474
|
+
"Performance:",
|
|
475
|
+
` FTP: ${athlete.icu_ftp ?? "N/A"} W | LTHR: ${athlete.icu_lthr ?? "N/A"} bpm`,
|
|
476
|
+
` Weight: ${athlete.icu_weight ?? athlete.weight ?? "N/A"} kg | VO2max: ${athlete.icu_vo2max ?? "N/A"}`,
|
|
477
|
+
` Resting HR: ${athlete.icu_resting_hr ?? "N/A"} bpm`,
|
|
478
|
+
].filter(Boolean).join("\n");
|
|
479
|
+
return { content: [{ type: "text", text: lines }] };
|
|
480
|
+
}
|
|
481
|
+
case "get_athlete_zones": {
|
|
482
|
+
const config = getConfig(args);
|
|
483
|
+
const zones = await getAthleteZones(config);
|
|
484
|
+
const lines = [];
|
|
485
|
+
const formatZones = (label, zoneList) => {
|
|
486
|
+
if (!zoneList?.length)
|
|
487
|
+
return;
|
|
488
|
+
lines.push(`## ${label} Zones`, "");
|
|
489
|
+
zoneList.forEach((z, i) => {
|
|
490
|
+
const name = z["name"] ?? `Zone ${i + 1}`;
|
|
491
|
+
const min = z["min"] ?? z["from"] ?? "";
|
|
492
|
+
const max = z["max"] ?? z["to"] ?? "";
|
|
493
|
+
lines.push(` Z${i + 1} ${name}: ${min}–${max}`);
|
|
494
|
+
});
|
|
495
|
+
lines.push("");
|
|
496
|
+
};
|
|
497
|
+
formatZones("Power", zones.power);
|
|
498
|
+
formatZones("Heart Rate", zones.hr);
|
|
499
|
+
formatZones("Pace", zones.pace);
|
|
500
|
+
if (!lines.length)
|
|
501
|
+
lines.push("No zone data available.");
|
|
502
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
503
|
+
}
|
|
504
|
+
case "get_athlete_summary": {
|
|
505
|
+
const config = getConfig(args);
|
|
506
|
+
const summary = await getAthleteSummary(config);
|
|
507
|
+
const text = [
|
|
508
|
+
"## Current Fitness Snapshot",
|
|
509
|
+
"",
|
|
510
|
+
`CTL (Fitness): ${summary.ctl ?? "N/A"}`,
|
|
511
|
+
`ATL (Fatigue): ${summary.atl ?? "N/A"}`,
|
|
512
|
+
`TSB (Form): ${summary.tsb ?? "N/A"}`,
|
|
513
|
+
`Ramp Rate: ${summary.rampRate ?? "N/A"}`,
|
|
514
|
+
].join("\n");
|
|
515
|
+
return { content: [{ type: "text", text }] };
|
|
516
|
+
}
|
|
517
|
+
case "get_wellness_day": {
|
|
518
|
+
const date = z.string().parse(args["date"]);
|
|
519
|
+
const config = getConfig(args);
|
|
520
|
+
const w = await getWellnessDay(config, date);
|
|
521
|
+
return { content: [{ type: "text", text: formatWellness(date, w) }] };
|
|
522
|
+
}
|
|
523
|
+
case "update_wellness": {
|
|
524
|
+
const date = z.string().parse(args["date"]);
|
|
525
|
+
const config = getConfig(args);
|
|
526
|
+
const fields = ["weight", "restingHR", "hrv", "hrvSDNN", "sleepSecs", "sleepScore", "sleepQuality",
|
|
527
|
+
"readiness", "soreness", "fatigue", "stress", "mood", "motivation", "steps", "kcalConsumed", "spO2", "comments"];
|
|
528
|
+
const input = {};
|
|
529
|
+
for (const f of fields) {
|
|
530
|
+
if (args[f] !== undefined)
|
|
531
|
+
input[f] = args[f];
|
|
532
|
+
}
|
|
533
|
+
const w = await updateWellness(config, date, input);
|
|
534
|
+
return { content: [{ type: "text", text: `Updated wellness for ${date}:\n\n${formatWellness(date, w)}` }] };
|
|
535
|
+
}
|
|
536
|
+
case "get_activity_streams": {
|
|
537
|
+
const activityId = z.string().parse(args["activity_id"]);
|
|
538
|
+
const config = getConfig(args);
|
|
539
|
+
const types = args["types"];
|
|
540
|
+
const streams = await getActivityStreams(config, activityId, types);
|
|
541
|
+
const keys = Object.keys(streams);
|
|
542
|
+
if (!keys.length)
|
|
543
|
+
return { content: [{ type: "text", text: "No stream data returned." }] };
|
|
544
|
+
const lines = [`Streams for activity ${activityId}`, `Available: ${keys.join(", ")}`, ""];
|
|
545
|
+
for (const key of keys) {
|
|
546
|
+
const arr = streams[key];
|
|
547
|
+
lines.push(`**${key}** (${arr.length} points): first 5 = [${arr.slice(0, 5).join(", ")}]`);
|
|
548
|
+
}
|
|
549
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
550
|
+
}
|
|
551
|
+
case "get_activity_power_curves": {
|
|
552
|
+
const activityId = z.string().parse(args["activity_id"]);
|
|
553
|
+
const config = getConfig(args);
|
|
554
|
+
const curves = await getActivityPowerCurves(config, activityId);
|
|
555
|
+
return { content: [{ type: "text", text: `Power curves for activity ${activityId}:\n\n${JSON.stringify(curves, null, 2)}` }] };
|
|
556
|
+
}
|
|
557
|
+
case "list_workouts": {
|
|
558
|
+
const config = getConfig(args);
|
|
559
|
+
const workouts = await listWorkouts(config);
|
|
560
|
+
if (!workouts.length)
|
|
561
|
+
return { content: [{ type: "text", text: "No workouts in library." }] };
|
|
562
|
+
const text = workouts.map(w => [`**${w.name ?? "Unnamed"}** (${w.id})`, w.type ? `Type: ${w.type}` : "", w.description ? `${w.description}` : ""].filter(Boolean).join("\n")).join("\n\n---\n\n");
|
|
563
|
+
return { content: [{ type: "text", text }] };
|
|
564
|
+
}
|
|
565
|
+
case "create_workout": {
|
|
566
|
+
const config = getConfig(args);
|
|
567
|
+
const workout = await createWorkout(config, {
|
|
568
|
+
name: args["name"],
|
|
569
|
+
description: args["description"],
|
|
570
|
+
type: args["type"],
|
|
571
|
+
folder_id: args["folder_id"],
|
|
572
|
+
workout_doc: args["workout_doc"],
|
|
573
|
+
});
|
|
574
|
+
const text = [`Created workout **${workout.name ?? "Unnamed"}** (${workout.id})`, workout.type ? `Type: ${workout.type}` : ""].filter(Boolean).join("\n");
|
|
575
|
+
return { content: [{ type: "text", text }] };
|
|
576
|
+
}
|
|
577
|
+
case "bulk_create_events": {
|
|
578
|
+
const config = getConfig(args);
|
|
579
|
+
const events = args["events"];
|
|
580
|
+
const created = await bulkCreateEvents(config, events);
|
|
581
|
+
const text = [`Created ${created.length} events:`, "", ...created.map(e => formatEvent(e))].join("\n");
|
|
582
|
+
return { content: [{ type: "text", text }] };
|
|
583
|
+
}
|
|
584
|
+
case "bulk_delete_events": {
|
|
585
|
+
const config = getConfig(args);
|
|
586
|
+
const ids = z.array(z.string()).parse(args["event_ids"]);
|
|
587
|
+
await bulkDeleteEvents(config, ids);
|
|
588
|
+
return { content: [{ type: "text", text: `Deleted ${ids.length} events: ${ids.join(", ")}` }] };
|
|
589
|
+
}
|
|
590
|
+
default:
|
|
591
|
+
return {
|
|
592
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
593
|
+
isError: true,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
599
|
+
return {
|
|
600
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
601
|
+
isError: true,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
async function main() {
|
|
606
|
+
const transport = new StdioServerTransport();
|
|
607
|
+
await server.connect(transport);
|
|
608
|
+
console.error("Intervals MCP server running on stdio");
|
|
609
|
+
}
|
|
610
|
+
main().catch((err) => {
|
|
611
|
+
console.error("Fatal error:", err);
|
|
612
|
+
process.exit(1);
|
|
613
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "intervals-icu-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Intervals.icu — 20 tools for activities, wellness, calendar events, workouts and more",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"intervals-icu-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsc --watch",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
14
|
+
"prepare": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"intervals.icu",
|
|
23
|
+
"cycling",
|
|
24
|
+
"training",
|
|
25
|
+
"fitness",
|
|
26
|
+
"claude",
|
|
27
|
+
"ai"
|
|
28
|
+
],
|
|
29
|
+
"author": "SergeyPirogov",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/SergeyPirogov/intervals-mcp.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/SergeyPirogov/intervals-mcp#readme",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
38
|
+
"dotenv": "^16.4.7",
|
|
39
|
+
"zod": "^3.24.4"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.15.18",
|
|
43
|
+
"typescript": "^5.8.3"
|
|
44
|
+
}
|
|
45
|
+
}
|