account_disconnect
ChatGPTRevokes the current AI-assistant connection for this account. DESTRUCTIVE: requires confirm=true, and you must confirm with the user before calling.
account_link_status
ChatGPTReturns current link status for this authenticated account.
activity_get_climbs
ChatGPTReturns ONLY the detected climbs for an activity — no per-minute streams. Each climb has length_m, elevation_gain_m, avg_grade_pct, max_grade_pct, category, duration_s, the ridden performance (avg_power_w, np_w, avg_hr_bpm, avg_cadence_rpm) and steepest[50|100|250|500|1000] (grade + the same performance per ramp), plus a small summary (NP). Prefer this over activity_get_compressed_streams for climb/ramp questions ("how were my climbs?") — it loads no stream rows. Same detection + 30-day cache; if not cached it queues a background fetch and returns data_source="not_cached" (retry in ~1 min). Climbs need intervals-sourced streams (data_source "cron"/"live").
activity_get_compressed_streams
ChatGPTReturns per-minute averages and NP for an activity: power_w, hr_bpm, speed_mps, cadence_rpm, altitude_m, distance_m, pace_s_per_km, temperature_c, torque_nm, smo2_pct, thb_g_dl, left_right_balance_pct, w_prime_balance_j, grade_pct, core_temperature_c, respiration_brpm, left_pedal_smoothness_pct, right_pedal_smoothness_pct, left_torque_effectiveness_pct, right_torque_effectiveness_pct, rmssd_ms, sdnn_ms, rr_mean_ms — whichever of these the activity actually recorded (a field is simply absent from a row when its sensor gave no value; summary.samples reports the non-null raw-sample count per SENSOR channel (0 = not recorded); the three per-minute HRV channels are counted separately in summary.derived_minutes as the number of minutes that carry a value). avg cadence_rpm and torque_nm are averaged over PEDALLING only — freewheeling 0-rpm samples are excluded, so the value is the real pedalling cadence/torque, not dragged down by coasting; power_w keeps its zeros (coasting is real 0 W). Because the zeros are dropped from those averages, each minute also carries pedalling_time_s (≈ seconds actually pedalled that minute — raw streams are 1 Hz) and coasting_pct (percent of the minute spent freewheeling), so a pause in a minute stays visible. Pedal-smoothness and torque-effectiveness (left/right, percent) reveal how force is distributed through the crank revolution — useful for steep low-cadence ramps where average torque_nm alone hides the split. rmssd_ms and sdnn_ms are real per-minute heart-rate variability computed from the raw beat-to-beat R-R intervals BEFORE averaging (the hrv stream carries an array of R-R intervals per sample; artifacts that jump >20% from the previous interval are dropped). rr_mean_ms is only the mean R-R interval (≈ 60000/hr_bpm, not variability). A minute is dropped from ALL three HRV fields when too few valid intervals survive filtering, or when rr_mean_ms disagrees with 60000/hr_bpm by more than 10% (merged/missed beats). sdnn_ms is additionally omitted in transition minutes (heart rate moving more than 10 bpm within the minute), where it would measure the trend rather than the variability; rmssd_ms is trend-immune and is always present when the minute is valid. During moderate/hard efforts HRV collapses toward zero (parasympathetic withdrawal); the signal is in the recovery — how fast rmssd_ms comes back in the rests between hard intervals is a useful fatigue marker across an interval session. Response includes summary (NP, per-channel sample counts), a "climbs" array (auto-detected ascents ≥500m length, ≥3% avg grade, score=length_m×avg_grade_pct≥1500; each has length_m, elevation_gain_m, avg_grade_pct, max_grade_pct, score, category Cat4/Cat3/Cat2/Cat1/HC/Cat5, duration_s (actual elapsed time over the climb from the stream, not an estimate), estimated_kj, expected_carbs_g, the ridden performance over the climb (avg_power_w, np_w, avg_hr_bpm, avg_cadence_rpm; null when that channel was not recorded, cadence excludes freewheeling zeros), cadence_penalty, heat_risk, climb_difficulty [terrain only], athlete_difficulty [terrain + this ride's fatigue/heat/gearing], difficulty_components breakdown, and steepest — the steepest sliding window of each length in metres: steepest.{50,100,250,500,1000} each = {grade_pct, start_distance_m, elevation_gain_m, plus the same avg_power_w/np_w/avg_hr_bpm/avg_cadence_rpm over that ramp}, windows longer than the climb omitted, steepest.smoothing_m = the altitude pre-smoothing distance in metres. Prefer steepest over avg_grade_pct/max_grade_pct when the question is about short steep ramps or whether a gear ratio is enough: avg_grade_pct averages ramps away and max_grade_pct says nothing about how long the steep part lasts, whereas steepest gives grade-by-length), and per_minute rows (each tagged with its minute offset into the activity; use from_minute/to_minute to fetch just one segment at full resolution). Climbs are only detected for activities whose stream data came from intervals.icu directly (data_source "cron"/"live"), not from app uploads. For extremely long a…
activity_get_descents
ChatGPTReturns detected DESCENTS for an activity — the downhill counterpart to activity_get_climbs, deliberately NOT its mirror image. The question on a descent is not "how hard" but "how well did the body recover, and did the rider actually rest or keep working". Each descent has start_distance_m, end_distance_m, length_m, elevation_loss_m, avg_grade_pct, max_grade_pct (both negative — a steeper descent is more negative), duration_s, avg_speed_kmh, max_speed_kmh, speed_cv (coefficient of variation of speed, computed from the raw 1 Hz stream — a proxy for braking/technical terrain; higher = more speed variation = more technical or more hesitant), hr_start_bpm/hr_end_bpm (mean of the first/last 30 s), hr_min_bpm, hr_drop_bpm (hr_start - hr_end — the CORE metric: a clean drop means the body is recovering; a flat or rising HR despite near-zero effort is an early-warning sign — heat, dehydration, fatigue still working, worth surfacing to the rider), hr_recovery_rate_bpm_per_min, kj_spent (mechanical work done DURING the descent — should be near zero on a cleanly-coasted descent; a high value means the rider pedalled instead of resting, which shows up later at the next climb), avg_power_w (informational only, not a performance measure), coasting_pct (percent of time freewheeling — the single most informative number on a descent), pedalling_time_s, and temperature_start_c/temperature_end_c/temperature_delta_c. Detection: candidate descents come from the activity's raw distance/altitude stream (peak-to-valley), length >= 1000 m, elevation_loss_m >= 50 m, avg_grade_pct <= -2, avg_speed_kmh >= 15 (excludes rest stops, which would otherwise look like a descent since altitude alone does not reverse at a stop — a sustained near-zero-speed stretch is instead treated as a hard boundary and is NEVER inside a reported descent). A short counter-climb within an otherwise-descending stretch does not split it, as long as the net elevation trend stays negative. Unlike activity_get_climbs (computed once at cache-build time and embedded in the per-minute object, so a detection-logic change only reaches an already-cached activity after a refetch or the 30-day TTL), this recomputes from the athlete's raw 1 Hz stream on every call — force_refresh recomputes from that same locally-stored raw stream (never a live intervals.icu fetch, no rate-limit exposure). If the raw stream is not yet available locally, this queues a background job and returns data_source="not_cached" (retry in ~1 min) — same as activity_get_compressed_streams.
activity_get_intervals
ChatGPTReturns the lap/interval breakdown of a completed activity from intervals.icu (?intervals=true) — the per-block summary that per-minute streams do not give you directly. Each interval: label, type (WORK/RECOVERY/…), start_s, duration_s, avg_w, np_w (normalized power for the block), max_w, avg_hr, max_hr, avg_cadence_rpm, avg_torque_nm, intensity_pct, w_prime_depletion_j (W' used across the block), zone, decoupling_pct, distance_m (fields absent when not recorded). Use this for questions like "how did HR drift block-to-block at the same power?" — it gives the athlete's real interval averages instead of reconstructing them from per-minute rows (which mixes in the ramp-up). Cached 30 min; force_refresh=true bypasses.
activity_set_feedback
ChatGPTWrites SUBJECTIVE ratings onto a completed activity in intervals.icu: feel (1-5), rpe (1-10 rate of perceived exertion), tags (string[]). MERGE, not replace: passing only rpe leaves an existing feel untouched. This is the only write that can overwrite something on an activity, so it deliberately merges. Response echoes updated:{} and previous:{}. Pass a field as null to clear it. (To DELETE an activity: not possible — activities are recorded measurements and are never deletable via this MCP.)
athlete_get_power_curve
ChatGPTReturns the athlete's best-power-vs-duration curve(s) from intervals.icu — the maximum mean power sustained for each duration over a time window. Answers "what is my 5-min / 20-min / 1-hour best power" and "what does the model think my FTP/CP/W' are" in one call (instead of triangulating from individual activities). Each requested curve carries best_watts_at_s at key durations (5s,15s,30s,1m,2m,5m,8m,10m,20m,30m,60m → {at_s, w, wkg}) plus power_models (CP/W'/Pmax/FTP estimates from MS_2P/MORTON_3P/FFT_CURVES/ECP), weight_kg, vo2max_5m and the window dates. curves selects the windows: comma-separated from 42d, 1y, 2y, s0 (current season), s1 (previous), all, or a custom range r.YYYY-MM-DD.YYYY-MM-DD (default "42d,1y,all"). sport_type defaults to "Ride". Cached 1 hour; force_refresh=true bypasses.
athlete_get_profile
ChatGPTReturns a small, curated athlete profile: display_name, sex, birth_year, height_cm, weight_kg, timezone, locale, measurement_system, sports[]. Intentionally omits secrets (API keys, email, tokens), integration/notification settings and internal intervals.icu fields. For per-sport FTP/zones/LTHR use athlete_get_sport_settings. Cached up to 1 hour; response includes fetched_at_utc and data_source ("live"|"cache"); force_refresh=true bypasses the cache.
athlete_get_sport_settings
ChatGPTReturns per-sport settings from intervals.icu: FTP, power zones, sweet-spot range, LTHR, max HR, HR zones, and which activity types each configuration applies to (e.g. "Ride", "Run"). Needed to interpret raw numbers from activity_get_compressed_streams or wellness_get_range (e.g. whether 250W or a given HR was easy or hard for this athlete). Shares the same 1-hour cache as athlete_get_profile (both read intervals.icu's athlete endpoint). Response includes fetched_at_utc and data_source ("live" or "cache"). Use force_refresh=true to bypass the cache.
nutrition_get_day
ChatGPTReads stored nutrition for one day (YYYY-MM-DD) from PaceKeeper MCP storage.
nutrition_get_meal
ChatGPTReads one meal/recipe for a day and meal type (e.g. breakfast, lunch, dinner).
nutrition_get_preferences
ChatGPTReturns the athlete's nutrition constraints. ALWAYS call this before generating any meals and stay within it. Fields: diet_type + diet_rule (vegan/vegetarian/pescetarian/"" ); meat_per_week / fish_per_week (max meals of that diet per week); allergies[] + intolerances[] (HARD — never include these ingredients); dislikes[] (avoid where possible); preferences (free text to honour); cuisine_style, preparation_style[], nutrition_goal, nutrition_focus[]; meals[] (the athlete's configured meal slots: each {type, time} — use these EXACT slot types for each meal's "t" and honour the slot time); upcoming_training[] (planned sessions for the next ~8 days: {date, start, end, duration_min, name, type, load} in the athlete's local time — use start/end to time pre/post-workout meals relative to the slot times, e.g. a carb-rich meal before an early session, protein after; each session also carries meal_flexibility {flexible, displaced_slots[], guidance} — when flexible=true its window displaces those slots, so THAT day you may set meal times freely (no max_deviation limit) and omit the displaced meals per the guidance, keeping at least one meal); work_schedule {shift_work, work_days (ISO weekday ints 1=Mon…7=Sun), work_from, work_to (HH:MM local)} — on a work day, any meal that falls within work_from…work_to should be portable / meal-prep-friendly / pre-cookable (no fresh on-the-spot cooking); schedule heavier cooking outside work hours; if shift_work=true the hours vary day to day, so keep meals flexible and meal-prep-friendly. fuel_products[] (the athlete's OWN fuelling products: {id, kind, name, url?, size?, per-unit carbs_g/fluid_ml/sodium_mg/kcal?, macros_known, and for containers is_container=true + capacity_ml}) — THREE kinds: CONTAINERS (is_container=true, kind bottle/flask — a capacity you fill with a mix, no own macros), SELF-MIXED (have a components[] array — built from ingredients, totals summed), and BOUGHT products (ready-to-use, macros set or resolvable from url). When planning a fuelling entry's items[], PREFER these over inventing new products and set the item's product_id to the matching id; if a product has macros_known=false, look its macros up from its url, fill carbs_g/fluid_ml/sodium_mg (and kcal) on the item, and the server persists them back onto the product. CONTAINERS (kind bottle/flask, is_container=true) have NO macros of their own — they hold capacity_ml; to use one, fill it with a drink_mix/self-mixed product and compute the item's carbs_g/fluid_ml from capacity_ml + that mix (a bottle item = the filled bottle: fluid_ml≈capacity_ml, carbs_g = the mix amount for that volume). SELF-MIXED products carry components[] (each {name, url?, amount?, per-component carbs_g/fluid_ml/sodium_mg/kcal?}) — if the product's macros are unknown or you need to estimate the mix, resolve EACH component's macros from ITS component url (+ its amount), SUM them for the product total, and put that total on the item; the product has no central url. The server enforces diet_rule and meat/fish limits and rejects meals containing an allergy/intolerance term, so respecting this avoids rejected writes.
nutrition_get_week
ChatGPTReads stored nutrition for one week, starting at week_start_local_date, default 7 days.
nutrition_patch_day
ChatGPTChanges day-level fields without rewriting the day's meals — currently the day note. Use this to fix a stale day note (e.g. after you shortened a session or changed a meal, so the note's "Sweet Spot 18:00–20:08" / "~4471 kcal" no longer matches) without re-sending every meal. Patchable: note (free text; null or "" clears it). targets are DERIVED from the day's meals and training load and are NOT settable here. Returns patched:{} and previous:{}.
nutrition_patch_meal
ChatGPTChanges ONLY the given fields of an EXISTING meal without re-sending its ingredients or preparation — a safe partial update (merge). Use this instead of nutrition_set_meal when you only need to move the meal time, rename it, correct its kcal/macros, tags, or its pre/post-session link. Patchable: time (HH:MM), name, k (kcal number), m ({f,c,p} grams), tags (string[]), ref (a training event id it belongs to), rel ("pre"|"post"). ing and prep are NOT patchable — to change ingredients or steps re-send the whole meal with nutrition_set_meal. The merged meal is re-validated (schema, delivery rules, diet/allergy preferences, meal-time policy) exactly like nutrition_set_meal, so an invalid patch is rejected. Returns patched:{} and previous:{}. The meal must already exist on that day.
nutrition_set_day
ChatGPTWrites one full nutrition day (day_nutrition schema). The input schema below documents every field. The meals[] array holds two ENTRY KINDS. (1) A normal MEAL (kind omitted or "meal") requires: t, name, tags, diet, srv, p, k, m, ing, prep — a point in time with a portion. Optionally add rel ("pre"|"post") + ref (a training event id from nutrition_get_preferences.upcoming_training[].id) to mark it as the pre-session or recovery meal. (2) A FUELLING entry (kind "fuelling") — on-the-move intake DURING a session (a window + per-hour rate, not a meal) — is better written with the dedicated nutrition_set_fuelling tool; see that tool for the fuelling schema (rate/window/ref/items). A day can hold BOTH a fuelling and normal meals. Put the per-hour numbers into rate, never as prose in prep. diet must be one of meat|fish|vegetarian|vegan (use meat/fish for meat/fish dishes, never mislabel as vegetarian). For every ingredient the amount goes into q (number, >0) + u ("g"|"ml"|"piece"), or u="pinch" with NO q for a pinch (eine Prise; dry spices/salt/pepper only, never converted to grams) and n is the NAME ONLY (e.g. "Skyr (natur)", never "200 g Skyr"); every meal MUST include prep (array of at least 3 concrete steps); macros m (f/c/p in grams) must be realistic, never 0/0/0. Payloads violating these are rejected with a specific error message — fix and resend. Call nutrition_get_preferences first and stay within the athlete's diet form, allergies/intolerances and weekly meat/fish limits — the server enforces them. Dates must be TODAY or later (see server_date from nutrition_get_preferences); past dates are rejected. EDIT IN PLACE — do NOT clear and recreate: to change an existing day, re-send it with the SAME meal slots (t) as the athlete's configured meals; each slot overwrites its meal and keeps that meal's revision history. By DEFAULT omitting a slot KEEPS it (safe partial update) — set delete_missing_meals=true to instead delete every slot not in this payload. To change just ONE meal, use nutrition_set_meal instead of rewriting the whole day.
nutrition_set_fuel_product
ChatGPTWrites/updates ONE entry in the athlete's fuel-product library (nutrition_get_preferences.fuel_products[]). Use this to TEACH the library a product's macros once you have resolved them — especially a drink_mix or self-mix whose macros_known was false: the implicit day write-back only reaches a bought single-unit product via an item's product_id and never overwrites, so the composition of a mix could not be persisted. Give kind + name (required). To UPDATE an existing product pass its id; omit id to create one (or to match+update by kind+name). Put the resolved macros for one reference unit into carbs_g/fluid_ml/sodium_mg/kcal and note that unit in size (e.g. "60 g serving" or "1014 g batch"). For a CONTAINER (bottle/flask) set capacity_ml and leave the macros empty — containers have no intrinsic macros. For a SELF-MIX also pass components[] (each name + amount + its resolved macros) so the composition is remembered; the product-level macros should be the sum over the reference size. macros_known flips to true automatically once any macro is stored. This only edits the product library — it does not change any day; the next fuelling can then reuse the product by product_id without re-deriving it.
nutrition_set_fuelling
ChatGPTWrites/replaces the on-the-move FUELLING (Verpflegung) for a day — intake DURING a session, NOT a meal (no ingredients, no prep). Use THIS for fuelling instead of nutrition_set_day/set_meal. Give a per-hour rate {carbs_g_per_h, optional fluid_ml_per_h, sodium_mg_per_h} and EITHER a window {start,end} OR a ref (a training event id from nutrition_get_preferences.upcoming_training[].id — the window then follows that session). The server derives kcal + macros + fluid/sodium totals from rate × window duration. ALSO give items[] — the CONCRETE carry objects the athlete takes and consumes (bottles with mix, flasks, gels, bars, chews, salt caps, water, real food): each {kind, optional name, count, product_id?, per-unit carbs_g/fluid_ml/sodium_mg/kcal}; together they should roughly meet the rate. PREFER the athlete's own fuel_products[] (set product_id); if a product has macros_known=false resolve its macros from its url and put them on the item (the server persists them back). CONTAINERS (bottle/flask, is_container) have no macros — fill them with a drink_mix and compute the amount from capacity_ml. The fuelling is stored inside the day's meals with kind="fuelling" so it counts toward the day total; it is exempt from slot times, diet form and the weekly meat/fish count. Re-writing with the same ref (or day, if no ref) replaces the existing fuelling for that session in place. Date must be TODAY or later.
nutrition_set_meal
ChatGPTWrites/replaces one meal (recipe) at a given day + meal type. The recipe input schema below documents every field. Required recipe fields: name, tags, diet, srv, p, k, m, ing, prep (t is set from the meal_type argument). diet must be one of meat|fish|vegetarian|vegan (use meat/fish for meat/fish dishes). Each ingredient's amount goes into q (number, >0) + u ("g"|"ml"|"piece"), or u="pinch" with NO q for a pinch (eine Prise; dry spices/salt/pepper only, never converted to grams); n is the NAME ONLY (never "200 g Skyr"); the meal MUST include prep (array of at least 3 concrete steps); macros m (f/c/p grams) must be realistic. Violations are rejected with a specific error — fix and resend. Call nutrition_get_preferences first and respect the athlete's diet form and allergies/intolerances — the server rejects meals that break them. The date must be TODAY or later (see server_date from nutrition_get_preferences); past dates are rejected. Prefer this over nutrition_set_day when changing a SINGLE meal: it overwrites exactly that one slot in place and leaves the rest of the day — and that meal's revision history — intact.
nutrition_set_week
ChatGPTWrites a week nutrition payload as validated day objects. days_payload is an array of day objects (typically 7). Each day object uses the key "d" (YYYY-MM-DD) plus "meals" — do NOT add a "date" key. Same RULES per meal as nutrition_set_day: each meal requires t, name, tags, diet (meat|fish|vegetarian|vegan), srv, p, k, m, ing, prep; ingredient amounts in q (number, >0) + u ("g"|"ml"|"piece"), or u="pinch" with NO q for a pinch (eine Prise; dry spices/salt/pepper only, never converted to grams) with n the NAME ONLY (never "200 g Skyr"); every meal MUST include prep (array of at least 3 concrete steps) and realistic macros m (f/c/p grams). Any day violating these is rejected with a specific error — fix and resend. Call nutrition_get_preferences first: the whole week is checked against the athlete's diet form, allergies/intolerances and weekly meat/fish limits before anything is stored. Every day's d must be TODAY or later (see server_date from nutrition_get_preferences); a past date rejects the whole write. EDIT IN PLACE — do NOT clear and recreate: keep the SAME meal slots (t) across re-writes so each meal keeps its revision history; to change a single meal, use nutrition_set_meal instead.
pacekeeper_get_context
ChatGPTSTART HERE. A small (<5 KB) orientation for this MCP: version, server date + athlete timezone, where the AUTHORITATIVE data lives (climb/ramp performance, sport intensity, nutrition constraints), units, cache + write rules, recommended tool flows, supported sports, and what is app-only. Read it once at the start of a session; it prevents recomputing values that already exist and avoids app-only dead ends. Tool list built 2026.08.06.1421 — if this differs from the mcp_version this tool returns live, your cached tool list is STALE: reconnect the connector to refresh it.
pacekeeper_ping
ChatGPTDiagnostic ping (build 2026.08.06.1421). Returns the live server build (mcp_version + server_build_utc). Use it to test tool-list propagation: its very PRESENCE in your tool list means the client refreshed to a recent version, and the returned mcp_version tells you the live build. If this tool is missing, or the build baked in this description differs from the returned mcp_version, the cached tool list is stale — reconnect.
plan_create
ChatGPTCreates a new training plan with periodization phases. The server validates before writing: phases MUST be contiguous (no gaps, no overlaps), cover the whole plan window (first phase starts on plan_start_date, last ends on plan_end_date), and use only the allowed phase types. Recommended phase order/min length: base_1 (>=21d) -> base_2 (>=21d) -> build_1/build_2 (>=21d) -> peak (>=10d) -> race_week (7d) -> recovery (>=7d); splice b_prep/b_recovery or c_prep/c_recovery around a B/C race instead of leaving a gap. main_goal is the A race. Add B/C races and non-race periods (vacation/illness/injury/training_camp/work_travel/rest_block/other) via events; "other" requires a description.
plan_get
ChatGPTReads the athlete's training plan(s): plan window, main goal (A race), periodization phases and manual events. Pass plan_uid to read one plan, or omit it to list all plans. Use this before plan_update to see the current phases/events.
plan_update
ChatGPTReplaces the plan definition (title, window, main goal, phases, events) of an EXISTING plan identified by plan_uid. Sessions/workouts and nutrition already attached to the plan are preserved. The same phase validation as plan_create applies (contiguous, no gaps, full window coverage, valid types). Read the plan first with plan_get. CONFIRMATION: if the new phases move FUTURE trainings into a different phase family, the first call returns applied=false, requires_confirmation=true and the list of trainings that would be removed WITHOUT changing anything — present that list to the user, and only after they confirm, call plan_update again with the same arguments plus confirm_prune=true. Past trainings are never removed.
recipe_update_single
ChatGPTUpdates exactly one recipe draft in PaceKeeper MCP storage (not intervals.icu). The server validates the recipe object against the existing day_nutrition meal schema before storing anything.
training_create_single
ChatGPTCreates one planned training. It is written into a PLAN as a session and only uploaded to intervals.icu if that plan is ACTIVE — for a draft/inactive plan it is stored in the plan and uploaded later when the athlete activates it. Pass plan_uid to target a specific plan; if you omit it the server AUTO-DETECTS the plan whose window covers the training date and routes into it (so a training in a plan window is always gated by that plan's active state and never uploaded past the guard). Only a date that lies in NO plan becomes a standalone intervals.icu calendar event. Re-calling with the same date+name+sport updates that session in place. The server validates the training object against day_training_schema_hf. REQUIRED training fields: d (YYYY-MM-DD), n (name), c=WORKOUT, sp (sport), tp (training type), st (HH:MM), desc, w (at least one work block). Workout structure in w: each item is either a step block (l/du/ta/ca, optional in=intensity) or a repeat block (l/n/steps), with steps as the repeated pattern only once (no nested repeats). Duration du accepts combined units, e.g. "1h40m" = 100 min. Set the optional in (warmup/interval/recovery/active/cooldown/rest) on interval work/rest steps so they are classified correctly; if omitted the server derives it from ta. Target examples for ta: "65%-75%", "Z4", "ramp 50%-75%", "4:45/km", "150-160 bpm", "Z2 HR", "90% LTHR", "70% HR", "Z2 Pace", "70% Pace". "Z4" alone is always a POWER zone; for heart rate use "Z2 HR"/"Z2-Z3 HR" (zone), "90% LTHR"/"90-95% LTHR" (percent of threshold HR — most common), or "70% HR"/"75-80% HR" (percent of max HR — a different basis than LTHR, not interchangeable). Absolute bpm ("150-160 bpm", "155 bpm") is converted to %LTHR server-side and requires LTHR to be configured for the sport in intervals.icu — if it isn't, the request is rejected with a clear error naming the target; prefer "% LTHR" directly when possible. Pace also accepts /100m, /100y, /mi, /500m, /400m, /250m instead of /km (e.g. "1:30/100m" for swimming — there is no power fallback for swim, pace is the only realistic target there). Cadence example for ca: "85-95 rpm".
training_delete_single
ChatGPTDeletes a PLANNED calendar event / plan session from intervals.icu (e.g. a workout that was scheduled but did not happen and still sits in the calendar). HARD INVARIANT: an activity is a recorded measurement and is NEVER deletable via this MCP — passing an activity id (e.g. "i170860714") is rejected with no fallback. Requires confirm=true. The id must be a numeric calendar event id; the server verifies it is actually an event before deleting.
training_get_range
ChatGPTReturns planned calendar events and completed activities for a date range (max 21 days). "planned_events" contains workouts, races, and notes scheduled in the calendar from intervals.icu/events. "activities" contains completed activities, each with a "streams_cached" boolean: true means activity_get_compressed_streams can return per-minute data for it right away, false means calling it will only queue a background fetch (retry in ~1 minute). Results are cached for 15 minutes. Response includes fetched_at_utc and data_source ("live" or "cache"). Use force_refresh=true to bypass the cache when the user says data is missing or outdated. By default (resolve=true) each planned workout step whose power/hr/pace target is a PERCENTAGE (units "%ftp"/"%lthr"/"%hr"/"%pace") is enriched with a "resolved" object of ABSOLUTE values, computed server-side from the athlete's per-sport settings: power → {low_w, high_w, ftp_used, source} where source is "indoor_ftp" for indoor events else "ftp"; hr → {low_bpm, high_bpm, lthr_used|max_hr_used, source}; pace → {low_mps, high_mps, threshold_pace_used, source}. Each resolved event also carries a top-level "targets_resolved_basis" ({ftp_used, ftp_source, lthr, max_hr, indoor}) so it is visible which FTP was applied. Percentages alone are ambiguous (indoor vs outdoor FTP differ) — prefer the resolved absolute values. Set resolve=false to skip this and return only the raw percentages. For completed activities: compliance is set to null when there is no paired planned workout (a 0 there means "unknown", not "0% completed"); and to judge aerobic decoupling/cardiac drift prefer icu_power_hr_z2 (together with icu_power_hr_z2_mins) over decoupling — decoupling is returned for every activity even when it was not a steady aerobic effort (intervals, recovery spins), where it is meaningless.
training_patch_single
ChatGPTChanges ONLY the given scalar fields of a planned calendar event, WITHOUT re-sending the whole workout — a safe partial update (merge). Use this instead of training_update_single when you only need to move the start time, rename, re-date, change the load target, description or sport: rewriting the full object risks silently dropping a forgotten field (e.g. a repeat block in w). Patchable: st (HH:MM start time), d (YYYY-MM-DD date), n (name), ts (load target, number), desc (description text), sp (sport, e.g. "Ride"/"Run"). The workout structure w is NOT patchable — it is all-or-nothing; to change the steps use training_update_single. Returns patched:{} (the fields you sent). The stored event is NOT echoed back — use training_get_range to read it. Omitted fields are left unchanged.
training_update_single
ChatGPTUpdates exactly one planned training event in intervals.icu. The server validates the training object against day_training_schema_hf before writing anything. REQUIRED training fields: d (YYYY-MM-DD), n (name), c=WORKOUT, sp (sport), tp (training type), st (HH:MM), desc, w (at least one work block). Workout structure in w follows the OpenAI training prompt rules: each item is either a step block (l/du/ta/ca, optional in=intensity) or a repeat block (l/n/steps), with steps as the repeated pattern only once (no nested repeats). Duration du accepts combined units, e.g. "1h40m" = 100 min. Set the optional in (warmup/interval/recovery/active/cooldown/rest) on interval work/rest steps so they are classified correctly; if omitted the server derives it from ta. Target examples for ta: "65%-75%", "Z4", "ramp 50%-75%", "4:45/km", "150-160 bpm", "Z2 HR", "90% LTHR", "70% HR", "Z2 Pace", "70% Pace". "Z4" alone is always a POWER zone; for heart rate use "Z2 HR"/"Z2-Z3 HR" (zone), "90% LTHR"/"90-95% LTHR" (percent of threshold HR — most common), or "70% HR"/"75-80% HR" (percent of max HR — a different basis than LTHR, not interchangeable). Absolute bpm ("150-160 bpm", "155 bpm") is converted to %LTHR server-side and requires LTHR to be configured for the sport in intervals.icu — if it isn't, the request is rejected with a clear error naming the target; prefer "% LTHR" directly when possible. Pace also accepts /100m, /100y, /mi, /500m, /400m, /250m instead of /km (e.g. "1:30/100m" for swimming — there is no power fallback for swim, pace is the only realistic target there). Cadence example for ca: "85-95 rpm".
wellness_get_range
ChatGPTReturns daily wellness data (HRV, resting HR, sleep, CTL/ATL/TSB, ramp rate, weight) for a date range (max 90 days) from intervals.icu. Results are cached for 15 minutes. Response includes fetched_at_utc and data_source ("live" or "cache"). Use force_refresh=true to bypass the cache when the user says data is missing or outdated.
wellness_set_day
ChatGPTWrites the athlete's SUBJECTIVE self-report for one day to intervals.icu wellness. MERGE, not replace: only the fields you pass are changed; the rest of the day (incl. other subjective fields) stay intact. Settable fields: soreness, fatigue, stress, mood, motivation, injury (each an integer 1-5, or null to clear), and comments (free text). MEASURED fields (restingHR, hrv, sleepSecs, weight, ctl, atl, …) come from devices/model and are REJECTED with an error if included. Use this to log things like rising soreness or a niggling injury so they become a data series instead of only chat history. Response echoes updated:{} and previous:{} (the prior values of all subjective fields). Date is any day (today or past).