intervals-icu-mcp-server 0.1.0 → 0.1.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 +67 -36
- package/dist/client.d.ts +37 -0
- package/dist/client.js +25 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +71 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,49 +1,33 @@
|
|
|
1
|
-
# intervals-mcp
|
|
1
|
+
# intervals-icu-mcp-server
|
|
2
2
|
|
|
3
3
|
MCP server for [Intervals.icu](https://intervals.icu) — connects Claude / Claude Code to your training data.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
| Tool | Description |
|
|
8
|
-
|------|-------------|
|
|
9
|
-
| `get_activities` | List activities with power, HR, training load, etc. |
|
|
10
|
-
| `get_activity_details` | Full metrics for a specific activity |
|
|
11
|
-
| `get_activity_intervals` | Per-interval breakdown (power, HR, speed, elevation) |
|
|
12
|
-
| `get_wellness_data` | Weight, HRV, sleep, CTL/ATL, subjective scores |
|
|
13
|
-
| `get_events` | Calendar events (workouts, races) |
|
|
14
|
-
| `get_event_by_id` | Full details for a specific event |
|
|
15
|
-
|
|
16
|
-
## Setup
|
|
17
|
-
|
|
18
|
-
### 1. Install & build
|
|
5
|
+
## Installation
|
|
19
6
|
|
|
20
7
|
```bash
|
|
21
|
-
|
|
22
|
-
npm run build
|
|
8
|
+
npx -y intervals-icu-mcp-server
|
|
23
9
|
```
|
|
24
10
|
|
|
25
|
-
|
|
11
|
+
Or install globally:
|
|
26
12
|
|
|
27
13
|
```bash
|
|
28
|
-
|
|
29
|
-
# Edit .env and fill in your API_KEY and ATHLETE_ID
|
|
14
|
+
npm install -g intervals-icu-mcp-server
|
|
30
15
|
```
|
|
31
16
|
|
|
32
|
-
|
|
33
|
-
- **ATHLETE_ID**: The `i12345` part of your intervals.icu profile URL
|
|
17
|
+
## Setup
|
|
34
18
|
|
|
35
|
-
###
|
|
19
|
+
### Claude Desktop
|
|
36
20
|
|
|
37
|
-
Add
|
|
21
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
38
22
|
|
|
39
23
|
```json
|
|
40
24
|
{
|
|
41
25
|
"mcpServers": {
|
|
42
|
-
"intervals": {
|
|
43
|
-
"command": "
|
|
44
|
-
"args": ["
|
|
26
|
+
"intervals-icu": {
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "intervals-icu-mcp-server"],
|
|
45
29
|
"env": {
|
|
46
|
-
"API_KEY": "
|
|
30
|
+
"API_KEY": "your_api_key",
|
|
47
31
|
"ATHLETE_ID": "i12345"
|
|
48
32
|
}
|
|
49
33
|
}
|
|
@@ -51,20 +35,18 @@ Add this to your `~/.claude/settings.json` (or project `.claude/settings.json`):
|
|
|
51
35
|
}
|
|
52
36
|
```
|
|
53
37
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
### 4. Add to Claude Desktop
|
|
38
|
+
### Claude Code
|
|
57
39
|
|
|
58
|
-
|
|
40
|
+
Add to `~/.claude/settings.json`:
|
|
59
41
|
|
|
60
42
|
```json
|
|
61
43
|
{
|
|
62
44
|
"mcpServers": {
|
|
63
|
-
"intervals": {
|
|
64
|
-
"command": "
|
|
65
|
-
"args": ["
|
|
45
|
+
"intervals-icu": {
|
|
46
|
+
"command": "npx",
|
|
47
|
+
"args": ["-y", "intervals-icu-mcp-server"],
|
|
66
48
|
"env": {
|
|
67
|
-
"API_KEY": "
|
|
49
|
+
"API_KEY": "your_api_key",
|
|
68
50
|
"ATHLETE_ID": "i12345"
|
|
69
51
|
}
|
|
70
52
|
}
|
|
@@ -72,9 +54,58 @@ In `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
72
54
|
}
|
|
73
55
|
```
|
|
74
56
|
|
|
57
|
+
- **API_KEY**: Settings → API → Generate key on intervals.icu
|
|
58
|
+
- **ATHLETE_ID**: The `i12345` part of your intervals.icu profile URL
|
|
59
|
+
|
|
60
|
+
## Tools
|
|
61
|
+
|
|
62
|
+
### Activities
|
|
63
|
+
| Tool | Description |
|
|
64
|
+
|------|-------------|
|
|
65
|
+
| `get_activities` | List activities with power, HR, training load, etc. |
|
|
66
|
+
| `get_activity_details` | Full metrics for a specific activity |
|
|
67
|
+
| `get_activity_intervals` | Per-interval breakdown (power, HR, speed, elevation) |
|
|
68
|
+
| `get_activity_streams` | Raw per-second data streams (watts, HR, cadence, speed, altitude) |
|
|
69
|
+
| `get_activity_power_curves` | Best power for all durations (5s, 1min, 5min, 20min, etc.) |
|
|
70
|
+
|
|
71
|
+
### Wellness & Fitness
|
|
72
|
+
| Tool | Description |
|
|
73
|
+
|------|-------------|
|
|
74
|
+
| `get_wellness_data` | Weight, HRV, sleep, CTL/ATL, subjective scores for a date range |
|
|
75
|
+
| `get_wellness_day` | Wellness data for a single date |
|
|
76
|
+
| `update_wellness` | Log weight, HRV, sleep, mood, soreness, etc. |
|
|
77
|
+
|
|
78
|
+
### Athlete
|
|
79
|
+
| Tool | Description |
|
|
80
|
+
|------|-------------|
|
|
81
|
+
| `get_athlete_profile` | FTP, LTHR, weight, VO2max, resting HR |
|
|
82
|
+
| `get_athlete_zones` | Power, HR and pace training zones |
|
|
83
|
+
| `get_athlete_summary` | Current CTL, ATL, TSB and ramp rate snapshot |
|
|
84
|
+
|
|
85
|
+
### Calendar Events
|
|
86
|
+
| Tool | Description |
|
|
87
|
+
|------|-------------|
|
|
88
|
+
| `get_events` | Calendar events (workouts, races) for a date range |
|
|
89
|
+
| `get_event_by_id` | Full details for a specific event |
|
|
90
|
+
| `create_event` | Create a new event (workout, note, race) |
|
|
91
|
+
| `update_event` | Update event fields or move it to a new date |
|
|
92
|
+
| `delete_event` | Delete an event |
|
|
93
|
+
| `bulk_create_events` | Create multiple events at once |
|
|
94
|
+
| `bulk_delete_events` | Delete multiple events at once |
|
|
95
|
+
|
|
96
|
+
### Workout Library
|
|
97
|
+
| Tool | Description |
|
|
98
|
+
|------|-------------|
|
|
99
|
+
| `list_workouts` | List all workouts in the library |
|
|
100
|
+
| `create_workout` | Save a new structured workout to the library |
|
|
101
|
+
|
|
75
102
|
## Development
|
|
76
103
|
|
|
77
104
|
```bash
|
|
105
|
+
git clone https://github.com/SergeyPirogov/intervals-mcp.git
|
|
106
|
+
cd intervals-mcp
|
|
107
|
+
npm install
|
|
108
|
+
npm run build
|
|
78
109
|
npm run dev # watch mode
|
|
79
110
|
npm run inspector # MCP inspector UI
|
|
80
111
|
```
|
package/dist/client.d.ts
CHANGED
|
@@ -346,6 +346,7 @@ export declare function getEvents(config: ClientConfig, params: {
|
|
|
346
346
|
endDate: string;
|
|
347
347
|
}): Promise<Event[]>;
|
|
348
348
|
export declare function getEventById(config: ClientConfig, eventId: string): Promise<Event>;
|
|
349
|
+
export declare function getNotesByDate(config: ClientConfig, date: string): Promise<Event[]>;
|
|
349
350
|
export declare function createEvent(config: ClientConfig, input: EventInput): Promise<Event>;
|
|
350
351
|
export declare function updateEvent(config: ClientConfig, eventId: string, input: EventInput): Promise<Event>;
|
|
351
352
|
export declare function deleteEvent(config: ClientConfig, eventId: string): Promise<void>;
|
|
@@ -360,3 +361,39 @@ export declare function listWorkouts(config: ClientConfig): Promise<Workout[]>;
|
|
|
360
361
|
export declare function createWorkout(config: ClientConfig, input: WorkoutInput): Promise<Workout>;
|
|
361
362
|
export declare function bulkCreateEvents(config: ClientConfig, events: EventInput[]): Promise<Event[]>;
|
|
362
363
|
export declare function bulkDeleteEvents(config: ClientConfig, eventIds: (string | number)[]): Promise<void>;
|
|
364
|
+
export declare const AthleteFitnessSchema: z.ZodObject<{
|
|
365
|
+
date: z.ZodOptional<z.ZodString>;
|
|
366
|
+
fitness: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
367
|
+
fatigue: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
368
|
+
form: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
369
|
+
rampRate: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
370
|
+
training_load: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
371
|
+
eftp: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
372
|
+
eftpPerKg: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
373
|
+
weight: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
374
|
+
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
375
|
+
date: z.ZodOptional<z.ZodString>;
|
|
376
|
+
fitness: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
377
|
+
fatigue: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
378
|
+
form: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
379
|
+
rampRate: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
380
|
+
training_load: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
381
|
+
eftp: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
382
|
+
eftpPerKg: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
383
|
+
weight: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
384
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
385
|
+
date: z.ZodOptional<z.ZodString>;
|
|
386
|
+
fitness: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
387
|
+
fatigue: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
388
|
+
form: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
389
|
+
rampRate: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
390
|
+
training_load: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
391
|
+
eftp: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
392
|
+
eftpPerKg: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
393
|
+
weight: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
394
|
+
}, z.ZodTypeAny, "passthrough">>;
|
|
395
|
+
export type AthleteFitness = z.infer<typeof AthleteFitnessSchema>;
|
|
396
|
+
export declare function getAthleteFitness(config: ClientConfig, params: {
|
|
397
|
+
startDate: string;
|
|
398
|
+
endDate: string;
|
|
399
|
+
}): Promise<AthleteFitness[]>;
|
package/dist/client.js
CHANGED
|
@@ -155,9 +155,17 @@ export async function getEvents(config, params) {
|
|
|
155
155
|
return z.array(EventSchema).parse(data);
|
|
156
156
|
}
|
|
157
157
|
export async function getEventById(config, eventId) {
|
|
158
|
-
const data = await request(`/athlete/${config.athleteId}/
|
|
158
|
+
const data = await request(`/athlete/${config.athleteId}/events/${eventId}`, config);
|
|
159
159
|
return EventSchema.parse(data);
|
|
160
160
|
}
|
|
161
|
+
export async function getNotesByDate(config, date) {
|
|
162
|
+
const data = await request(`/athlete/${config.athleteId}/events`, config, { oldest: date, newest: date });
|
|
163
|
+
const events = z.array(EventSchema).parse(data);
|
|
164
|
+
return events.filter((e) => {
|
|
165
|
+
const raw = e;
|
|
166
|
+
return raw["category"] === "NOTE" || (!e.race && !e.workout);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
161
169
|
async function mutate(method, path, config, body) {
|
|
162
170
|
const url = `${BASE_URL}${path}`;
|
|
163
171
|
const res = await fetch(url, {
|
|
@@ -234,3 +242,19 @@ export async function bulkCreateEvents(config, events) {
|
|
|
234
242
|
export async function bulkDeleteEvents(config, eventIds) {
|
|
235
243
|
await mutate("POST", `/athlete/${config.athleteId}/events/bulk-delete`, config, eventIds);
|
|
236
244
|
}
|
|
245
|
+
export const AthleteFitnessSchema = z.object({
|
|
246
|
+
date: z.string().optional(),
|
|
247
|
+
fitness: z.number().nullable().optional(),
|
|
248
|
+
fatigue: z.number().nullable().optional(),
|
|
249
|
+
form: z.number().nullable().optional(),
|
|
250
|
+
rampRate: z.number().nullable().optional(),
|
|
251
|
+
training_load: z.number().nullable().optional(),
|
|
252
|
+
eftp: z.number().nullable().optional(),
|
|
253
|
+
eftpPerKg: z.number().nullable().optional(),
|
|
254
|
+
weight: z.number().nullable().optional(),
|
|
255
|
+
}).passthrough();
|
|
256
|
+
export async function getAthleteFitness(config, params) {
|
|
257
|
+
const data = await request(`/athlete/${config.athleteId}/athlete-summary.json`, config, { oldest: params.startDate, newest: params.endDate, athleteId: config.athleteId });
|
|
258
|
+
const all = z.array(AthleteFitnessSchema).parse(data);
|
|
259
|
+
return all.filter((e) => e["athlete_id"] === config.athleteId);
|
|
260
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import "dotenv/config";
|
|
2
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
5
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
6
|
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 { getActivities, getActivityDetails, getActivityIntervals, getWellness, getEvents, getEventById, getNotesByDate, createEvent, updateEvent, deleteEvent, getAthleteProfile, getAthleteZones, getAthleteSummary, getWellnessDay, updateWellness, getActivityStreams, getActivityPowerCurves, listWorkouts, createWorkout, bulkCreateEvents, bulkDeleteEvents, getAthleteFitness, } from "./client.js";
|
|
7
8
|
import { formatActivity, formatWellness, formatEvent, formatIntervalRow, } from "./format.js";
|
|
8
9
|
function getConfig(args) {
|
|
9
10
|
const apiKey = args["api_key"] ?? process.env.API_KEY ?? "";
|
|
@@ -298,6 +299,32 @@ const TOOLS = [
|
|
|
298
299
|
},
|
|
299
300
|
},
|
|
300
301
|
},
|
|
302
|
+
{
|
|
303
|
+
name: "get_athlete_fitness",
|
|
304
|
+
description: "Get Fitness (CTL), Fatigue (ATL), Form (TSB), ramp rate and eFTP from the Fitness chart for a date range. Matches what you see on the Intervals.icu Fitness page.",
|
|
305
|
+
inputSchema: {
|
|
306
|
+
type: "object",
|
|
307
|
+
properties: {
|
|
308
|
+
start_date: { type: "string", description: "Start date YYYY-MM-DD (default: 30 days ago)" },
|
|
309
|
+
end_date: { type: "string", description: "End date YYYY-MM-DD (default: today)" },
|
|
310
|
+
athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
|
|
311
|
+
api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "get_note",
|
|
317
|
+
description: "Get a specific note/event by ID, or list all notes for a given date.",
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: {
|
|
321
|
+
event_id: { type: "string", description: "Event/note ID to fetch by ID" },
|
|
322
|
+
date: { type: "string", description: "Date YYYY-MM-DD to list all notes for that day" },
|
|
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
|
+
},
|
|
301
328
|
{
|
|
302
329
|
name: "bulk_create_events",
|
|
303
330
|
description: "Create multiple calendar events at once.",
|
|
@@ -432,6 +459,49 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
432
459
|
const event = await getEventById(config, eventId);
|
|
433
460
|
return { content: [{ type: "text", text: formatEvent(event) }] };
|
|
434
461
|
}
|
|
462
|
+
case "get_athlete_fitness": {
|
|
463
|
+
const config = getConfig(args);
|
|
464
|
+
const startDate = args["start_date"] ?? daysAgo(30);
|
|
465
|
+
const endDate = args["end_date"] ?? today();
|
|
466
|
+
const entries = await getAthleteFitness(config, { startDate, endDate });
|
|
467
|
+
if (!entries.length) {
|
|
468
|
+
return { content: [{ type: "text", text: "No fitness data found for the specified date range." }] };
|
|
469
|
+
}
|
|
470
|
+
const lines = entries.map((e) => {
|
|
471
|
+
const ctl = e.fitness != null ? e.fitness.toFixed(1) : "N/A";
|
|
472
|
+
const atl = e.fatigue != null ? e.fatigue.toFixed(1) : "N/A";
|
|
473
|
+
const tsb = e.form != null ? e.form.toFixed(1) : "N/A";
|
|
474
|
+
const ramp = e.rampRate != null ? e.rampRate.toFixed(1) : "N/A";
|
|
475
|
+
const eftp = e.eftp != null ? `${e.eftp.toFixed(0)}W` : "N/A";
|
|
476
|
+
const eftpKg = e.eftpPerKg != null ? `${e.eftpPerKg.toFixed(2)} W/kg` : "N/A";
|
|
477
|
+
const tl = e.training_load != null ? e.training_load.toFixed(0) : "N/A";
|
|
478
|
+
return [
|
|
479
|
+
`**${e.date ?? "N/A"}**`,
|
|
480
|
+
` CTL (Fitness): ${ctl} | ATL (Fatigue): ${atl} | TSB (Form): ${tsb}`,
|
|
481
|
+
` Ramp Rate: ${ramp} | eFTP: ${eftp} (${eftpKg}) | TL: ${tl}`,
|
|
482
|
+
].join("\n");
|
|
483
|
+
});
|
|
484
|
+
return { content: [{ type: "text", text: lines.join("\n\n") }] };
|
|
485
|
+
}
|
|
486
|
+
case "get_note": {
|
|
487
|
+
const config = getConfig(args);
|
|
488
|
+
const eventId = args["event_id"];
|
|
489
|
+
const date = args["date"];
|
|
490
|
+
if (eventId) {
|
|
491
|
+
const event = await getEventById(config, eventId);
|
|
492
|
+
return { content: [{ type: "text", text: formatEvent(event) }] };
|
|
493
|
+
}
|
|
494
|
+
else if (date) {
|
|
495
|
+
const notes = await getNotesByDate(config, date);
|
|
496
|
+
if (!notes.length)
|
|
497
|
+
return { content: [{ type: "text", text: `No notes found for ${date}.` }] };
|
|
498
|
+
const text = notes.map(formatEvent).join("\n\n---\n\n");
|
|
499
|
+
return { content: [{ type: "text", text }] };
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
return { content: [{ type: "text", text: "Provide either event_id or date." }], isError: true };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
435
505
|
case "create_event": {
|
|
436
506
|
const config = getConfig(args);
|
|
437
507
|
const event = await createEvent(config, {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "intervals-icu-mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "MCP server for Intervals.icu — 20 tools for activities, wellness, calendar events, workouts and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"intervals-icu-mcp": "
|
|
7
|
+
"intervals-icu-mcp": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "tsc",
|
|
10
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
11
11
|
"dev": "tsc --watch",
|
|
12
12
|
"start": "node dist/index.js",
|
|
13
13
|
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js",
|