PaceKeeper AI
Training & recovery insights
- Category
- Health & Wellness
- Primary Subcategory
- Endurance Sport Training Planners
Integration details
Description
PaceKeeper helps athletes review their profile and sport settings, inspect planned and completed training, understand wellness and recovery trends, and analyze compressed activity streams from their connected intervals.icu account.
- Integration type
- Plugin
- Verification status
- Not applicable
- Platform
- ChatGPT
- Primary Subcategory
- Endurance Sport Training Planners
- Secondary Subcategories
- None listed
- Brand
- PaceKeeper AI
- Access
- Account required
- First tracked
- 2026-08-23
- Tool count
- 37
- Geography
- US
The Primary Subcategory used for this profile’s headline score.
Other Subcategories where the Integration is listed.
ChatGPT Plugin Discovery Score
ChatGPT Plugin discovery is coming soon
ChatGPT can surface a Plugin when it matches a user's request.Your Plugin Discovery Score measures how often yours appears.
No spam. Unsubscribe any time.
What discovery looks like

Competing in ChatGPT Endurance Sport Training Planners
View Category37 tools agents can invoke
Appends a note to an activity's comment log. PaceKeeper stores the structured comment (source of truth) AND writes a flat, human-readable "── PaceKeeper ──" block back into the intervals.icu activity description (one-way; that text is never read back). APPEND-only. Give comment.text (required); optional kind ("during"|"post"|"retrospective"|"note") and elapsed_s — the seconds INTO the activity the note refers to (e.g. 18000 = the 5-hour mark), which lines the note up with the minute rows from activity_get_compressed_streams so it can be read against power/cadence/torque; and optional at (ISO-8601 write time, default now). Returns the created comment incl. its comment_id.
activity_add_comment
Writes 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).
wellness_set_day
Changes 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_patch_meal
Changes 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_patch_single
Changes 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_day
Returns current link status for this authenticated account.
account_link_status
Creates 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_create_single
Creates 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_create
Deletes 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_delete_single
Revokes the current AI-assistant connection for this account. DESTRUCTIVE: requires confirm=true, and you must confirm with the user before calling.
account_disconnect
Corrects or soft-deletes ONE comment in an activity's log (never a hard delete — a corrected/removed comment keeps a deleted_at marker). Give text to replace the wording and/or delete=true to soft-delete. The intervals.icu "── PaceKeeper ──" block is regenerated afterwards. Get comment_id from activity_get_comments.
activity_edit_comment
START 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_get_context
Returns the activity's comment log (chronological), each with comment_id, text, kind, elapsed_s, at. Read the log before editing or to answer questions about earlier notes. Soft-deleted comments are omitted unless include_deleted=true.
activity_get_comments
Returns 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 activities (>1440 min / 24h) data is automatically downsampled; up to that length every minute is available. Check "streams_cached" on the activity in training_get_range first: true returns data immediately, false queues a background fetch and returns data_source="not_cached" — retry this call in about a minute. Streams are cached 30 days; once fetched via the PaceKeeper app they are available immediately. Pass force_refresh=true to rebuild an already-cached activity with the latest processing (the cache holds the previously compressed result, so newer per-minute fields appear only after a rebuild) — it queues a fresh raw fetch + recompress and returns data_source="refreshing"; retry in ~1 min.
activity_get_compressed_streams
Returns 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_get_intervals
Returns 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_profile
Returns 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_climbs
Returns 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_descents
Reads stored nutrition for one day (YYYY-MM-DD) from PaceKeeper MCP storage.
nutrition_get_day
Returns 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_preferences
Reads stored nutrition for one week, starting at week_start_local_date, default 7 days.
nutrition_get_week
Reads one meal/recipe for a day and meal type (e.g. breakfast, lunch, dinner).
nutrition_get_meal
Returns 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_get_range
Returns 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_power_curve
Returns 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.
athlete_get_sport_settings
Reads 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_get
Returns 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_get_range
Diagnostic 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.
pacekeeper_ping
Writes 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.)
activity_set_feedback
Writes 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.
nutrition_set_week
Writes/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_fuelling
Writes/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_meal
Updates 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.
recipe_update_single
Updates 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".
training_update_single
Replaces 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.
plan_update
Writes/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_fuel_product
Writes 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_day
How do I improve a ChatGPT Plugin's discoverability?
The levers are the listing surface agents actually read: names, descriptions, keywords, tool metadata, and registry health. Which lever matters depends on where discovery breaks, which is what continuous measurement shows.
What are PaceKeeper AI alternatives on ChatGPT?
As of 2026-09-12, PaceKeeper AI competes with AI Endurance, COROS, Eixo Run, Endurance Planner, Fitness AI Connector, Flow State, Freediver, Joules, Leo - Running Coach, Pelaris, Propusher, rit.run, Tredict in ChatGPT Endurance Sport Training Planners, ranked by public Discoverability Score.
Where is this profile measured?
This profile uses the geography attached to the latest public registry snapshot: US. Locale tags are intentionally omitted.