intervals-icu-mcp-server 0.1.8 → 0.2.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/dist/client.d.ts CHANGED
@@ -343,6 +343,34 @@ export declare function getActivities(config: ClientConfig, params: {
343
343
  }): Promise<Activity[]>;
344
344
  export declare function getActivityDetails(config: ClientConfig, activityId: string): Promise<Activity>;
345
345
  export declare function getActivityIntervals(config: ClientConfig, activityId: string): Promise<z.infer<typeof IntervalSchema>>;
346
+ export declare const MessageSchema: z.ZodObject<{
347
+ id: z.ZodOptional<z.ZodNumber>;
348
+ athlete_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
349
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
350
+ created: z.ZodOptional<z.ZodNullable<z.ZodString>>;
351
+ type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
352
+ content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
353
+ attachment_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
354
+ }, "strip", z.ZodTypeAny, {
355
+ id?: number | undefined;
356
+ name?: string | null | undefined;
357
+ type?: string | null | undefined;
358
+ athlete_id?: string | null | undefined;
359
+ created?: string | null | undefined;
360
+ content?: string | null | undefined;
361
+ attachment_url?: string | null | undefined;
362
+ }, {
363
+ id?: number | undefined;
364
+ name?: string | null | undefined;
365
+ type?: string | null | undefined;
366
+ athlete_id?: string | null | undefined;
367
+ created?: string | null | undefined;
368
+ content?: string | null | undefined;
369
+ attachment_url?: string | null | undefined;
370
+ }>;
371
+ export type Message = z.infer<typeof MessageSchema>;
372
+ export declare function getActivityMessages(config: ClientConfig, activityId: string): Promise<Message[]>;
373
+ export declare function addActivityMessage(config: ClientConfig, activityId: string, content: string): Promise<void>;
346
374
  export declare function getWellness(config: ClientConfig, params: {
347
375
  startDate: string;
348
376
  endDate: string;
package/dist/client.js CHANGED
@@ -138,13 +138,29 @@ export async function getActivityIntervals(config, activityId) {
138
138
  const data = await request(`/activity/${activityId}/intervals`, config);
139
139
  return IntervalSchema.parse(data);
140
140
  }
141
+ export const MessageSchema = z.object({
142
+ id: z.number().optional(),
143
+ athlete_id: z.string().nullable().optional(),
144
+ name: z.string().nullable().optional(),
145
+ created: z.string().nullable().optional(),
146
+ type: z.string().nullable().optional(),
147
+ content: z.string().nullable().optional(),
148
+ attachment_url: z.string().nullable().optional(),
149
+ });
150
+ export async function getActivityMessages(config, activityId) {
151
+ const data = await request(`/activity/${activityId}/messages`, config);
152
+ return z.array(MessageSchema).parse(data);
153
+ }
154
+ export async function addActivityMessage(config, activityId, content) {
155
+ await mutate("POST", `/activity/${activityId}/messages`, config, { content });
156
+ }
141
157
  export async function getWellness(config, params) {
142
158
  const data = await request(`/athlete/${config.athleteId}/wellness`, config, { oldest: params.startDate, newest: params.endDate });
143
159
  if (Array.isArray(data)) {
144
160
  const result = {};
145
161
  for (const entry of data) {
146
162
  const parsed = WellnessSchema.parse(entry);
147
- const key = entry["date"] ?? parsed.id ?? String(Object.keys(result).length);
163
+ const key = parsed.id ?? String(Object.keys(result).length);
148
164
  result[key] = parsed;
149
165
  }
150
166
  return result;
package/dist/format.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { Activity, Wellness, Event, SportSettings } from "./client.js";
1
+ import type { Activity, Wellness, Event, SportSettings, Message } from "./client.js";
2
2
  export declare function formatActivity(a: Activity): string;
3
3
  export declare function formatWellness(date: string, w: Wellness): string;
4
4
  export declare function formatEvent(e: Event): string;
5
+ export declare function formatMessage(m: Message): string;
5
6
  export declare function formatSportZones(s: SportSettings): string;
6
7
  export declare function formatIntervalRow(interval: Record<string, unknown>, idx: number): string;
package/dist/format.js CHANGED
@@ -83,6 +83,10 @@ export function formatEvent(e) {
83
83
  .filter(Boolean)
84
84
  .join("\n");
85
85
  }
86
+ export function formatMessage(m) {
87
+ const when = m.created ? new Date(m.created).toLocaleString() : "Unknown time";
88
+ return `**${m.name ?? "Unknown"}** (${when}): ${m.content ?? ""}`;
89
+ }
86
90
  function zoneRanges(breakpoints, names) {
87
91
  const count = names?.length ?? (breakpoints?.length ?? 0) + 1;
88
92
  const ranges = [];
@@ -110,19 +114,23 @@ export function formatSportZones(s) {
110
114
  lines.push("");
111
115
  }
112
116
  if (s.hr_zones?.length) {
117
+ // Unlike power_zones (%FTP), hr_zones stores absolute bpm thresholds directly.
113
118
  lines.push(`HR Zones${s.lthr != null ? ` (LTHR: ${s.lthr}bpm)` : ""}:`);
114
119
  for (const z of zoneRanges(s.hr_zones, s.hr_zone_names)) {
115
- const abs = s.lthr != null ? ` (${Math.round((s.lthr * z.from) / 100)}-${z.to != null ? Math.round((s.lthr * z.to) / 100) + "bpm" : "bpm+"})` : "";
116
- const pct = z.to != null ? `${z.from}-${z.to}%` : `${z.from}%+`;
117
- lines.push(` ${z.name}: ${pct}${abs}`);
120
+ const pctFrom = s.lthr != null ? Math.round((z.from / s.lthr) * 100) : null;
121
+ const pctTo = s.lthr != null && z.to != null ? Math.round((z.to / s.lthr) * 100) : null;
122
+ const pct = pctFrom != null ? ` (${pctFrom}${pctTo != null ? "-" + pctTo : ""}% LTHR)` : "";
123
+ const bpm = z.to != null ? `${z.from}-${z.to} bpm` : `${z.from}+ bpm`;
124
+ lines.push(` ${z.name}: ${bpm}${pct}`);
118
125
  }
119
126
  lines.push("");
120
127
  }
121
128
  if (s.pace_zones?.length) {
122
- lines.push("Pace Zones (% of threshold pace):");
129
+ // Same as hr_zones — absolute thresholds, not percentages of threshold_pace.
130
+ lines.push(`Pace Zones${s.threshold_pace != null ? ` (Threshold: ${s.threshold_pace} m/s)` : ""}:`);
123
131
  for (const z of zoneRanges(s.pace_zones, s.pace_zone_names)) {
124
- const pct = z.to != null ? `${z.from}-${z.to}%` : `${z.from}%+`;
125
- lines.push(` ${z.name}: ${pct}`);
132
+ const range = z.to != null ? `${z.from}-${z.to} m/s` : `${z.from}+ m/s`;
133
+ lines.push(` ${z.name}: ${range}`);
126
134
  }
127
135
  }
128
136
  return lines.join("\n").trimEnd();
package/dist/index.js CHANGED
@@ -4,8 +4,8 @@ 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, } from "./client.js";
8
- import { formatActivity, formatWellness, formatEvent, formatIntervalRow, formatSportZones, } from "./format.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, getActivityMessages, addActivityMessage, } from "./client.js";
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 ?? "";
11
11
  const athleteId = args["athlete_id"] ??
@@ -17,18 +17,24 @@ 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
+ function toDateStr(d) {
21
+ const y = d.getFullYear();
22
+ const m = String(d.getMonth() + 1).padStart(2, "0");
23
+ const day = String(d.getDate()).padStart(2, "0");
24
+ return `${y}-${m}-${day}`;
25
+ }
20
26
  function today() {
21
- return new Date().toISOString().slice(0, 10);
27
+ return toDateStr(new Date());
22
28
  }
23
29
  function daysAgo(n) {
24
30
  const d = new Date();
25
31
  d.setDate(d.getDate() - n);
26
- return d.toISOString().slice(0, 10);
32
+ return toDateStr(d);
27
33
  }
28
34
  function daysFromNow(n) {
29
35
  const d = new Date();
30
36
  d.setDate(d.getDate() + n);
31
- return d.toISOString().slice(0, 10);
37
+ return toDateStr(d);
32
38
  }
33
39
  const TOOLS = [
34
40
  {
@@ -125,7 +131,7 @@ const TOOLS = [
125
131
  category: { type: "string", description: "Event category: WORKOUT, NOTE, RACE_A, RACE_B, RACE_C (race priority A/B/C — this is what makes it a race, there is no separate race flag), PLAN, HOLIDAY, SICK, INJURED, TARGET, and others" },
126
132
  type: { type: "string", description: "Activity type: Ride, Run, Swim, WeightTraining, etc." },
127
133
  distance: { type: "number", description: "Distance in meters (e.g. 100000 for 100km)" },
128
- sub_type: { type: "string", description: "Race category/sub-type (e.g. A, B, C)" },
134
+ sub_type: { type: "string", description: "One of NONE, COMMUTE, WARMUP, COOLDOWN, RACE. Not race priority — that's set via category (RACE_A/RACE_B/RACE_C)." },
129
135
  color: { type: "string", description: "Event color, e.g. '#1f77b4' (hex) or a named color like 'blue', 'green', 'sky'" },
130
136
  workout_doc: { type: "object", description: "Pre-computed Intervals.icu workout_doc (as returned by get_event_by_id/list_workouts on an existing structured workout) to copy verbatim onto this event. Hand-written partial docs won't render correctly — prefer setting `description` with the Workout Builder syntax and omitting this field so Intervals.icu computes it." },
131
137
  athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
@@ -148,7 +154,7 @@ const TOOLS = [
148
154
  category: { type: "string", description: "New category" },
149
155
  type: { type: "string", description: "New activity type" },
150
156
  distance: { type: "number", description: "Distance in meters (e.g. 100000 for 100km)" },
151
- sub_type: { type: "string", description: "Race category/sub-type (e.g. A, B, C)" },
157
+ sub_type: { type: "string", description: "One of NONE, COMMUTE, WARMUP, COOLDOWN, RACE. Not race priority — that's set via category (RACE_A/RACE_B/RACE_C)." },
152
158
  color: { type: "string", description: "Event color, e.g. '#1f77b4' (hex) or a named color like 'blue', 'green', 'sky'" },
153
159
  workout_doc: { type: "object", description: "Pre-computed Intervals.icu workout_doc to copy verbatim. Hand-written partial docs won't render correctly — prefer updating `description` with Workout Builder syntax instead." },
154
160
  athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
@@ -276,6 +282,33 @@ const TOOLS = [
276
282
  },
277
283
  },
278
284
  },
285
+ {
286
+ name: "get_activity_messages",
287
+ description: "Get the comments/notes left on a specific activity (the chat thread shown under an activity in Intervals.icu, e.g. coach feedback).",
288
+ inputSchema: {
289
+ type: "object",
290
+ required: ["activity_id"],
291
+ properties: {
292
+ activity_id: { type: "string", description: "The activity ID" },
293
+ athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
294
+ api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
295
+ },
296
+ },
297
+ },
298
+ {
299
+ name: "add_activity_message",
300
+ description: "Post a comment/note on a specific activity (adds to the activity's chat thread in Intervals.icu).",
301
+ inputSchema: {
302
+ type: "object",
303
+ required: ["activity_id", "content"],
304
+ properties: {
305
+ activity_id: { type: "string", description: "The activity ID" },
306
+ content: { type: "string", description: "Comment text to post" },
307
+ athlete_id: { type: "string", description: "Athlete ID (defaults to ATHLETE_ID env var)" },
308
+ api_key: { type: "string", description: "API key (defaults to API_KEY env var)" },
309
+ },
310
+ },
311
+ },
279
312
  {
280
313
  name: "list_workouts",
281
314
  description: "List all workouts in the athlete's workout library.",
@@ -618,6 +651,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
618
651
  const curves = await getActivityPowerCurves(config, activityId);
619
652
  return { content: [{ type: "text", text: `Power curves for activity ${activityId}:\n\n${JSON.stringify(curves, null, 2)}` }] };
620
653
  }
654
+ case "get_activity_messages": {
655
+ const activityId = z.string().parse(args["activity_id"]);
656
+ const config = getConfig(args);
657
+ const messages = await getActivityMessages(config, activityId);
658
+ if (!messages.length)
659
+ return { content: [{ type: "text", text: "No comments on this activity." }] };
660
+ const text = messages.map(formatMessage).join("\n");
661
+ return { content: [{ type: "text", text }] };
662
+ }
663
+ case "add_activity_message": {
664
+ const activityId = z.string().parse(args["activity_id"]);
665
+ const content = z.string().parse(args["content"]);
666
+ const config = getConfig(args);
667
+ await addActivityMessage(config, activityId, content);
668
+ return { content: [{ type: "text", text: `Comment added to activity ${activityId}.` }] };
669
+ }
621
670
  case "list_workouts": {
622
671
  const config = getConfig(args);
623
672
  const workouts = await listWorkouts(config);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "intervals-icu-mcp-server",
3
- "version": "0.1.8",
4
- "description": "MCP server for Intervals.icu — 20 tools for activities, wellness, calendar events, workouts and more",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for Intervals.icu — 24 tools for activities, wellness, calendar events, workouts and more",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "intervals-icu-mcp": "dist/index.js"