btwb-mcp
Based on the provided schema, this MCP server lets an LLM interact with Beyond the Whiteboard: search movements, log workout results, retrieve history/session details, delete sessions, and refresh authentication.
Search BTWB's movement library by name to get movement IDs.
Log a single-movement result (e.g., a 1RM) with reps, weight, unit, date, and notes; privacy is hardcoded to Only Me.
Log a multi-movement rounds/For Time WOD result with sections, total time, Rx status, and optional track event; privacy is hardcoded to Only Me.
Retrieve a movement's full logged history over a date range, including individual sets and a Potential Max trend.
Fetch details of an already-logged workout session by session ID.
Permanently delete an already-logged workout session by session ID.
Manually re-authenticate and refresh the stored BTWB session cookie.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@btwb-mcp log my 1RM deadlift at 315 lbs today"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
btwb-mcp
An unofficial MCP server that lets an LLM search BTWB's movement library, log CrossFit/weightlifting/gymnastics results, and pull your full training history for Beyond the Whiteboard (BTWB) - straight from a chat, no copy-pasting between apps.
BTWB has no public API. This server calls the same internal JSON/form endpoints the BTWB web app itself uses, found by inspecting its network traffic. It authenticates with a copied browser session cookie rather than a real API key.
This is unofficial and unsupported by BTWB. It can break if they change their app, and your session cookie will periodically expire and need refreshing. Use it for personal automation only.
Disclaimer
This project calls BTWB's internal, undocumented endpoints rather than a published API, using your own logged-in session cookie in place of an API key. It isn't affiliated with, endorsed by, or supported by BTWB, LLC. Using it may be subject to BTWB's own Terms of Service - review those and use this at your own discretion and risk. Provided as-is, with no warranty (see LICENSE).
Related MCP server: hevy-mcp-server
Tools
search_movement(term)- search BTWB's movement library, returns{id, name, modality, posting_trait}matches.log_workout(movementId, movementName, reps, weight, weightUnit, performedDate, notes)- logs a single-movement result (e.g. a 1RM). Always posts with Privacy: Only Me - this is hardcoded insrc/btwb-client.jsand is not an exposed parameter, on purpose.log_rounds_workout(workoutId, workoutSlug, memberId, sections, totalTimeSeconds, performedDate, rxd, notes, trackEventId)- logs a multi-movement "rounds" result (e.g. a For Time WOD with several movements per round). Only "For Time" / total-time scoring is supported. Always posts with Privacy: Only Me, same aslog_workout.log_weigh_in(weight, weighedInDate, hour, minute, percentBodyFat, notes)- logs a body-weight entry to BTWB's Weigh-Ins tracker (the dedicated weigh-in feature, not the "Weigh In" movement). Re-submits the height already stored on your BTWB profile (in Imperial mode BTWB reads it from the form's separate feet/inches fields, so those are sent too - without them every weigh-in fails with "Height must be greater than 0"). No per-entry privacy - BTWB's weigh-in form has no privacy field, so visibility follows your BTWB account settings, same as entering one by hand.get_weigh_ins(days)- reads your Weigh-Ins tracker: each entry's weight, BTWB's change vs. the previous entry, and timestamp, newest first. Optionally limited to the last N days.get_track_events(date, track)- lists the track events (class programming, personal tracks, ...) on one day of your whiteboard calendar, with each workout event'strackEventId,workoutIdandworkoutSlug. Events that already have a result come back asloggedwith theirsessionId. Optionaltrackname filter (e.g."class").log_sets_workout(workoutId, workoutSlug, sets, performedDate, rxd, notes, trackEventId)- logs a set-by-set lifting result (aweightlifting/setsorsingle/emomworkout, e.g. "Bench Press : 3 @ 80%, ... 2 @ 85%") against that prescribed workout, one actual weight per prescribed set, optionally linked to a track event. The performed date can differ from the track event's day. Always posted with Privacy: Only Me.log_amrap_workout(workoutId, workoutSlug, rounds, extraReps, performedDate, rxd, loads, notes, trackEventId)- logs a finished AMRAP (completed rounds + reps into the next round, filled across movements in prescription order) so BTWB stores a real AMRAP score.loadsoverrides load by position for scaled results. Always posted with Privacy: Only Me.log_ascending_amreps_workout(workoutId, workoutSlug, rounds, extraReps, performedDate, rxd, loads, notes, trackEventId)- logs a finished ascending-ladder AMRAP (an "AMReps 2-4-6-8-10..." workout, BTWB typeamreps/ascending, scored in total reps).roundsis the number of completed rounds,extraRepsthe reps done in the unfinished round (filled across movements in prescription order); the tool returns thetotalRepsBTWB stores.loadsoverrides load by position for scaled results. Always posted with Privacy: Only Me.create_multi_emom_workout(everySeconds, untilSeconds, movements, name, description)- defines an EMOM where ALL the movements are done inside every interval (e.g. "Every 1:00 for 6:00: 10 Bench Press at 135 + 3 Strict Pull-ups", BTWB typemultiple/emom, scored in rounds). Find-or-create like the other builders; without anamea no-match returnsneedsNameinstead of creating anything.log_multi_emom_workout(workoutId, workoutSlug, rounds, performedDate, rxd, loads, notes, trackEventId)- logs a finishedmultiple/emomworkout:roundsis the number of intervals completed, 1 to ceil(until / every).loadsoverrides load by position for scaled results. Not for alternating EMOMs (alternating/emom, which has no logger yet). Always posted with Privacy: Only Me.log_time_each_round_workout(workoutId, workoutSlug, roundTimesSeconds, performedDate, rxd, loads, notes, trackEventId)- logs atimeEachRoundresult (e.g. "3x RFT: DB Bench Press, Devil Press, Wall Walks" where every round is timed): one time per round, in order; BTWB totals them.loadsoverrides load by position for scaled results. Always posted with Privacy: Only Me.log_effort_sets_workout(workoutId, workoutSlug, sets, performedDate, rxd, notes, trackEventId)- logs agymnastics/setsresult (e.g. "Strict Ring Dips: 10x max rep", scored in total reps) or amonostructural/setsresult (e.g. "Row: 4x 1500 m, rest 2:30", scored in total time or distance).setshas one entry per prescribed set, in order, withreps,timeSeconds,distanceorweightas that workout asks (seeget_workout_prescription). Always posted with Privacy: Only Me.log_rounds_max_reps_workout(workoutId, workoutSlug, roundScores, performedDate, rxd, loads, notes, trackEventId)- logs aroundsForMaxRepsresult (e.g. "5 RF-ME: Front Squats and Strict Pull-ups"):roundScoresis the max reps each round (a number for one max-rep movement, or an array per round for several, in prescription order). A pick-load movement needs its weight inloadsat its position. BTWB totals the score. Always posted with Privacy: Only Me.log_timed_rounds_workout(workoutId, workoutSlug, roundScores, performedDate, rxd, notes, trackEventId)- logs a timed-rounds, max-reps result (e.g. a class RemReps piece: 5 x 1 min of 10 burpees and max Echo Bike calories).roundScoresis the max-rep score per round in order (calories per round); BTWB totals them, and fixed-rep movements stay as prescribed, so note any difference innotes. Always posted with Privacy: Only Me.get_workout_prescription(workoutId, workoutSlug, performedDate)- read-only: returns a prescribed workout's raw prescription JSON (prescriptionType,scoring,prescription,contents) from its own Log Result page, without posting anything. Use this to see the exact shape BTWB uses for a workout type before writing or extending alog_*tool for it - every logger above was built by inspecting this same JSON. Posts no write, so there's nothing to clean up afterward.log_for_time_workout(workoutId, workoutSlug, totalTimeSeconds, performedDate, rxd, loads, notes, trackEventId)- logs a finished For Time result (with or without a time cap) against that prescribed workout, e.g. a class track's chipper, optionally linked to a track event.loadsoverrides a movement's load by position for scaled results. For a time-capped chipper (forTime/timeCap, e.g. "Room on the Broom") passtotalTimeSecondsif you finished, orrepsCompletedif the cap hit; plain For Time workouts still only support finishes. Always posted with Privacy: Only Me.get_movement_history(memberId, movementId, movementSlug, days)- pulls the full logged history for a movement over a date range: every individual set (date, reps, weight), not just PRs, plus a computed "Potential Max" trend line.memberIdis optional and defaults to the signed-in member.get_member_id()- returns the signed-in member's own numeric member ID, read from the "Analyze Dashboard" link in BTWB's navigation menu.get_workout_session(sessionId)- fetches details of an already-logged result by its session ID.delete_workout_session(sessionId)- permanently deletes an already-logged result by its session ID. No undo.refresh_session_cookie()- manually re-authenticates and replaces the stored session cookie. Every other tool already does this automatically on an expired session (see "Automatic cookie refresh" below) - this is mainly for testing your setup or forcing an early refresh.
Known gaps: workout types with no logger yet
Every log_* tool above checks the real prescription.type BTWB sends back for that exact workout, and only accepts the type string(s) it's actually been verified against - a tight match, never a prefix or substring test, so an unverified sibling type fails with a clear error instead of silently posting something structurally wrong.
BTWB's own prescriptionType enum has 21 values, found in its builder JS bundle and captured in issue #7. This project currently covers 13 of them - weightlifting/sets, single/emom, forTime, roundsForTime, forTime/timeCap, timeEachRound, roundsForMaxReps, bookends, amrap, amreps/ascending, timedRounds/maxReps, gymnastics/sets, monostructural/sets - each one added only after being hit for real in a live session and inspected. The rest:
In the taxonomy, wire format unconfirmed:
alternating-emom,amrapBuyIn,amrapMaxRep,amrapRepeats,amreps(plain, non-ascending),deathby-reps,fgb,forQuality,multiple-emom,rftVaryingReps,roundsForMaxReps,tabata,timeEachRound,twentyOne.
Use get_workout_prescription(workoutId, workoutSlug, performedDate) on a real workout of one of these types to see its exact prescription JSON (read-only, posts nothing), then add a logger for it following the pattern of the existing log_* functions in src/btwb-client.js - see the "Known BTWB prescription types" comment block there for the full picture, including the hyphen-to-slash pattern observed so far between the JS bundle's names and the actual wire format. As with any other write here, verify it against a real (then-deleted) throwaway entry before trusting it - there's no BTWB sandbox to test against instead (see Contributing).
Setup
1. Install dependencies
npm install2. Get your session cookie into Keychain
Log into beyondthewhiteboard.com in your browser.
Open DevTools → Network tab, reload the page.
Click any request to
beyondthewhiteboard.com.Copy the full value of the
Cookierequest header.Store it directly in Keychain (run this yourself in a terminal - never paste your cookie into a chat/AI session):
security add-generic-password -a "$USER" -s "btwb-session-cookie" -A -U -w "your_cookie_here"
This cookie is tied to your login session. If tools start failing with a CSRF/session error, it has expired - repeat these steps for a fresh one, or set up automatic refresh below so you never have to.
3. Register with Claude Code
Add to your .mcp.json (project-level or global):
{
"mcpServers": {
"btwb": {
"command": "node",
"args": ["/absolute/path/to/btwb-mcp/src/index.js"]
}
}
}No env block needed - the cookie lives in Keychain, not in config. Or via the CLI:
claude mcp add btwb -- node /absolute/path/to/btwb-mcp/src/index.js4. (Optional) Enable automatic cookie refresh
By default, when your session cookie expires you refresh it by hand (repeat step 2). Optionally, you can let the server re-authenticate for you automatically whenever it detects an expired session - every tool call transparently retries once through a fresh login if needed, so you never have to touch DevTools again. This has been verified working end-to-end (login → fresh cookie → Keychain update → live authenticated request).
This requires storing your actual BTWB password (not just a session cookie) in Keychain. Weigh that before opting in - see the caveats below.
Store your BTWB password in Keychain (run this yourself in a terminal - never paste your password into a chat/AI session):
security add-generic-password -a "$USER" -s "btwb-password" -A -w-wwith nothing after it makessecurityprompt for the password on a separate line with hidden input - it's never part of the command itself, so it's never echoed and never saved to shell history. (Prefer a GUI? Keychain Access.app → File → New Password Item → namebtwb-password, account = your Mac username, works identically.)Note:
-a "$USER"is just the Keychain lookup key the code uses internally (your Mac account name) - it isn't your BTWB login and doesn't need to match your email.Running this on a Claude cloud session (Claude Code on the web, a remote/cloud environment) instead of your own Mac? Those run in a Linux container with no Keychain at all, so
securitycan't work there. Set theBTWB_PASSWORDenv var instead - the server tries Keychain first and falls back to it automatically. Since a password doesn't go stale the way a copied cookie does, this doesn't reintroduce the "stale cookie in an env var" problem Keychain-only storage was chosen to avoid; the cookie itself still only ever lives in memory for that process, never in an env var or on disk.Set
BTWB_EMAILto your BTWB login email (this one isn't sensitive on its own, unlike the password). Since GUI-launched MCP clients don't source your shell profile, the reliable place is your.mcp.json'senvblock, alongside the cookie:{ "mcpServers": { "btwb": { "command": "node", "args": ["/absolute/path/to/btwb-mcp/src/index.js"], "env": { "BTWB_EMAIL": "you@example.com" } } } }(
.envorexport BTWB_EMAIL=...also work for terminal-launched sessions. On a Claude cloud session, addBTWB_PASSWORDas an environment secret rather than committing it to.mcp.json.)Restart the MCP server.
refresh_session_cookie(or any other tool, automatically, whenever it detects an expired session) will now sign in with those credentials and overwrite the stored session cookie.
Caveats:
This stores a second, more sensitive secret (your actual login password) in Keychain (or, on a Claude cloud session, in
BTWB_PASSWORD), not just a session token.It depends on BTWB's
/signin→/sessionlogin form staying script-friendly. If BTWB ever adds a CAPTCHA or 2FA step, automatic refresh will start failing (with a clear error, not silently) and you'll fall back to the manual method.On a Claude cloud session, the refreshed cookie is cached in memory only and re-fetched on every process restart (one extra login) - it can't be persisted the way it is on macOS.
Don't want this? Just skip this step - everything else works exactly as before, you'll just refresh the cookie by hand when it expires.
Privacy
Every workout result this server logs is posted with Privacy: Only Me, hardcoded in the client, not passed as a parameter. The one exception is log_weigh_in: BTWB's Weigh-Ins form has no privacy field at all, so weigh-ins follow your BTWB account settings. If you ever need a differently-scoped post, do it by hand in the BTWB app rather than changing this server's default.
How the endpoints were found
Documented in commit history / session notes: found by watching Network tab traffic in a real logged-in browser session while performing each action (searching a movement, submitting the "Log Result" form, viewing a movement's PR page), then reading the resulting request URLs and the log form's actual field names directly out of the page DOM.
Search:
GET /exercises/autocomplete_name.json?posting_trait=true&term={term}Log (single movement):
POST /workouts/logger(form-encoded, CSRF-protected,workout_session[definition]JSON)Log (multi-movement/rounds):
POST /workouts/{workoutId}-{slug}/workout_sessions(form-encoded, CSRF-protected,workout_session[uiobject]JSON - a different field name and shape than the single-movement flow)Weigh-in:
GET /members/{memberId}/weigh_ins/new(CSRF token + stored height) thenPOST /weigh_ins(form-encoded:weigh_in[weighed_in_at(1i-5i)]date/hour/minute parts,weigh_in[weight],weigh_in[height],weigh_in[metric], optionalweigh_in[percent_body_fat],weigh_in[notes])History:
GET /members/{memberId}/movements/{movementId}-{slug}/vmax?d={seconds}Single session detail:
GET /workout_sessions/{id}(HTML scrape - no JSON endpoint)Delete:
DELETE /workout_sessions/{id}(CSRF-protected, same endpoint as the app's own "Delete" UJS links)Sign in (for automatic cookie refresh):
GET /signin(pre-login session cookie + CSRF token) thenPOST /session(form-encoded:login,password,authenticity_token,remember_me)
Contributing
Bug reports and PRs are welcome - see CONTRIBUTING.md for how this project is tested (there's no automated test suite) and what to include in a report.
Security
Found a security issue (e.g. a way this could leak your session cookie)? See SECURITY.md for how to report it privately.
License
Available Tools
7 toolsdelete_workout_sessionA
Permanently delete an already-logged BTWB result by its session ID. This cannot be undone - BTWB has no trash/undo for deleted sessions.
Inputs: sessionId (required).
get_movement_historyA
Get the full logged history for a movement over a date range - every individual set (date, reps, weight), not just PRs - plus a computed 'Potential Max' trend line. Requires the BTWB member ID and the movement's numeric ID plus its URL slug (e.g. movementId 35, movementSlug 'deadlift' for beyondthewhiteboard.com/.../35-deadlift).
Inputs: memberId (required), movementId (required), movementSlug (required), days.
get_workout_sessionA
Get the full details of one already-logged BTWB result by its session ID (the number in a beyondthewhiteboard.com/workout_sessions/{id} URL): workout name, performed date/time, the movements/sets, the result/score, and level/WOD-rank stats. There's no search-by-date endpoint yet - you need the session ID already (e.g. from a URL, or from log_workout's redirectedTo field).
Inputs: sessionId (required).
log_rounds_workoutA
Log a multi-movement 'rounds' result (e.g. a For Time WOD with several movements per round) to BTWB - as opposed to log_workout, which only handles a single movement. Only 'For Time' workouts scored by total time are supported (other scoring types like AMRAP/total-reps are untested). workoutId/workoutSlug come from the workout's URL (beyondthewhiteboard.com/workouts/{workoutId}-{workoutSlug}/...). Every entry logged through this tool is always posted with Privacy: Only Me - this is hardcoded and cannot be overridden.
Inputs: rxd (required), memberId (required), sections (required), workoutId (required), +5 more.
log_workoutA
Log a single-movement result (e.g. a 1-rep max) to BTWB. Every entry logged through this tool is always posted with Privacy: Only Me - this is hardcoded and cannot be overridden.
Inputs: reps (required), weight (required), movementId (required), movementName (required), +3 more.
refresh_session_cookieA
Manually re-authenticate to BTWB and replace the stored session cookie with a fresh one. Every other tool already does this automatically when it detects an expired session, so you normally don't need to call this directly - it's mainly useful to proactively refresh, or to test that BTWB_EMAIL and the Keychain-stored password are set up correctly. Requires BTWB_EMAIL and a password stored in Keychain (service: btwb-password) - see README "Automatic cookie refresh".
Inputs: no named parameters.
search_movementA
Search BTWB's movement library by name (e.g. 'Deadlift', 'Squat Clean'). Returns matching movements with their numeric IDs, which log_workout and get_movement_history both require.
Inputs: term (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 7 tools
Each tool targets a distinct resource and action: search_movement (library lookup) vs get_movement_history (per-set history), log_workout (single movement) vs log_rounds_workout (multi-movement rounds), get_workout_session (read) vs delete_workout_session (remove), plus a standalone auth utility. Descriptions proactively clarify overlaps, e.g. log_rounds_workout explicitly contrasts itself with log_workout.
All seven tools use consistent snake_case verb_noun phrasing (search_movement, log_workout, get_workout_session, delete_workout_session, refresh_session_cookie). No style mixing or vague verb-only names.
Seven tools is well-scoped for a workout-logging and movement-data integration; each tool earns its place with a clear role and no redundant entries.
The surface covers search, log (two variants), read, and delete, but has no update/edit for an already-logged result, and get_workout_session requires a known session ID with no search-by-date or list-sessions tool. Discovery of existing sessions/workouts is thus a notable gap.
Maintenance
Related MCP Connectors
Create Hevy routines and analyze your training from chat. Unofficial; BYO Hevy PRO API key.
Workout journal Claude and ChatGPT write to: log workouts, get a program, see your progress.
Talk to your own gym log. Reps: Gym Workout Log (repsworkout.com) is a workout log for iPhone and Android. Connect it to Claude, ChatGPT or any MCP client to read your workout history, PRs, lift progression, weekly summaries, routines and training plan, and to save routines and plans after you confirm them. It never logs a workout for you and can’t delete your history. Sign in with an e-mail code, Apple or Google; new users can create a free account while connecting, then get the app to log workouts.
Connect your PRzilla account through OAuth to search CrossFit and HYROX workouts, read benchmark targets and scaling notes, and look up movement technique. Three read-only catalog tools; no access to personal training history or score logging. Setup: https://github.com/kangax/przilla-mcp
Related MCP Servers
- AlicenseBqualityDmaintenanceIntegrates with the Boostcamp fitness platform to provide workout history, programs, exercises, and analytics via natural language.12MIT
- AlicenseNot gradedqualityBmaintenanceEnables LLMs to read and write Hevy workout tracking data, including workouts, routines, exercise templates, per-exercise history, and body measurements through all 15 Hevy public API endpoints.16 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants to plan, build, and log calisthenics workouts in a user's own account, including searching exercises, managing custom workouts and plans, reading progress and coaching data, and logging completed sessions. Account mutations require explicit confirmation, and workouts can be displayed as interactive cards in the chat.MIT
- FlicenseNot gradedqualityAmaintenanceConnects AI assistants to a TrainingPeaks account via secure cookie authentication—no API approval required—so users can query and build workouts, schedule and copy sessions, manage calendar events and notes, log health metrics, and review CTL/ATL/TSB fitness trends, power PRs and zone settings through natural language. It also covers strength workouts, equipment, workout libraries, multi-week training plans, and coach features such as athlete groups.-