intervals-icu-mcp-server 0.4.0 → 0.6.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 CHANGED
@@ -78,10 +78,18 @@ Add to `~/.claude/settings.json`:
78
78
  ### Athlete
79
79
  | Tool | Description |
80
80
  |------|-------------|
81
+ | `list_athletes` | List athletes you follow or coach (including yourself) — find athlete IDs for a coach account |
82
+ | `get_coach_ticks` | Get coach-tick presets on an athlete's profile (in practice a fixed 1-5 rating scale) |
83
+ | `set_activity_coach_tick` | Set or clear the coach's 1-5 rating on a specific activity |
81
84
  | `get_athlete_profile` | FTP, LTHR, weight, VO2max, resting HR |
82
85
  | `get_athlete_zones` | Power, HR and pace training zones |
83
86
  | `get_athlete_summary` | Current CTL, ATL, TSB and ramp rate snapshot |
84
87
 
88
+ **Coach accounts:** every tool accepts an optional `athlete_id` argument that overrides the
89
+ `ATHLETE_ID` env var for that call. Log in with a coach's `API_KEY`, call `list_athletes` to get
90
+ IDs for the athletes they coach, then pass `athlete_id: "iXXXXX"` on any tool call to work with
91
+ that specific athlete — no need to swap accounts or `.env` files.
92
+
85
93
  ### Calendar Events
86
94
  | Tool | Description |
87
95
  |------|-------------|
