everfit-mcp
MCP server + API client for Everfit, from the client side (not coach). Reads your assigned training sessions, workout detail, and profile.
Unofficial. Reverse-engineered from iOS app traffic (v3.88.1); Everfit can change or block these endpoints at any time.
Install
Needs Node >= 20. Nothing else — no Python, no build step.
npx everfit-mcp # run the MCP server over stdio
npx -p everfit-mcp everfit sessions # or the CLI
Not published yet? Three ways to install it elsewhere without a registry:
npm pack # produces everfit-mcp-<version>.tgz
npm i -g ./everfit-mcp-0.0.3.tgz # copy the tarball to any machine
npx github:mistersimon/everfit-mcp # or install straight from the git repo
git clone https://github.com/mistersimon/everfit-mcp.git && npm install # for development
Claude Code
npx -p everfit-mcp everfit login --save-password # once, prompts for your password
claude mcp add everfit -- npx -y everfit-mcp # no credentials in the config
Claude Desktop / Cursor / any MCP client
Run npx -p everfit-mcp everfit login --save-password once, then in
claude_desktop_config.json (or the equivalent):
{
"mcpServers": {
"everfit": {
"command": "npx",
"args": ["-y", "everfit-mcp"]
}
}
}
Credentials come from the stored session, so nothing secret goes in this file. Env vars still work if you prefer them — see Credentials.
Hosting it elsewhere
What a host needs to run the MCP server: Node >= 20, the ability to spawn a child process and talk
MCP over stdio, per-server env vars, and outbound HTTPS to api-prod4.everfit.io. No inbound
port, no writable disk. Any agent host meeting that runs it with npx -y everfit-mcp and two env
vars, exactly like Claude Code.
Two cases that need more than a config entry:
- Host only accepts remote (HTTP) MCP servers. This ships stdio only. The SDK's streamable-HTTP transport is a small addition, but an HTTP endpoint holding your Everfit credentials needs its own auth — don't expose it unauthenticated.
- Runtime without Node (React Native / Hermes, browsers, Cloudflare Workers, Deno).
src/server.mjsandsrc/cli.mjsare Node-only: child process,node:fs, stdio. Butsrc/client.mjs(everfit-mcp/client) is dependency-free and uses only web-standard globals (fetch,URL,URLSearchParams,atob,Intl) — noprocess, noBuffer— so you can import it directly and skip MCP.npm testasserts this by loading it withprocessandBufferdeleted.
Credentials
Everfit has no API keys, so auth is your app email and password. Two ways to supply them.
Recommended: everfit login
npx -p everfit-mcp everfit login --save-password
Prompts for email and password (password is not echoed) and writes a session to
~/.config/everfit/session.json, mode 600. Then the MCP config needs no credentials at all:
{ "mcpServers": { "everfit": { "command": "npx", "args": ["-y", "everfit-mcp"] } } }
everfit whoami shows who is logged in and when the token expires; everfit logout deletes the file.
--save-password stores the password alongside the token so the session renews itself indefinitely.
Without it only the token is stored, and you re-run everfit login when it expires (~10 days).
Either way the secret sits in one 600 file you own rather than in an MCP client config, and it
never passes through the model.
Or environment variables
Env always wins over the stored session, which is what you want for containers and CI.
Credentials are never accepted as tool arguments. A token passed per call would sit in the model's
context and in transcripts; keeping auth in the process is the reason there is no login tool,
only a login command.
Statelessness and token expiry
Nothing is written to disk, ever. A token is treated as a cache of a login, not as state:
- With email + password the client is effectively stateless. It holds a token in memory for the
process lifetime, re-logs-in automatically ~5 minutes before the token's
exp, and also renews once on an unexpected401(revoked token, changed password, clock skew). Restart the process, or run it in a fresh container, and it simply logs in again. Nothing to rotate or refresh by hand. - With
EVERFIT_TOKENalone there is no way to renew, so it stops working ~10 days after issue and fails withtoken expired and no credentials to renew it. Fine for a shell session or CI run; wrong for a long-lived MCP server install.
The token is a JWT carrying profileId (your client id) and exp, so the client reads both
locally and needs no extra configuration and no probe request. Everfit's login response does
include a refresh_token, but no refresh endpoint appears in the captured traffic — re-login is
the refresh path, which is why the password is the durable credential.
Whichever you choose, the secret is plaintext at rest. everfit login at least confines it to
one 600 file instead of an MCP config that other tools read, and keeps it out of shell history.
On a shared box, anyone who can read that file has your Everfit account. Never bake credentials
into a container or machine image — pass them at run time. Where you can live with the 10-day
ceiling, login without --save-password (or a bare EVERFIT_TOKEN) limits the blast radius; a
long-lived unattended install needs the password to keep working.
MCP tools
log_activity and log_body_metric are the only tools that write. Its description tells the model to confirm details
first and not to invent metrics, but that is a prompt, not a guarantee — an agent can create real
entries in your training log. There is deliberately no delete tool, so nothing an agent does is
destructive; remove a mistaken entry yourself with everfit delete-activity <id> (the tool returns
the exact command). Drop the log_activity registration from src/server.mjs if you want a
strictly read-only server.
CLI
everfit login [--save-password] # store a session the MCP server reads
everfit whoami # who is logged in, when the token expires
everfit logout # delete the stored session
everfit sessions [start] [end] # workouts, YYYY-MM-DD, default -7d..+14d
everfit days [start] [end] # raw day-by-day payload, includes rest days
everfit workout <assignment_id> # one workout, flattened
everfit workout <assignment_id> --raw # untouched API payload
everfit profile
everfit get <path> [k=v ...] # any endpoint, e.g. get habits/client-today
get is the escape hatch for exploring endpoints this package does not wrap yet.
Logging a cardio session
Cardio and other non-assigned activity is a log activity, a different object from a coach's
workout assignment. everfit log creates one, the same as "+ Activity" in the app:
everfit activities # popular activity types
everfit activities run # search: Run, Trail Run, ...
everfit log Run --duration 46 --distance 7.22km --hr 126
everfit log Run --start "2026-08-12T08:55" --end "2026-08-12T09:41" --distance 4.5mi --dry-run
everfit activity <activity_id> # read one back
everfit edit <activity_id> --duration 46 --distance 7.22km
everfit edit <activity_id> --no-hr --note "felt easy" # --no-<metric> clears one
everfit delete-activity <activity_id> # remove it
everfit metrics # body metrics you can log, with their units
everfit weigh 82.8 # shorthand for: metric weight 82.8
everfit metric weight 82.8 --unit lb --date 2026-09-04
everfit metric steps 9432
--durationis minutes. With no--start, the session is assumed to have just ended.- Metric flags:
--distance --hr --calories --elevation --cadence --power --reps. A bare number uses the field's default unit (miles for distance), so pass7.22kmif you mean km. - Each activity type accepts only some metrics — Run takes distance, avg_heart_rate and calories, and anything else is rejected with the list it does accept rather than being silently dropped.
--dry-runprints the payload without sending it.metric/weightake the metric's own unit unless you pass--unit; a metric with several units and no stored preference asks for one rather than guessing.--dateacceptsYYYY-MM-DD(logged at local noon) or a full ISO time. There is no delete — remove a bad entry in the app.editmerges: the API replaces the whole record, so the client reads the current activity first and carries over anything you did not pass. Clear a single metric with--no-hr,--no-distanceand so on.ratingcannot be changed — the API accepts the field and ignores it (verified), so the existing value is preserved.
Library
The package entry point is the MCP server, since that is the primary artifact. The API client is a subpath import, and needs no MCP:
import { createClient } from "everfit-mcp/client";
const everfit = createClient(); // or { email, password, token, clientId, timezone, baseUrl }
const sessions = await everfit.listTrainingSessions({ start_date: "2026-08-01" });
const detail = await everfit.getTrainingSession(sessions[0].assignment_id);
const habits = await everfit.get("habits/client-today"); // raw GET, any endpoint
Also exported from everfit-mcp/client: decodeToken, mdy, setStr, sections, statusName,
summarizeAssignment, EverfitError (carries .status, .path, .body). Importing
everfit-mcp gives you the MCP server object and its configured everfit client; it only
speaks stdio when run as a script, so importing it is side-effect free.
Layout
src/server.mjs MCP server (package entry point, `everfit-mcp` bin)
src/client.mjs Everfit API client, no deps, no Node built-ins
src/store.mjs session file handling (Node-only, kept out of the client)
src/cli.mjs `everfit` bin: login/whoami/logout and direct endpoint access
test/index.mjs npm test
Notes from the captured traffic
- Auth:
POST /api/auth/login/form-encoded, token goes in thex-access-tokenheader (notAuthorization), alongsidex-app-type: ios-client. A 401 triggers one re-login and retry. - Dates: the API wants
MM-DD-YYYY. This client acceptsYYYY-MM-DDand converts. - Set values are strings and reps can be ranges (
"4-6"), so sets render as text:160kg x5 @RPE7 rest120. - Assignment
status: 0 and 2 confirmed (not_started,completed); 1 (in_progress) is inferred. Unknown codes pass through as numbers. - Body metrics are a third object again:
POST /api/body-metric-entry/add-entrywith{client, date, unique_code, value, unit}.unique_codeselects the metric ("weight"),unitis a per-metric ObjectId — there is no global unit table, so both come fromGET /api/body-metric/get-full-list-client-body-metric?clientId=. The server derivestypeand theMM-DD-YYYYdaybucket fromdate, so a date-only input is sent at local noon rather than midnight. No delete endpoint appears in the captured traffic; entries have to be removed in the app. The catalog returns every metric the workspace defines (36 on the captured account, including coach-added ones keyed<name>_<teamId>), which is a superset of what the app shows. - Logged activities (cardio) are a separate object from workout assignments:
POST /api/log-activitieswith{log_activity_library, tracking_fields, start_time, end_time, icon, background_icon_color}. Duration is implied by the timestamps — there is no duration field.tracking_fieldsentries are{log_activity_field, unit, value}id triples, and the valid field and unit ids for an activity come fromGET /api/log-activity-libraries/:id. - Future workouts are ordinary assignments: same endpoint,
status: not_started, full prescribed sets, emptylogged. There is no separate "upcoming" endpoint and no server-side cap on how far ahead you can query — you only get days your coach has actually scheduled, so a range past the end of the assigned program returns an empty list rather than an error. get_profilestrips thefirebase_tokenthe endpoint returns, so credentials stay out of model context.
Development
npm test # offline: response shaping, token/expiry logic, session store, prompts
node src/cli.mjs login # run the CLI from a clone without installing
claude mcp add everfit-dev -- node "$PWD/src/server.mjs" # register the working tree
If you capture your own traffic with mitmproxy, remember the flow file contains your plaintext
password and a live token. .gitignore excludes *.flows and *.har; keep it that way.
License
MIT — see LICENSE.