progression_trend
Tracks estimated one-rep max progression for a single exercise, returning session-by-session data for charting strength gains over time.
Instructions
Top-set e1RM over time for a single exercise. Returns a per-session series suitable for charting.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| exercise_template_id | Yes | ||
| since | No | ||
| max_pages | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/hevy_mcp/tools/analytics.py:120-158 (handler)The `progression_trend` async function is the core handler. It takes an `exercise_template_id`, optional `since`, and `max_pages`, collects sets for that exercise, computes e1RM per session via Epley formula, returns a sorted series of (date, e1rm_kg) plus a linear slope in kg/week.
async def progression_trend( exercise_template_id: str, since: str | None = None, max_pages: int = MAX_PAGES_DEFAULT, ) -> dict[str, Any]: """Top-set e1RM over time for a single exercise. Returns a per-session series suitable for charting. """ since_dt = _parse_dt(since) sets = await _collect_sets_for_exercise(client, exercise_template_id, max_pages) # bucket by date, take best e1RM per session per_day: dict[str, float] = {} for s, when in sets: w, r = s.get("weight_kg"), s.get("reps") if w is None or r is None or r <= 0 or r > 15 or s.get("type") == "warmup": continue dt = _parse_dt(when) if since_dt and dt and dt < since_dt: continue if dt is None: continue day = dt.date().isoformat() e1rm = _epley(w, r) if e1rm > per_day.get(day, 0): per_day[day] = e1rm series = [{"date": d, "e1rm_kg": round(v, 1)} for d, v in sorted(per_day.items())] if len(series) >= 2: slope_per_week = _slope_per_week(series) else: slope_per_week = None return { "series": series, "slope_kg_per_week": slope_per_week, "text": ( f"{len(series)} sessions; " f"slope ~{slope_per_week:+.2f}kg/week" if slope_per_week is not None else f"{len(series)} sessions" ), } - Input schema defined via function signature: `exercise_template_id: str` (required), `since: str | None` (optional ISO-8601), `max_pages: int` (default 30).
async def progression_trend( exercise_template_id: str, since: str | None = None, max_pages: int = MAX_PAGES_DEFAULT, ) -> dict[str, Any]: - The `_slope_per_week` helper computes a least-squares linear regression slope from the e1RM series, scaled to kg/week.
def _slope_per_week(series: list[dict[str, Any]]) -> float: """Simple least-squares slope of e1RM vs. day-index, scaled to per-week.""" xs = [] ys = [] base = datetime.fromisoformat(series[0]["date"]) for pt in series: d = datetime.fromisoformat(pt["date"]) xs.append((d - base).days) ys.append(pt["e1rm_kg"]) n = len(xs) mx = sum(xs) / n my = sum(ys) / n num = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) den = sum((x - mx) ** 2 for x in xs) or 1.0 slope_per_day = num / den return slope_per_day * 7.0 - The `_collect_sets_for_exercise` helper iterates workouts and collects all sets matching a given exercise_template_id.
async def _collect_sets_for_exercise( client, template_id: str, max_pages: int, ) -> list[tuple[dict[str, Any], str | None]]: out: list[tuple[dict[str, Any], str | None]] = [] async for w in _iter_workouts(client, max_pages): when = w.get("start_time") or w.get("end_time") for ex in w.get("exercises") or []: if ex.get("exercise_template_id") != template_id: continue for s in ex.get("sets") or []: out.append((s, when)) return out - src/hevy_mcp/tools/__init__.py:1-12 (registration)The `register_all` function in `tools/__init__.py` calls `analytics.register(mcp, ctx)` which registers all analytics tools including `progression_trend`.
"""Tool registration. Each module exposes `register(mcp, ctx)` to attach its tools.""" from . import analytics, folders, routines, templates, webhooks, workouts def register_all(mcp, ctx) -> None: workouts.register(mcp, ctx) routines.register(mcp, ctx) folders.register(mcp, ctx) templates.register(mcp, ctx) webhooks.register(mcp, ctx) analytics.register(mcp, ctx)