# WorkoutLog Pro — AI connector (MCP)

Log and read workouts from Claude or ChatGPT. The connector is a remote MCP server
(Streamable HTTP) at:

    https://workoutlogpro.com/mcp

## Connect

**Claude:** Settings → Connectors → Add custom connector → paste the URL → Connect. Sign in
with Google on the WorkoutLog Pro screen (this creates a free account if you don't have
one) and click **Allow**.

**ChatGPT:** Settings → Apps & Connectors → Advanced → Developer mode, then Create → paste
the URL, authentication OAuth.

Authentication is OAuth 2.1: dynamic client registration (RFC 7591), PKCE S256,
protected-resource metadata (RFC 9728) at `/.well-known/oauth-protected-resource/mcp`,
authorization-server metadata (RFC 8414), and client ID metadata documents (CIMD: an https
`client_id`, fetched with SSRF guards and cached a day; loopback redirects match any port).
Public clients revoke without a secret. Access tokens last 1 hour; refresh tokens 90 days,
rotated on every use. Only token hashes are stored. Scopes: `read`, `write`. Disconnect any
time in the app under Settings → AI assistant.

## Designed for a photo of a notebook page

A page of lifts with partial weights and reps becomes ONE `log_workout` call. Sets are
logged as written; unknowns are left out (never invented); unclear sets get
`needs_detail: true` and a `note`. The app shows them in a "sets to refine" card on the
dashboard and a "to refine" badge; editing a set in the app clears the flag. The user's
answers become ONE `update_sets` call. Nicknames ("RDL") become exercise aliases via
`save_exercise`, so the next page needs no questions.

## Plans: today's session in the app

An assistant that plans a workout saves it with `set_plan` (one call per day). The dashboard
shows it as a checklist: each exercise with its target, "last time" from the log, and one chip
per planned set. Tapping a chip opens the set with the planned numbers prefilled; nothing is
logged until the user confirms. Progress is read from the sets actually logged that day, so
calling `set_plan` again (a new version) never loses what's done, and sets logged on the
exercise page or with `log_workout` tick the plan too. `get_plan_context` returns last session
+ records for every planned exercise in one call, so targets start from real numbers.

## Tools

Dates are `YYYY-MM-DD` in the user's timezone (Settings), default today. Weights are in the
user's unit (Settings, lb or kg) unless a set passes `unit`.

| Tool | Arguments | Returns |
|---|---|---|
| `log_workout` | `exercises: [{exercise, sets, note?}]` (max 50 exercises, 200 sets), `date?`, `name?`, `note?` | logged set ids per exercise, `created_exercises` (with `maybe_same_as`), totals |
| `get_workout` | `date?` | name, note, exercises with every set (ids), sets, volume, `to_refine` |
| `get_history` | `exercise` (name/nickname/id), `limit?` (10, max 100) | last sessions with sets, top set, volume |
| `get_prs` | `exercises?` (default: all logged) | heaviest set, best e1RM (Epley, ≤12 reps), best session volume, most reps, longest duration/distance |
| `get_range` | `start`, `end` (≤366 days) | workouts per day, days trained, per week, total sets/volume, top exercises |
| `update_sets` | `updates: [{id, …}]` (max 50) | updated sets; per-day totals |
| `delete_sets` | `ids` (max 200) | `deleted`, `not_found`, per-day totals |
| `delete_workout` | `date` | `{deleted, sets}`: the day's sets, name, note and exercise notes |
| `lookup` | `query` (a term or a list; `""` lists all) | per term: `match` (id, type, aliases, last session) or ranked `candidates` (gym shorthand like RDL/OHP/bench, initials, shared words), never auto-matched |
| `save_exercise` | `name` or `exercise_id`; `aliases?`, `type?` (reps/time/distance), `weighted?`, `bilateral?`, `per_hand?`, `icon?`, `hidden?` | the saved exercise |
| `get_entries_to_refine` | `days?` (30, max 365) | sets still flagged `needs_detail` |
| `submit_feedback` | `text`, `category` (feature_request/bug/other) | `{received, id}`; stored and sent to the developer |
| `set_plan` | `exercises: [{exercise, sets?, reps? (number or one per set), reps_max?, weight?, unit?, duration_s?, target?, note?}]` (max 30), `date?`, `title?`, `note?` | the plan (version), per exercise sets done that day and last session, `unmatched` names |
| `get_plan` | `date?` | `plan` (or null) with targets, done sets, last session, totals; `upcoming` planned days |
| `delete_plan` | `date` | `{deleted}`; logged sets untouched |
| `get_plan_context` | `exercises` (names/ids, max 30) | per exercise: last session, sessions, heaviest set, best e1RM; unknown names → candidates |

A set is any of `reps`, `weight` + `unit` (lb/kg; for per-hand exercises one dumbbell),
`band` (light/medium/heavy/xheavy), `duration_s`, `distance` + `distance_unit` (km/mi/m),
`side` (left/right), `rpe` (1–10), `note`, `needs_detail`, and `repeat` (1–20 identical sets:
"3x10 @135" = `{reps: 10, weight: 135, repeat: 3}`). A set with nothing known must have
`needs_detail: true`. Unknown fields are errors.

Rules:
- Exercise names match the user's exercises by name or alias, ignoring case, accents,
  spaces, dashes and a trailing plural ("pull ups" = "Pull-ups"). An unmatched name in
  `log_workout` creates a custom exercise (type inferred from the sets).
- One workout per day: later `log_workout` calls for the same date add sets to it.
- `log_workout` and `update_sets` are all-or-nothing. `update_sets` changes only the fields
  passed (null clears); changing reps/weight/band/duration/distance clears `needs_detail`
  unless passed; `exercise` moves a set to another exercise, `date` to another day.
- An identical `log_workout` call within 60 s returns the first result (client retries).
  Repeated sets inside one call are never merged.

Annotations: every tool has a title, `openWorldHint: false`, and read-only / destructive /
idempotent hints. Read-only: `get_*`, `lookup`. Destructive: `update_sets`, `delete_sets`, `delete_workout`,
`set_plan` (replaces that day's plan), `delete_plan`,
`save_exercise` (it can overwrite an exercise's settings). `submit_feedback` and
`log_workout` write but don't destroy.

## Security
- Every tool is scoped to the signed-in user; other users' sets, exercises and ids are
  invisible (`not found`).
- `/mcp` rejects browser requests from origins other than claude.ai, claude.com,
  chatgpt.com and chat.openai.com; server-to-server calls send no Origin.
- 60 requests per minute per user × app. nginx keeps no access log for `/mcp`.
- Each tool call is recorded in `mcp_calls` (tool, ok, duration, app; no arguments).

## Account and policies

Privacy: /privacy (FR /fr/confidentialite) · Terms: /terms · Support: /support. Users export
everything (`GET /api/account/export`) or delete their account (`POST /api/account/delete`,
`{"confirm": "DELETE"}`) from Settings → Account. Directory reviewer accounts sign in with a
password at `/auth/password` (set with `python account.py set-password EMAIL`; no other account
can use it); anyone can get an emailed sign-in link at `/auth/email`.