package/dist/client.d.ts CHANGED
@@ -99,6 +99,7 @@ export declare const ActivitySchema: z.ZodObject<{
99
99
  carbs_used: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
100
100
  carbs_ingested: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
101
101
  avg_lr_balance: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
102
+ coach_tick: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
102
103
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
103
104
  id: z.ZodString;
104
105
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -195,6 +196,7 @@ export declare const ActivitySchema: z.ZodObject<{
195
196
  carbs_used: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
196
197
  carbs_ingested: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
197
198
  avg_lr_balance: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
199
+ coach_tick: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
198
200
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
199
201
  id: z.ZodString;
200
202
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -291,8 +293,20 @@ export declare const ActivitySchema: z.ZodObject<{
291
293
  carbs_used: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
292
294
  carbs_ingested: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
293
295
  avg_lr_balance: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
296
+ coach_tick: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
294
297
  }, z.ZodTypeAny, "passthrough">>;
295
298
  export type Activity = z.infer<typeof ActivitySchema>;
299
+ export declare const CoachTickSchema: z.ZodObject<{
300
+ id: z.ZodOptional<z.ZodNumber>;
301
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
302
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
303
+ id: z.ZodOptional<z.ZodNumber>;
304
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
305
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
306
+ id: z.ZodOptional<z.ZodNumber>;
307
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
308
+ }, z.ZodTypeAny, "passthrough">>;
309
+ export type CoachTick = z.infer<typeof CoachTickSchema>;
296
310
  export declare const WellnessSchema: z.ZodObject<{
297
311
  id: z.ZodOptional<z.ZodString>;
298
312
  ctl: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
@@ -498,6 +512,18 @@ export declare const AthleteSchema: z.ZodObject<{
498
512
  country: z.ZodOptional<z.ZodNullable<z.ZodString>>;
499
513
  icu_resting_hr: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
500
514
  icu_weight: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
515
+ icu_coach: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
516
+ icu_permission: z.ZodOptional<z.ZodNullable<z.ZodString>>;
517
+ coach_ticks: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
518
+ id: z.ZodOptional<z.ZodNumber>;
519
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
520
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
521
+ id: z.ZodOptional<z.ZodNumber>;
522
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
523
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
524
+ id: z.ZodOptional<z.ZodNumber>;
525
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
526
+ }, z.ZodTypeAny, "passthrough">>, "many">>>;
501
527
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
502
528
  id: z.ZodOptional<z.ZodString>;
503
529
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -509,6 +535,18 @@ export declare const AthleteSchema: z.ZodObject<{
509
535
  country: z.ZodOptional<z.ZodNullable<z.ZodString>>;
510
536
  icu_resting_hr: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
511
537
  icu_weight: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
538
+ icu_coach: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
539
+ icu_permission: z.ZodOptional<z.ZodNullable<z.ZodString>>;
540
+ coach_ticks: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
541
+ id: z.ZodOptional<z.ZodNumber>;
542
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
543
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
544
+ id: z.ZodOptional<z.ZodNumber>;
545
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
546
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
547
+ id: z.ZodOptional<z.ZodNumber>;
548
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
549
+ }, z.ZodTypeAny, "passthrough">>, "many">>>;
512
550
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
513
551
  id: z.ZodOptional<z.ZodString>;
514
552
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -520,6 +558,18 @@ export declare const AthleteSchema: z.ZodObject<{
520
558
  country: z.ZodOptional<z.ZodNullable<z.ZodString>>;
521
559
  icu_resting_hr: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
522
560
  icu_weight: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
561
+ icu_coach: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
562
+ icu_permission: z.ZodOptional<z.ZodNullable<z.ZodString>>;
563
+ coach_ticks: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
564
+ id: z.ZodOptional<z.ZodNumber>;
565
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
566
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
567
+ id: z.ZodOptional<z.ZodNumber>;
568
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
569
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
570
+ id: z.ZodOptional<z.ZodNumber>;
571
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
572
+ }, z.ZodTypeAny, "passthrough">>, "many">>>;
523
573
  }, z.ZodTypeAny, "passthrough">>;
524
574
  export type Athlete = z.infer<typeof AthleteSchema>;
525
575
  export declare const SportSettingsSchema: z.ZodObject<{
@@ -678,6 +728,8 @@ export declare function createEvent(config: ClientConfig, input: EventInput): Pr
678
728
  export declare function updateEvent(config: ClientConfig, eventId: string, input: EventInput): Promise<Event>;
679
729
  export declare function deleteEvent(config: ClientConfig, eventId: string): Promise<void>;
680
730
  export declare function getAthleteProfile(config: ClientConfig): Promise<Athlete>;
731
+ export declare function listAthletes(config: ClientConfig): Promise<Athlete[]>;
732
+ export declare function setActivityCoachTick(config: ClientConfig, activityId: string, coachTickId: number | null): Promise<Activity>;
681
733
  export declare function getAthleteZones(config: ClientConfig): Promise<SportSettings[]>;
682
734
  export declare function getAthleteSummary(config: ClientConfig): Promise<AthleteFitness | undefined>;
683
735
  export declare function getWellnessDay(config: ClientConfig, date: string): Promise<Wellness>;
package/dist/client.js CHANGED
@@ -97,6 +97,14 @@ export const ActivitySchema = z.object({
97
97
  carbs_used: z.number().nullable().optional(),
98
98
  carbs_ingested: z.number().nullable().optional(),
99
99
  avg_lr_balance: z.number().nullable().optional(),
100
+ // id of one of the athlete's coach_ticks presets (see AthleteSchema), or null if unset.
101
+ coach_tick: z.number().nullable().optional(),
102
+ }).passthrough();
103
+ // A coach-configured quick-rating preset (e.g. "🔥 Great", "⚠️ Review") an athlete's activities
104
+ // can be tagged with via Activity.coach_tick (which stores just the preset's id).
105
+ export const CoachTickSchema = z.object({
106
+ id: z.number().optional(),
107
+ text: z.string().nullable().optional(),
100
108
  }).passthrough();
101
109
  export const WellnessSchema = z.object({
102
110
  id: z.string().optional(),
@@ -161,6 +169,11 @@ export const AthleteSchema = z.object({
161
169
  country: z.string().nullable().optional(),
162
170
  icu_resting_hr: z.number().nullable().optional(),
163
171
  icu_weight: z.number().nullable().optional(),
172
+ icu_coach: z.boolean().nullable().optional(),
173
+ // Present only on entries returned by /athletes: the caller's access level on this athlete
174
+ // (NONE/READ/WRITE for someone they follow/coach, absent for their own profile).
175
+ icu_permission: z.string().nullable().optional(),
176
+ coach_ticks: z.array(CoachTickSchema).nullable().optional(),
164
177
  }).passthrough();
165
178
  export const SportSettingsSchema = z.object({
166
179
  types: z.array(z.string()).nullable().optional(),
@@ -293,6 +306,22 @@ export async function getAthleteProfile(config) {
293
306
  const data = await request(`/athlete/${config.athleteId}`, config);
294
307
  return AthleteSchema.parse(data);
295
308
  }
309
+ // Requires an API_KEY (not a bearer token). Athlete ID in `config` is ignored — this call
310
+ // is not athlete-scoped, it lists everyone the key's account follows or coaches.
311
+ export async function listAthletes(config) {
312
+ const data = await request(`/athletes`, config);
313
+ return z.array(AthleteSchema).parse(data);
314
+ }
315
+ // coach_tick is a fixed 1-5 rating scale (verified against the live API — PUT /athlete/{id}
316
+ // with a custom coach_ticks list is accepted but never persists, so presets aren't
317
+ // API-configurable). A JSON null is silently ignored by the server (treated as "no change");
318
+ // -1 is what actually clears an existing rating.
319
+ export async function setActivityCoachTick(config, activityId, coachTickId) {
320
+ const data = await mutate("PUT", `/activity/${activityId}`, config, {
321
+ coach_tick: coachTickId ?? -1,
322
+ });
323
+ return ActivitySchema.parse(data);
324
+ }
296
325
  export async function getAthleteZones(config) {
297
326
  const data = await request(`/athlete/${config.athleteId}/sport-settings`, config);
298
327
  return z.array(SportSettingsSchema).parse(data);
package/dist/format.js CHANGED
@@ -44,6 +44,9 @@ export function formatActivity(a) {
44
44
  }
45
45
  }
46
46
  lines.push("", "Other:", ` Cadence: ${n(a.average_cadence, " rpm")} | Calories: ${n(a.calories)} | Speed: ${n(a.average_speed, " m/s")} (max ${n(a.max_speed, " m/s")})`, ` Temp: ${n(a.average_temp, "°C")} | Trainer: ${a.trainer ? "Yes" : "No"} | Coasting: ${fmtDuration(a.coasting_time)}`, ` RPE: ${n(a.icu_rpe ?? a.perceived_exertion)}/10 | Feel: ${n(a.feel)}/5 | S-RPE: ${n(a.session_rpe)}`, ` L/R Balance: ${a.avg_lr_balance != null ? `${(100 - a.avg_lr_balance).toFixed(0)}/${a.avg_lr_balance.toFixed(0)}` : "N/A"}`, "", "Fitness:", ` CTL: ${n(a.icu_ctl)} | ATL: ${n(a.icu_atl)} | TRIMP: ${n(a.trimp)} | Polarization Index: ${n(a.polarization_index)}`, ` Power Load: ${n(a.power_load)} | HR Load: ${n(a.hr_load)} | Strain: ${n(a.strain_score)}`, ` SS CP: ${n(a.ss_cp, " W")} | SS W': ${n(a.ss_w_prime)} | SS Pmax: ${n(a.ss_p_max)}`, ` Weight: ${n(a.icu_weight, " kg")} | CHO Used: ${n(a.carbs_used, " g")} | CHO In: ${n(a.carbs_ingested, " g")}`, "", "Equipment:", ` Gear: ${n(a.gear?.name)} | Device: ${n(a.device_name)} | Power Meter: ${n(a.power_meter)}`, ` Source: ${n(a.source)} | Compliance: ${a.compliance != null ? `${a.compliance}%` : "N/A"}`);
47
+ if (a.coach_tick != null) {
48
+ lines.push(` Coach Tick: [${a.coach_tick}] (see get_coach_ticks for the label)`);
49
+ }
47
50
  if (a.description)
48
51
  lines.push("", `Description: ${a.description}`);
49
52
  return lines.join("\n");
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { z } from "zod";
7
- import { getActivities, getActivityDetails, getActivityIntervals, getWellness, getEvents, getEventById, getNotesByDate, createEvent, updateEvent, deleteEvent, getAthleteProfile, getAthleteZones, getAthleteSummary, getWellnessDay, updateWellness, getActivityStreams, getActivityPowerCurves, listWorkouts, createWorkout, bulkCreateEvents, bulkDeleteEvents, getAthleteFitness, getActivityMessages, addActivityMessage, deleteActivityMessage, updateActivityMessage, } from "./client.js";
7
+ import { getActivities, getActivityDetails, getActivityIntervals, getWellness, getEvents, getEventById, getNotesByDate, createEvent, updateEvent, deleteEvent, getAthleteProfile, getAthleteZones, getAthleteSummary, listAthletes, setActivityCoachTick, getWellnessDay, updateWellness, getActivityStreams, getActivityPowerCurves, listWorkouts, createWorkout, bulkCreateEvents, bulkDeleteEvents, getAthleteFitness, getActivityMessages, addActivityMessage, deleteActivityMessage, updateActivityMessage, } from "./client.js";
8
8
  import { formatActivity, formatWellness, formatEvent, formatIntervalRow, formatSportZones, formatMessage, } from "./format.js";
9
9
  function getConfig(args) {
10
10
  const apiKey = args["api_key"] ?? process.env.API_KEY ?? "";
@@ -17,6 +17,14 @@ function getConfig(args) {
17
17
  throw new Error("No athlete ID provided. Set ATHLETE_ID in .env or pass athlete_id.");
18
18
  return { apiKey, athleteId };
19
19
  }
20
+ // list_athletes isn't athlete-scoped (it lists everyone the key's account can access), so it
21
+ // only needs the API key — requiring athlete_id here would break coaches with no default athlete.
22
+ function getApiKey(args) {
23
+ const apiKey = args["api_key"] ?? process.env.API_KEY ?? "";
24
+ if (!apiKey)
25
+ throw new Error("No API key provided. Set API_KEY in .env or pass api_key.");
26
+ return apiKey;
27
+ }
20
28
  function toDateStr(d) {
21
29
  const y = d.getFullYear();
22
30
  const m = String(d.getMonth() + 1).padStart(2, "0");
@@ -175,6 +183,41 @@ const TOOLS = [
175
183
  },
176
184
  },
177
185
  },
186
+ {
187
+ name: "list_athletes",
188
+ description: "List athletes you follow or coach, including yourself. Use this with a coach account to find athlete IDs, then pass athlete_id on other tools (get_activities, get_wellness_data, get_events, etc.) to work with a specific athlete under that same account.",
189
+ inputSchema: {
190
+ type: "object",
191
+ properties: {
192
+ api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
193
+ },
194
+ },
195
+ },
196
+ {
197
+ name: "get_coach_ticks",
198
+ description: "Get the coach-tick rating configured on an athlete's profile presets, if any. In practice coach_tick is a fixed 1-5 rating scale on Intervals.icu — use set_activity_coach_tick to rate an activity.",
199
+ inputSchema: {
200
+ type: "object",
201
+ properties: {
202
+ athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
203
+ api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
204
+ },
205
+ },
206
+ },
207
+ {
208
+ name: "set_activity_coach_tick",
209
+ description: "Set or clear the coach's rating on a specific activity (Intervals.icu's coach-tick feature: a 1-5 scale). Pass coach_tick_id 1-5 to rate it, or omit to clear the current rating.",
210
+ inputSchema: {
211
+ type: "object",
212
+ required: ["activity_id"],
213
+ properties: {
214
+ activity_id: { type: "string", description: "The activity ID" },
215
+ coach_tick_id: { type: "number", description: "Rating 1-5. Omit to clear the current rating." },
216
+ athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
217
+ api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
218
+ },
219
+ },
220
+ },
178
221
  {
179
222
  name: "get_athlete_profile",
180
223
  description: "Get athlete profile: name, sex, DOB, weight, resting HR, city/country. For FTP/LTHR/zones use get_athlete_zones.",
@@ -602,6 +645,43 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
602
645
  await deleteEvent(config, eventId);
603
646
  return { content: [{ type: "text", text: `Event ${eventId} deleted.` }] };
604
647
  }
648
+ case "list_athletes": {
649
+ const apiKey = getApiKey(args);
650
+ const athletes = await listAthletes({ apiKey, athleteId: "" });
651
+ if (!athletes.length)
652
+ return { content: [{ type: "text", text: "No athletes found." }] };
653
+ const lines = athletes.map((a) => {
654
+ const perm = a.icu_permission ? ` [${a.icu_permission}]` : " (self)";
655
+ const coach = a.icu_coach ? " · coach" : "";
656
+ const email = a.email ? ` · ${a.email}` : "";
657
+ return `- **${a.name ?? "Unnamed"}** — id: ${a.id}${perm}${coach}${email}`;
658
+ });
659
+ return { content: [{ type: "text", text: lines.join("\n") }] };
660
+ }
661
+ case "get_coach_ticks": {
662
+ const config = getConfig(args);
663
+ const athlete = await getAthleteProfile(config);
664
+ const ticks = athlete.coach_ticks ?? [];
665
+ if (!ticks.length) {
666
+ return { content: [{ type: "text", text: "No coach-tick presets on this athlete's profile — coach_tick is a fixed 1-5 rating scale. Use set_activity_coach_tick to rate an activity." }] };
667
+ }
668
+ const lines = ticks.map((t) => `- [${t.id}] ${t.text ?? ""}`);
669
+ return { content: [{ type: "text", text: lines.join("\n") }] };
670
+ }
671
+ case "set_activity_coach_tick": {
672
+ const activityId = z.string().parse(args["activity_id"]);
673
+ const coachTickId = args["coach_tick_id"];
674
+ const config = getConfig(args);
675
+ await setActivityCoachTick(config, activityId, coachTickId ?? null);
676
+ return {
677
+ content: [{
678
+ type: "text",
679
+ text: coachTickId != null
680
+ ? `Coach tick ${coachTickId} set on activity ${activityId}.`
681
+ : `Coach tick cleared on activity ${activityId}.`,
682
+ }],
683
+ };
684
+ }
605
685
  case "get_athlete_profile": {
606
686
  const config = getConfig(args);
607
687
  const athlete = await getAthleteProfile(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intervals-icu-mcp-server",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "MCP server for Intervals.icu — 26 tools for activities, wellness, calendar events, workouts and more",
5
5
  "type": "module",
6
6
  "bin": {