API & MCP. Build on top of Workout Chat.

A remote MCP server you can plug into Claude, ChatGPT, Cursor, and other AI clients — plus a simple JSON REST API for workouts, exercises, weigh-ins, injuries, and goals. OAuth for MCP, Bearer tokens for REST, no versioning ceremony either way.

MCP server

Workout Chat speaks the Model Context Protocol. Add it to any MCP-capable client and the AI you're already talking to can log your workout, record a weigh-in, check on an injury, set a goal, or look up an exercise — using your Workout Chat account. Same tools the in-app assistant uses.

Endpoint

https://workout.chat/mcp

Streamable HTTP transport. Protocol version 2025-03-26.

Authentication

OAuth 2.1 with PKCE and dynamic client registration (RFC 7591). The first time your client connects, it'll open a browser tab to log into Workout Chat and approve access — there's no API key to copy or rotate. The same tokens work for the REST API.

Available tools

  • log_workout — create a workout from natural-language exercises
  • list_workouts — recent sessions, paginated and date-filterable
  • update_workout · update_exercise — fix titles, notes, sets/reps/weight
  • weighin · update_profile — log bodyweight, change unit system
  • analyze_progress · lookup_exercise — trends, PRs, canonical exercise lookup
  • log_injury · update_injury · checkin_injury — track and update injuries
  • set_goal · list_goals · abandon_goal — capture and review training targets

Set up in Claude Desktop

In Settings → Connectors → Add custom connector, paste:

https://workout.chat/mcp

Or, if you prefer the config file (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):

{
  "mcpServers": {
    "workout-chat": {
      "url": "https://workout.chat/mcp"
    }
  }
}

Restart Claude. The first message that touches Workout Chat will pop a browser tab for OAuth — log in, approve, and you're connected.

Set up in ChatGPT

On a plan that supports MCP connectors: Settings → Connectors → Add MCP server. Server URL:

https://workout.chat/mcp

ChatGPT will run the OAuth flow on first use.

Set up in Cursor, Cline, Continue, Goose, etc.

Drop the same JSON snippet into your client's MCP config (the file path varies — check your client's docs):

{
  "mcpServers": {
    "workout-chat": {
      "url": "https://workout.chat/mcp"
    }
  }
}

Discovery URLs

For clients that auto-discover OAuth metadata:

/.well-known/oauth-protected-resource/mcp   # RFC 9728
/.well-known/oauth-authorization-server     # RFC 8414
/.well-known/mcp/server-card.json           # SEP-1649 server card

Try it

Once connected, ask your AI things like:

"Log bench 3x8 at 185, OHP 3x10 at 95 to my workout chat"
"What did I train this week?"
"Set a goal to bench 225 by July"
"My left shoulder felt tight after OHP — log that"
"How am I progressing on squat?"

REST API. Build your own client.

Bearer-token JSON for everything in the app — workouts, exercises, weigh-ins, injuries, goals, and live chat streaming. Same auth as MCP; if you've already connected, the token works here too.

Getting started

Base URL

https://workout.chat/api

Authentication

Every request requires an Authorization: Bearer <token> header. Tokens are issued through the same OAuth 2.1 flow the MCP server uses — if you've already connected via Claude Desktop / ChatGPT / Cursor, that token works here too. Visit /.well-known/oauth-authorization-server for discovery.

curl https://workout.chat/api/profile \
  -H "Authorization: Bearer YOUR_TOKEN"

Response shape

Every successful response wraps its payload in data:

{
  "data": { ... }        // single resource
}

{
  "data": [ ... ],       // list
  "meta": {
    "total":    142,
    "page":     1,
    "per_page": 50
  }
}

Errors

{
  "error": {
    "code":    "validation_failed",
    "message": "Name can't be blank",
    "details": { "name": [{"error": "blank"}] }
  }
}

Standard HTTP status codes: 401 unauthorized, 404 not found, 422 validation failed, 400 bad request. Resources scoped to another user return 404 rather than 403.

Pagination

List endpoints accept ?page=N&per_page=M. Defaults: page=1, per_page=50. Max per_page=200.

Index / ping

GET /api

Returns API metadata and the authenticated user. Useful as a "is my token working?" ping.

{
  "name": "Workout Chat API",
  "docs_url": "https://workout.chat/api_docs",
  "authenticated_as": { "id": 1, "email": "you@example.com", "name": "…" },
  "resources": {
    "profile":  "/api/profile",
    "stats":    "/api/stats",
    "weighins": "/api/weighins",
    "workouts": "/api/workouts",
    "injuries": "/api/injuries"
  }
}

Stats

GET /api/stats

Aggregated, high-level KPIs for the authenticated user — workout counts across rolling windows, exercise totals, injury status breakdown, latest weigh-in, and top personal records. Useful for building your own dashboard without walking every resource.

{
  "data": {
    "workouts": {
      "total":              142,
      "last_7_days":        4,
      "last_30_days":       15,
      "last_90_days":       42,
      "first_performed_at": "2025-09-14T00:00:00Z",
      "last_performed_at":  "2026-04-18T17:30:00Z"
    },
    "exercises": {
      "total":        920,
      "unique_names": 48,
      "last_30_days": 120
    },
    "weighins": {
      "total":  22,
      "latest": { "date": "2026-04-18", "weight_kg": 81.2 }
    },
    "injuries": {
      "total":      2,
      "active":     1,
      "recovering": 1,
      "healed":     0
    },
    "personal_records": {
      "count": 15,
      "top": [
        { "name": "Deadlift",    "weight_kg": 142.88 },
        { "name": "Squat",       "weight_kg": 136.08 },
        { "name": "Bench Press", "weight_kg": 83.91 }
      ]
    }
  }
}

