# Workout Tracker > Workout rotation tracker at https://workout.lbyte.app. Agents can search a rated exercise catalog, check a draft plan's weekly sets per muscle and its ratings, then save the plan to the user's account and make it the one they train. The signed-in app picks up changes when it next syncs: on open, or on returning to it. All endpoints are JSON under https://workout.lbyte.app. Full contract (OpenAPI 3.1): https://workout.lbyte.app/openapi.json ## Auth - Reading the catalog, searching and analyzing needs no auth. - Acting for a user (saving or picking plans, reading their plans) needs their personal access token: `Authorization: Bearer wt_...`. The user creates one in the app under Settings > Agent access. If you don't have one, ask the user for it. Never log or repeat it. Tokens cover plans, timing, gear, targets and target presets, `GET /api/me` and read-only history; workout sync (`POST /api/sync`) and token management stay with the browser session. ## Planning loop 1. `GET /api/plans/{id}` or `GET /api/plans`: the user's current plans and `activePlanId`. Start from the active plan. `GET /api/me/history?since=YYYY-MM-DD&limit=50` shows what they actually trained. 2. `GET /api/catalog`: vocabularies (muscles, movement patterns, equipment, rating facets with their 1-5 rubrics). It also lists every exercise; prefer search over loading it all into context. 3. `GET /api/exercises?q=&muscle=&primary=true&pattern=&mechanic=&equipment=a,b&sort=&limit=20`: find exercises. `equipment` keeps only exercises doable with the listed gear; machines are specific (`leg-press-machine`), and `machine` stands for all of them. `sort` ranks best first by a facet (low is best for `injury-risk`, `skill-demand`, `spinal-load`). Unknown ids return 400 with details. 4. `GET /api/exercises/{id}`: one exercise with its sourced ratings (video, timestamp, our summary of the claim, evidence level, and `muscle` when the claim judged it for one muscle). Search results carry headline `scores` (generic claims and claims about a prime mover) and `muscleScores` per muscle where claims name one; use the muscle's score when choosing for that muscle. 5. `POST /api/plans/analyze` with `{"plan": {...}, "targets": {...}}`: validation `errors`, `warnings` (retired exercises), and `analysis`: - `weekly`: fractional sets per muscle per week (prime mover 1 set, helper 0.5), training days per week, `target` and `status` (`under`, `ok`, `over`). Optional entries are reported apart and do not count toward the status. - `under` / `over`: muscles off target; `untrained`: muscles nothing hits. - `days`: the same per day, plus movement patterns. - `scores`: whole-plan rating per facet, the exercises' means weighted by required sets, with `coverage` (share of sets that are rated). - `sessions`: estimated minutes per day (`minutes` required work, `withOptional` with optional work too), and the `timing` it used. - `insights`: the same review the app's Plan analysis page shows the user, so match its conclusions. `gaps` lists what is missing, most important first (`untrained`, `pattern`: an essential movement group without required sets, `optional-only`, `under`, `balance`, `frequency`: a muscle trained about once a week, `over`). `groups`: weekly sets for squat or lunge, hinge, horizontal and vertical push and pull, and core. `balance`: push vs pull, quads vs hamstrings, front vs rear delts, upper vs lower, each with `ratio`, `ok` and a `note`. Also `regions` (prime-mover upper/core/lower split), `patterns`, `exercises` (weekly sets merged across days), `totals` (compound and single-side share), `focus`, `equipment` and `cycleWeeks` (above 1, weekly numbers are averages). - Default targets come from `muscles[].target` in `GET /api/catalog` (`min`, `max`, `note` with the basis): 10-20 weekly sets for chest, lats, upper-back, side-delts, rear-delts, biceps, triceps, abs, glutes, quads, hamstrings and calves, at least 2 for traps, none for the rest; with a token, the user's own changes (`GET /api/me/targets`) apply on top. Override per muscle with `"targets": {"calves": {"min": 6}, "abs": null}` (null drops a target). 6. Revise and analyze again until `errors` is empty, the volume fits the user's goal, `insights.gaps` holds nothing the user cares about and `sessions` fit their time. 7. `PUT /api/plans/{id}` with the plan as the body: saves only when it validates (422 with the same analysis otherwise). Saving under the id of the plan the user is on replaces it for them and keeps their place in the rotation, as long as the day ids stay the same (the next day follows the last completed day id). Use a new id for a separate plan. 8. `PUT /api/me/active-plan` with `{"planId": "..."}`: make it the plan the user trains. The latest pick to reach the server wins, so a later switch in the app replaces it. 9. `DELETE /api/plans/{id}`: remove a saved plan; a built-in plan it replaced comes back. Deleting the active plan sends the user back to the default. ## Session length Session length is computed, never written into the plan: warm-up, plus per exercise `sets x (setSeconds + rest)`, plus 60 s between exercises. Unilateral exercises (`laterality` in the catalog) do each set once per side, so their `setSeconds` counts twice. A superset is all its moves, then one rest. Rest is the user's override when set, else the plan's `restSeconds`; an entry's own `restSeconds` always wins. - `GET /api/me/timing`: the user's `{setSeconds, warmupMinutes, restSeconds}` (null rest follows the plan) and whether they changed the defaults (40 s, 5 min, plan rest). - `PUT /api/me/timing`: change it, for example when the user says their sets take longer. The app follows on its next sync, and they can edit it in Settings > Session time. - `POST /api/plans/analyze` uses the user's timing with a token, the defaults without one, or a `timing` in the body. ## Gear - `GET /api/me/gear`: the equipment the user trains with, `{gear, saved}`. `gear` is equipment ids from `GET /api/catalog` (a parent such as `machine` covers its children), or null for a full gym. The app swaps plan exercises their gear rules out for the closest ones it allows. - `PUT /api/me/gear` with `{gear}`: change it, for example when they train at home this week. Unknown ids are a 400. They can also edit it in Settings > Your gear. - `GET /api/me/targets`: the user's changes to the catalog's weekly set targets, `{targets, saved}`. `targets` maps muscle ids to whole-number `{min, max}` (either may be left out) or null for no target; muscles left out follow the catalog. `POST /api/plans/analyze` with their token judges against these, and a request's own `targets` layer on top. - `PUT /api/me/targets` with `{targets}`: replace the whole map, for example when they want more trap work. `{}` goes back to the catalog's. Unknown muscles or min above max are a 400. They can also edit them in Settings > Weekly set targets. - Presets: built-in ones are `targetPresets` in `GET /api/catalog` (`id`, `name`, `summary`, `note` with the basis, `targets` as a whole map: a muscle left out has no target). The user's own: `GET /api/me/target-presets` (`{presets: [{id, name, targets}]}`), `POST /api/me/target-presets` with `{name, targets}` (201; names unique per user, case-insensitive), `PUT /api/me/target-presets/{id}` with `{name?, targets?}`, `DELETE /api/me/target-presets/{id}`. `POST /api/me/target-presets/{id}/apply` (built-in or own `my-...` id) makes it their targets and returns the new `{targets}`. ## Plan shape ```json { "id": "upper-lower", "name": "Upper / lower", "startDate": "2026-11-09", "daysPerWeek": 4, "restSeconds": 120, "restGuidance": "Rest 2-3 min on compounds, 1-2 min on isolations.", "guidance": ["Short tips shown in the app."], "days": [ { "id": "A", "name": "Upper", "entries": [ { "exercise": "incline-dumbbell-press", "sets": 3, "alternatives": ["machine-chest-press"] }, { "exercise": "lat-pulldown", "sets": 3, "superset": "s1" }, { "exercise": "cable-lateral-raise", "sets": 2, "superset": "s1", "optional": true, "note": "Slow negatives." } ] } ] } ``` - Ids are kebab-case. Exercise ids come from the catalog; retired exercises warn. - `sets` is a whole number 1-20, `daysPerWeek` 1-14, `restSeconds` 1-1800. An exercise appears at most once per day. Entries sharing a `superset` label must be consecutive. - The rotation follows day order (A, B, C, A...) regardless of the calendar; `daysPerWeek` only sets the weekly volume math. - Printable ASCII text only. Limits: 14 days, 50 entries per day, 64 KB of JSON per plan, 20 saved plans per user. ## Notes - Ratings are LLM summaries of claims from RP, Jeremy Ethier and Jeff Nippard videos, each checked against its transcript. Treat them as guidance and say so. - `GET /api/me/history?since=YYYY-MM-DD&limit=50` (token or browser session): the user's logged workouts, newest first (limit up to 500; default 50), as `{completions: [{id, planId, dayId, date, completedAt, done, lifts}]}`. `done` are exercise ids; `lifts` is the optional top set per exercise `{exercise, weight (null is bodyweight), unit kg|lb, reps}`. Deleted workouts are left out. Read-only: workouts are logged in the app, and `POST /api/sync` refuses tokens. ## MCP The same API is an MCP server (Streamable HTTP, stateless, JSON responses, POST only) at three endpoints that share one set of tools (same names, schemas and annotations): - https://workout.lbyte.app/mcp: every tool. Send `Authorization: Bearer wt_...` (Settings > Agent access) for the user's tools; the public tools work without it. - https://workout.lbyte.app/mcp/public: only the public tools, no auth. Any Authorization header is ignored, so `analyze_plan` there uses default timing and the catalog's targets unless you pass `timing` and `targets`. - https://workout.lbyte.app/mcp/account: only the user's tools. Needs a valid token; without one the server answers HTTP 401 with how to create one. Public tools: `search_exercises`, `get_exercise`, `get_vocabulary` (muscles, patterns, equipment, rating facets with rubrics; no exercise list), `analyze_plan`. User tools (need the token): `list_plans`, `get_plan`, `save_plan`, `delete_plan`, `set_active_plan`, `get_timing`, `set_timing`, `get_gear`, `set_gear`, `get_targets`, `set_targets`, `list_target_presets`, `save_target_preset`, `delete_target_preset`, `apply_target_preset`, `get_history`. They call the HTTP routes above, so rules and limits are the same. Follow the planning loop: search, draft, analyze, revise, save, activate. Pick one setup: `/mcp` alone, or `/mcp/public` plus `/mcp/account` as two servers. Two servers suit clients that refuse to connect when a header placeholder such as `${WORKOUT_TRACKER_TOKEN}` is unset: the catalog tools keep working without an account. Do not add `/mcp` next to the other two, or every tool shows up twice. Claude Code: `claude mcp add --transport http workout https://workout.lbyte.app/mcp --header "Authorization: Bearer wt_..."` Other clients (Cursor, ChatGPT developer mode, ...): add a remote MCP server with one of those URLs and, for `/mcp` or `/mcp/account`, the Authorization header. OAuth for claude.ai web connectors is not supported yet; use a client that sends custom headers.