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 +28 -0
- package/dist/client.js +17 -1
- package/dist/format.d.ts +2 -1
- package/dist/format.js +14 -6
- package/dist/index.js +56 -7
- package/package.json +2 -2
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 =
|
|
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
|
|
116
|
-
const
|
|
117
|
-
|
|
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
|
-
|
|
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
|
|
125
|
-
lines.push(` ${z.name}: ${
|
|
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()
|
|
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
|
|
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
|
|
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: "
|
|
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: "
|
|
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.
|
|
4
|
-
"description": "MCP server for Intervals.icu —
|
|
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"
|