Profile

GET /api/profile

Returns the authenticated user's profile.

PATCH /api/profile

Updates name, email, unit system (metric / imperial), birth year, height, or gender.

curl -X PATCH https://workout.chat/api/profile \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"profile": {"system": "metric", "height_cm": 178}}'

Weigh-ins

GET /api/weighins

List weigh-ins, newest first. Filter with ?since=YYYY-MM-DD and/or ?until=YYYY-MM-DD.

POST /api/weighins

weight_kg is required. date defaults to today if omitted.

curl -X POST https://workout.chat/api/weighins \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"weighin": {"date": "2026-04-18", "weight_kg": 84.1}}'
DELETE /api/weighins/:id

Workouts

GET /api/workouts

List workouts, most recent first. Supports ?since= / ?until= date filters. Each workout includes its exercise_count.

GET /api/workouts/:id

Returns the workout with its full list of exercises nested under exercises.

POST /api/workouts

performed_at defaults to the current time if omitted. Accepts title, notes, performed_at, raw_text.

curl -X POST https://workout.chat/api/workouts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "workout": {
      "title": "Upper Body Push",
      "notes": "Bench focus day",
      "performed_at": "2026-04-18T17:30:00Z"
    }
  }'
PATCH /api/workouts/:id
DELETE /api/workouts/:id

Exercises

GET /api/workouts/:workout_id/exercises

List exercises on a workout, in order.

POST /api/workouts/:workout_id/exercises

Appends an exercise to a workout. position auto-assigns to the next slot if omitted. raw_line defaults to the exercise name when blank. Accepts name, sets, reps, weight_kg, entered_weight, entered_weight_unit, duration_seconds, distance_km, kind, equipment, muscle_area, notes, position, raw_line.

curl -X POST https://workout.chat/api/workouts/123/exercises \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "exercise": {
      "name": "Bench Press",
      "sets": 3,
      "reps": 8,
      "entered_weight": 185,
      "entered_weight_unit": "lbs"
    }
  }'
GET /api/exercises/:id
PATCH /api/exercises/:id
DELETE /api/exercises/:id

Injuries

GET /api/injuries

List injuries, newest first. Filter with ?status=active, recovering, or healed.

GET /api/injuries/:id

Returns the injury with its check-in history nested under checkins.

POST /api/injuries

occurred_on defaults to today if omitted. severity defaults to moderate, status defaults to active.

curl -X POST https://workout.chat/api/injuries \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "injury": {
      "name": "Left shoulder impingement",
      "body_area": "shoulder",
      "side": "left",
      "injury_type": "impingement",
      "severity": "moderate",
      "exercises_to_avoid": ["OHP", "Lateral Raise"]
    }
  }'
PATCH /api/injuries/:id
DELETE /api/injuries/:id

Chat

The chat loop is a two-step flow: you POST a user message to enqueue it, and the assistant's tokens stream back over a WebSocket as the job runs. Same pipeline the web app uses.

Send a message

POST /api/chat/messages

Creates the user message and enqueues the AI turn. Returns 202 Accepted immediately — listen on the WebSocket for the streamed response.

curl -X POST https://workout.chat/api/chat/messages \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message": {"content": "bench 3x8 at 185, OHP 3x10 at 95"}}'
{
  "data": {
    "id":         123,
    "role":       "user",
    "content":    "bench 3x8 at 185, OHP 3x10 at 95",
    "created_at": "2026-04-18T17:30:00Z"
  },
  "meta": {
    "status":       "queued",
    "channel":      "ChatChannel",
    "stream_scope": "user"
  }
}

Stream responses over WebSocket

Connect to the ActionCable WebSocket and subscribe to ChatChannel. Authenticate by passing your bearer token as a query param:

wss://workout.chat/cable?token=YOUR_TOKEN

Once connected, send the standard ActionCable subscribe frame:

{
  "command":    "subscribe",
  "identifier": "{\"channel\":\"ChatChannel\"}"
}

The server pushes three kinds of payloads as the job runs. Token frames arrive as the model streams; a single done frame signals completion.

{ "type": "token", "content": "Logged " }
{ "type": "token", "content": "2 exercises." }
{ "type": "done" }

// on failure:
{ "type": "error", "content": "Something went wrong." }

The channel is scoped to the authenticated user — you'll only receive your own chat's stream. Full typical flow from an iOS client:

1. Open WebSocket to wss://workout.chat/cable?token=$TOKEN
2. Send the `subscribe` frame above
3. POST /api/chat/messages with the user's input
4. Concatenate incoming `token` payloads into the assistant bubble
5. When `done` arrives, finalize the bubble (render markdown, etc.)

Injury check-ins

GET /api/injuries/:injury_id/checkins

List check-ins on an injury, newest first.

POST /api/injuries/:injury_id/checkins

Log a check-in. date defaults to today and status defaults to "same" if omitted. If status is "healed" the parent injury auto-updates to healed and records healed_on. If "improving" and the injury is still active, it moves to recovering.

curl -X POST https://workout.chat/api/injuries/42/checkins \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "checkin": {
      "date": "2026-04-18",
      "severity": 4,
      "sensation": "tightness",
      "status": "improving",
      "notes": "Feels 80% better"
    }
  }'