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 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
- ## Tools
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
- npm install
22
- npm run build
8
+ npx -y intervals-icu-mcp-server
23
9
  ```
24
10
 
25
- ### 2. Configure credentials
11
+ Or install globally:
26
12
 
27
13
  ```bash
28
- cp .env.example .env
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
- - **API_KEY**: Settings → API → Generate key on intervals.icu
33
- - **ATHLETE_ID**: The `i12345` part of your intervals.icu profile URL
17
+ ## Setup
34
18
 
35
- ### 3. Add to Claude Code
19
+ ### Claude Desktop
36
20
 
37
- Add this to your `~/.claude/settings.json` (or project `.claude/settings.json`):
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": "node",
44
- "args": ["/Users/spirohov/work/intervals/dist/index.js"],
26
+ "intervals-icu": {
27
+ "command": "npx",
28
+ "args": ["-y", "intervals-icu-mcp-server"],
45
29
  "env": {
46
- "API_KEY": "your_key_here",
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
- Or use a `.env` file — the server loads it automatically from the project root.
55
-
56
- ### 4. Add to Claude Desktop
38
+ ### Claude Code
57
39
 
58
- In `~/Library/Application Support/Claude/claude_desktop_config.json`:
40
+ Add to `~/.claude/settings.json`:
59
41
 
60
42
  ```json
61
43
  {
62
44
  "mcpServers": {
63
- "intervals": {
64
- "command": "node",
65
- "args": ["/Users/spirohov/work/intervals/dist/index.js"],
45
+ "intervals-icu": {
46
+ "command": "npx",
47
+ "args": ["-y", "intervals-icu-mcp-server"],
66
48
  "env": {
67
- "API_KEY": "your_key_here",
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}/event/${eventId}`, config);
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
@@ -1 +1,2 @@
1
+ #!/usr/bin/env node
1
2
  import "dotenv/config";
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.0",
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": "./dist/index.js"
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",