{"ok":true,"site":{"host":"spot.mootoo.co","url":"https://spot.mootoo.co/","type":"live","desc":"A WebMCP-native workout tracker: you log the sets, an agent in the same browser reshapes what is left. Sixteen page-scoped tools on document.modelContext, one live session, one set of rules.","tools":[{"name":"get_training_context","kind":"answer","impl":"imperative","description":"Start here. Returns what this app can do, the few facts that decide what to call next (whether a workout is in progress and its revision, whether a draft is waiting, units, how many training constraints are active, available gym equipment), and the operating rules for editing someone's training. Call it once at the start of a conversation about training, and again after anything that could have changed state. Read-only.","annotations":{"readOnlyHint":true},"inputSchema":{"type":"object","properties":{},"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/","/gym"]},{"name":"get_active_workout","kind":"answer","impl":"imperative","description":"Read the workout currently being performed, exactly as the app has it: exercises in order, warm-up and working sets, which sets are already completed, prescribed targets, rest, superset groups, and the `revision`. Call this immediately before any edit and pass the `revision` you get back as `expected_revision`. Returns { active: null } when no workout is in progress. Never infer completed sets from conversation — this is the only source of truth for what has actually been performed. Read-only.","annotations":{"readOnlyHint":true},"inputSchema":{"type":"object","properties":{},"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/","/gym"]},{"name":"search_exercises","kind":"answer","impl":"imperative","description":"Search the exercise catalog. Results are filtered twice — to what the active gym can equip, and to what the current training constraints allow — so anything returned here is safe to add or substitute. Use this BEFORE proposing any exercise by name; names must match the catalog exactly for edit_active_workout to accept them. `excluded_count` is how many otherwise-matching movements were withheld, and `excluded_by_equipment` how many of those were the gym rather than the body — if a movement someone asked for is missing, that pair says which to tell them. Counts describe the `sampled` rows examined, not the whole `catalog_total`. Use list_gyms or switch_gym when the equipment is the problem. Read-only.","annotations":{"readOnlyHint":true},"inputSchema":{"type":"object","properties":{"q":{"type":"string","description":"Free-text search over name, primary muscle and equipment."},"muscle":{"type":"string","description":"Canonical muscle region to filter by, e.g. chest, lats, quads, hamstrings."},"equipment":{"type":"string","description":"Equipment class, e.g. barbell, dumbbell, machine, cable, body only, bands."},"modality":{"type":"string","description":"Filter by movement type: 'strength' (default for programming), 'stretch', 'dynamic', 'cardio'. Pass 'strength' to keep stretches and mobility drills out of a working set search."},"limit":{"type":"integer","description":"Maximum rows to return (default 50, max 200)."}},"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/","/gym"]},{"name":"get_muscle_readiness","kind":"answer","impl":"imperative","description":"Per-muscle-region readiness, freshest first: status (fresh, ready, recovering, undertrained, untrained), days since the region was last worked, and its weighted working sets over the last seven days. Use it to choose what to train and to justify a choice. This is a TRAINING-HISTORY signal derived from logged sets only — it is not a recovery score, not a physiological measurement, and no wearable data enters it. Read-only.","annotations":{"readOnlyHint":true},"inputSchema":{"type":"object","properties":{},"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/","/gym"]},{"name":"get_training_constraints","kind":"answer","impl":"imperative","description":"List the training constraints currently in force — the body regions someone has said they cannot load right now, and how limiting each one is. These are hard limits on exercise selection, not preferences. Check them before proposing or substituting any movement. Read-only.","annotations":{"readOnlyHint":true},"inputSchema":{"type":"object","properties":{"include_resolved":{"type":"boolean","description":"Default false. True also returns constraints that have already been resolved."}},"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/","/gym"]},{"name":"set_training_constraint","kind":"act","impl":"imperative","description":"Records a user-stated training limitation so exercise eligibility can exclude conflicting movements. This does not diagnose an injury or prescribe rehabilitation. Use action \"create\" with a canonical region when someone says they cannot load something; \"update\" with an id to change its severity or note; \"resolve\" with an id when they say it is fine again. Severity: nagging (train around it), limiting or out (both exclude conflicting movements outright).","inputSchema":{"type":"object","properties":{"action":{"type":"string","enum":["create","update","resolve"]},"id":{"type":"string","description":"update/resolve: the constraint id from get_training_constraints."},"region":{"type":"string","description":"create: the body region. Canonical sites: shoulder_joint, elbows, wrists, hands, upper_arms, spine, ribs, hips, pelvis, groin, thighs, lower_legs, feet, head, neck, knees, ankles, plus the muscle regions traps, delts, chest, biceps, forearms, abs, obliques, quads, calves, lats, mid_back, lower_back, triceps, glutes, hamstrings. Common words are accepted and mapped (shoulder → shoulder_joint, knee → knees, back → lower_back, hamstring → hamstrings, …)."},"severity":{"type":"string","enum":["nagging","limiting","out"],"description":"nagging = noted, nothing is excluded; limiting = every movement that loads the site is excluded from search, drafts and live edits; out = the site is unusable (same exclusion). Choose limiting when the person wants to work around it today."},"label":{"type":"string","description":"Short human label, e.g. \"left shoulder\"."},"note":{"type":"string","description":"What the person actually said, in their words."},"expires_in_days":{"type":"integer","description":"create: register a self-expiring soft flag for this many days instead of an open-ended constraint."}},"required":["action"],"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/","/gym"]},{"name":"get_exercise_progress","kind":"answer","impl":"imperative","description":"Everything known about one movement: personal records, the last eight sessions summarised by working volume, best set and top estimated 1RM, the per-day trend series, and the explicit progression rule that applies plus the target it produces for the next session. Use it before suggesting a load, and to answer \"how am I doing on X\". Progression is deterministic arithmetic over logged sets, not an opinion — quote the rule rather than inventing one. If the name does not resolve, the response lists close matches; retry with one of those. Read-only.","annotations":{"readOnlyHint":true},"inputSchema":{"type":"object","properties":{"exercise":{"type":"string","description":"Exercise name. Use search_exercises if you are unsure of the exact catalog name."}},"required":["exercise"],"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/","/gym"]},{"name":"get_workout_history","kind":"answer","impl":"imperative","description":"Completed sessions: a month calendar, the last eight weeks of session count and working volume, a page of sessions newest first (name, date, duration, exercise and set counts, volume, PRs, source template), and the template \"eras\" the training has moved through. Use it for \"what did I do last week\", for streaks and for volume trends. Only completed workouts appear here — the one in progress is get_active_workout. Read-only.","annotations":{"readOnlyHint":true},"inputSchema":{"type":"object","properties":{"month":{"type":"string","description":"Calendar month as YYYY-MM. Defaults to the current month."},"limit":{"type":"integer","description":"Sessions per page."},"offset":{"type":"integer","description":"Sessions to skip, for paging further back."}},"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/","/gym"]},{"name":"list_gyms","kind":"answer","impl":"imperative","description":"List the gyms on record and which one is currently active. The active gym decides what counts as available equipment, so every draft, substitution and exercise search is limited to it. Check this before assuming a movement is possible. Read-only.","annotations":{"readOnlyHint":true},"inputSchema":{"type":"object","properties":{},"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/","/gym"]},{"name":"switch_gym","kind":"act","impl":"imperative","description":"Make a gym the active one. Everything selected afterwards — drafts, substitutions, search — is restricted to that gym's equipment, so switching is how you say \"I am somewhere else today\". Matches an existing gym by name, case-insensitively. When the name is new, pass `equipment` to record it on the spot: that is the travelling case, where the room in front of you has never been seen before. Passing no equipment for an unknown name fails with the list of known gyms rather than inventing one.","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"The gym to switch to, e.g. \"Hotel gym\" or the name from list_gyms."},"equipment":{"type":"array","items":{"type":"string","enum":["barbell","dumbbell","machine","cable","body only","kettlebells","bands","e-z curl bar","medicine ball","exercise ball","foam roll","other"]},"description":"Creating a new gym: the equipment categories actually present. Anything omitted is treated as absent, which is the point — a dumbbell-only room should exclude every barbell lift."},"machines":{"type":"array","items":{"type":"string"},"description":"Creating a new gym: named machines present, in the room's own words."},"notes":{"type":"string","description":"Creating a new gym: anything worth remembering about it."}},"required":["name"],"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/","/gym"]},{"name":"get_training_plan","kind":"answer","impl":"imperative","description":"ANSWERS \"what's next up\" / \"what am I training today\". Returns the active training plan — its ordered days and which one comes next — plus how many sessions have been completed. When a plan is active it DECIDES the next session: report its next day rather than choosing one from muscle readiness, from whichever template was performed least recently, or from what has never been done. Readiness explains why a day suits; it does not choose. Do not just NAME the next session — the person cannot see a name: stage it with draft_workout mode \"tune\" and the returned nextDay.templateId so it appears in the app as a reviewable draft, then start_workout from \"plan\" when they say go. If no plan is active it says so, and only then is a fresh draft_workout the right next step. Read-only.","annotations":{"readOnlyHint":true},"inputSchema":{"type":"object","properties":{},"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/","/gym"]},{"name":"draft_workout","kind":"act","impl":"imperative","description":"Builds a deterministic starting draft from readiness, constraints and equipment. The adaptive planner is not part of this repo. The draft is a proposal, not a session — nothing is logged until start_workout. mode \"draft\" deals a fresh one; \"tune\" anchors to a saved template — including the day an active plan says is next, which is what you should stage rather than dealing a fresh rotation over the top of a running programme (get_training_plan returns that templateId); \"shuffle\" rotates the exercises of an existing draft. Follow it with edit_workout_draft to adjust, and tell the person what the draft actually contains rather than describing it in the abstract.","inputSchema":{"type":"object","properties":{"mode":{"type":"string","enum":["draft","tune","shuffle"],"description":"Default \"draft\"."},"focus":{"type":"string","description":"Optional emphasis, e.g. \"push\", \"legs\", \"upper\". Constraints still override it."},"templateId":{"type":"string","description":"tune: the template to anchor the draft to."},"proposalId":{"type":"string","description":"shuffle: the draft to rotate."}},"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/","/gym"]},{"name":"start_workout","kind":"act","impl":"imperative","description":"Start a session. from \"plan\" begins the active training plan's NEXT day — the right choice whenever a plan is running, because the plan decides the order (check get_training_plan first). from \"draft\" begins today's draft; \"template\" begins a saved template; \"repeat_last\" repeats the previous session; \"empty\" starts a blank one to fill in. From here on, the workout is live and edits go through edit_active_workout. If a workout is already in progress this refuses and tells you so — read it with get_active_workout rather than starting another. Confirm with the person before starting; a started session is what the logger records against.","inputSchema":{"type":"object","properties":{"from":{"type":"string","enum":["plan","draft","template","repeat_last","empty"]},"dayId":{"type":"string","description":"from \"plan\": start a specific day instead of the plan's next one. Omit to take the next day, which is almost always what is wanted."},"templateId":{"type":"string","description":"from \"template\": which template."},"proposalId":{"type":"string","description":"from \"draft\": which draft. Defaults to today's."},"workoutId":{"type":"string","description":"Optional: repeat a specific past session by id."}},"required":["from"],"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/","/gym"]},{"name":"report_training_constraint","kind":"act","impl":"imperative","description":"Stage a training limitation the person has stated, so exercise eligibility can exclude conflicting movements. This does not diagnose an injury or prescribe rehabilitation. This tool fills the constraint form on the dashboard and then waits: nothing is recorded until the person presses Add on the page, and the button is theirs to press — do not click it for them. The call returns \"recorded\" once they have, or \"awaiting_confirmation\" if they have not yet; in that case the values stay on screen and get_training_constraints will show the result after they do. Use it when someone says they cannot load something; read get_training_constraints first to avoid duplicating one already in force. For a limitation the agent may record on its own authority, use set_training_constraint instead.","inputSchema":{"type":"object","properties":{"region":{"type":"string","enum":["traps","delts","chest","biceps","forearms","abs","obliques","quads","calves","lats","mid_back","lower_back","triceps","glutes","hamstrings","neck","knees","wrists","ankles","head","shoulder_joint","upper_arms","elbows","hands","spine","ribs","hips","pelvis","groin","thighs","lower_legs","feet","other"],"description":"The body region that cannot be loaded. Canonical site; common words are accepted and mapped (shoulder → shoulder_joint, knee → knees, back → lower_back)."},"severity":{"type":"string","enum":["nagging","limiting","out"],"description":"nagging = noted, nothing excluded; limiting = movements that load the site are excluded from search, drafts and live edits; out = the site is unusable (same exclusion)."},"label":{"type":"string","description":"Short human label, e.g. \"left shoulder\"."},"note":{"type":"string","description":"What the person actually said, in their words."}},"required":["region","severity"],"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/","pages":["/"]},{"name":"report_training_constraint","kind":"act","impl":"declarative","description":"Record a training limitation the person has stated, so exercise eligibility can exclude conflicting movements. This does not diagnose an injury or prescribe rehabilitation. Filling this form does not save anything on its own: the values appear on screen and the call completes only when the person presses Add. Use it when someone says they cannot load something; read get_training_constraints first to avoid duplicating one already in force.","inputSchema":{"type":"object","properties":{"region":{"type":"string","enum":["traps","delts","chest","biceps","forearms","abs","obliques","quads","calves","lats","mid_back","lower_back","triceps","glutes","hamstrings","neck","knees","wrists","ankles","head","shoulder_joint","upper_arms","elbows","hands","spine","ribs","hips","pelvis","groin","thighs","lower_legs","feet","other"]},"severity":{"type":"string","enum":["nagging","limiting","out"]},"label":{"type":"string","description":"e.g. left shoulder"},"note":{"type":"string","description":"what they actually said"}},"required":["region","severity"]},"__source":"spec","__declarative":true,"page":"/","pages":["/"]},{"name":"edit_active_workout","kind":"act","impl":"imperative","description":"Modify the workout currently being performed. Reads canonical live state before mutation; pass `expected_revision` from get_active_workout. Completed sets are preserved unless explicitly correcting logged data. Exercise additions and replacements must satisfy current equipment and training constraints; use search_exercises first. Ops apply in order and each is accepted or rejected on its own — the response lists both. A stale_revision answer means someone logged a set while you were thinking: re-read with get_active_workout and retry. Not for finished sessions.","inputSchema":{"type":"object","properties":{"expected_revision":{"type":"integer","description":"The `revision` from your most recent get_active_workout. Always pass it."},"ops":{"type":"array","items":{"type":"object","properties":{"op":{"type":"string","enum":["add_exercise","remove_exercise","replace_exercise","reorder","set_scheme","set_weight","set_warmup_sets","set_rest","set_superset","clear_superset","set_notes","rename","restore_template_weights"],"description":"Which change to make."},"exercise_name":{"type":"string","description":"The exercise in the CURRENT workout this op acts on (case-insensitive)."},"replacement_exercise_name":{"type":"string","description":"replace_exercise: the movement to swap in. Must satisfy current constraints and equipment."},"exercise_names":{"type":"array","items":{"type":"string"},"description":"set_superset: every current exercise to put in one group (at least two)."},"to":{"type":"string","enum":["top","bottom"],"description":"reorder: destination shortcut."},"to_position":{"type":"integer","description":"add_exercise/reorder: 1-based destination slot."},"sets":{"type":"integer","description":"set_scheme: absolute WORKING-set count. Warm-ups are excluded and preserved."},"reps":{"type":"integer","description":"set_scheme: reps for incomplete working sets."},"sets_delta":{"type":"integer","description":"set_scheme: relative working-set change, e.g. -1."},"weight":{"type":"number","description":"set_weight: load for the target set(s)."},"weight_unit":{"type":"string","enum":["lb","kg"]},"warmup_sets":{"type":"array","items":{"type":"object","properties":{"weight":{"type":"number","description":"Exact warm-up load."},"weight_unit":{"type":"string","enum":["lb","kg"]},"reps":{"type":"integer","description":"Exact warm-up reps."},"duration_s":{"type":"integer","description":"Exact warm-up hold in seconds."},"rpe":{"type":"number"},"rest_seconds":{"type":"integer","description":"Rest after this exact warm-up set."},"side":{"type":"string","enum":["left","right"]}},"additionalProperties":false},"description":"set_warmup_sets: the exact ordered warm-up ramp. Pass [] to clear incomplete warm-ups. Completed warm-ups must be repeated unchanged."},"set_number":{"type":"integer","description":"set_weight/set_rest: 1-based set. Omit to apply to all incomplete working sets."},"apply_to_completed":{"type":"boolean","description":"Default false. True ONLY when explicitly correcting data that was already logged — it rewrites performed values."},"rest_seconds":{"type":"integer","description":"set_rest: rest after each set of this exercise."},"notes":{"type":"string","description":"set_notes: this session's cue for the exercise. Empty clears it."},"workout_name":{"type":"string","description":"rename: the new session name."}},"required":["op"],"additionalProperties":false},"description":"Ordered edits to apply."}},"required":["ops"],"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/gym","pages":["/gym"]},{"name":"edit_workout_draft","kind":"act","impl":"imperative","description":"Adjust today's workout draft before it is started — add, remove, swap, reorder, change sets/reps, target load, rest, or supersets. Additions and replacements go through the same eligibility gate as the live editor, so a movement excluded by a training constraint is refused here too; use search_exercises to find a legal alternative. This edits the PLAN. Once a session has started, use edit_active_workout instead — that is the one that touches performance.","inputSchema":{"type":"object","properties":{"ops":{"type":"array","items":{"type":"object","properties":{"op":{"type":"string","enum":["add_exercise","remove_exercise","replace_exercise","reorder","set_scheme","set_weight","set_rest","set_superset","clear_superset","rename"]},"exercise_name":{"type":"string","description":"The draft exercise this op acts on."},"replacement_exercise_name":{"type":"string","description":"replace_exercise: the movement to swap in."},"exercise_names":{"type":"array","items":{"type":"string"},"description":"set_superset: at least two draft exercises to group."},"to":{"type":"string","enum":["top","bottom"]},"to_position":{"type":"integer","description":"1-based destination slot."},"sets":{"type":"integer","description":"Working sets for this slot."},"reps":{"type":"integer"},"weight":{"type":"number","description":"Target load for the slot."},"rest_seconds":{"type":"integer"},"workout_name":{"type":"string","description":"rename: the draft name."}},"required":["op"],"additionalProperties":false},"description":"Ordered edits to apply to the draft."}},"required":["ops"],"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/gym","pages":["/gym"]},{"name":"log_active_sets","kind":"act","impl":"imperative","description":"Record completed sets in the workout currently being performed. Call get_active_workout immediately before this and pass its revision. Supply the actual result for every set: weight and reps for weighted work, reps for bodyweight work, duration for timed work, and distance plus duration for distance work. This tool never guesses from a target or a previous session. It refuses already completed sets; use edit_active_workout with apply_to_completed only for an explicit correction.","inputSchema":{"type":"object","properties":{"expected_revision":{"type":"integer","description":"Revision returned by the most recent get_active_workout call."},"sets":{"type":"array","items":{"type":"object","properties":{"exercise_name":{"type":"string","description":"Exact exercise name from get_active_workout."},"set_number":{"type":"integer","description":"1-based set number from get_active_workout."},"side":{"type":"string","enum":["left","right"],"description":"Required only when that set has separate left/right rows."},"weight":{"type":"number","description":"Actual load used. Required for weighted and timed sets."},"weight_unit":{"type":"string","enum":["lb","kg"],"description":"Unit for weight; defaults to the workout unit."},"reps":{"type":"integer","description":"Actual repetitions. Required for rep-based sets."},"duration_seconds":{"type":"number","description":"Actual duration in seconds. Required for timed sets."},"distance_m":{"type":"number","description":"Actual distance in metres. Required for distance-and-time sets."},"rpe":{"type":"number","description":"Optional actual RPE."}},"required":["exercise_name","set_number"],"additionalProperties":false},"description":"The actual completed sets to record."}},"required":["expected_revision","sets"],"additionalProperties":false},"hasHandler":true,"hasExecute":true,"__source":"spec","executable":true,"handlerField":"execute","page":"/gym","pages":["/gym"]}],"category":"Health & Education","favicon":"/favicons/spot.mootoo.co.svg","apiSurface":"spec","toolCount":18}}