Skip to main content
Glama
 ██████╗ ██╗   ██╗███╗   ███╗
██╔════╝ ╚██╗ ██╔╝████╗ ████║
██║  ███╗ ╚████╔╝ ██╔████╔██║
██║   ██║  ╚██╔╝  ██║╚██╔╝██║
╚██████╔╝   ██║   ██║ ╚═╝ ██║
 ╚═════╝    ╚═╝   ╚═╝     ╚═╝

A barbell and macro log your chat client can actually use.

gym is an MCP server with a deliberately small web front end. Routines, workouts, sets, food, water — all of it logged by talking to Claude (or any MCP client) instead of tapping through a tracker app between sets.


Why this exists

Every training app asks you to stop what you are doing and operate a UI. Mid-set, out of breath, hands chalked, that is real friction — and friction is why logs die three weeks in. Same story with food: nobody wants to search a database for "chicken thigh, roasted" while dinner gets cold.

The chat client is already open. "bench, 3 more at 225" is one sentence. "how much protein do I have left today?" is one question. Both should be one tool call, not a screen.

So the tracker became a server and the interface became a conversation. There are 21 tools, an OAuth 2.0 flow so connecting a client is safe, and a web app that does exactly two things: sign you in, and let you approve a client. Everything else happens in chat.

Related MCP server: Workout Planner Ai MCP

What it can do

Routines — the program, and its planned days.

Tool

create_routine

Create a training program

get_routines

List programs, with their sessions

delete_routine

Remove a program; logged workouts survive and unlink

create_routine_session

Add a planned day (Push, Pull, Legs) with its exercises

get_routine_sessions

Read planned days and their exercise order

delete_routine_session

Remove a planned day

Workouts — what was actually trained.

Tool

start_workout_session

Open a live session, optionally from a routine day

append_set_entry

Log one completed set — 3x8 is three calls, not one

end_workout_session

Close it out

get_workout_sessions

Recent sessions with their sets

get_exercises

Search the exercise catalogue

get_exercise_history

Every recent set of one lift, newest first — call before suggesting a weight

Nutrition — food, water, targets, and the calendar day they belong to.

Tool

set_timezone

Decide where the user's day starts and ends

set_macro_targets / get_macro_targets

Daily calorie and macro goals

log_food_entry

One item per call, macros estimated when not stated

get_food_entries / delete_food_entry

Read and correct the day's log

log_water_entry

Track intake

get_macro_summary

Eaten vs. remaining, broken down by meal — the "how am I doing today" tool

Plus ping, which exists to prove a connection is live.

The hard part: "today"

This sounds trivial and is not. The server runs in UTC. Postgres agreed it was 2026-08-23 01:12+00 while the person logging dinner was standing in New York at 21:12 on the 22nd. Log that meal against now() and it lands on tomorrow — the single most annoying way a food tracker can lie to you.

Two servers in two regions would disagree about the same meal. NOW() is not an answer, and neither is the server's local clock.

So the day is resolved at write time, in the user's own zone:

  • users.timezone holds an IANA name (America/New_York), set once via set_timezone.

  • Every entry stores both logged_at timestamptz (the instant) and logged_on date (the calendar day it counts towards, computed through Intl.DateTimeFormat in that user's zone).

  • Reads filter on logged_on, never on a timestamp range, over (user_id, logged_on) indexes.

An 11pm snack counts towards that night. A 12:05am snack counts towards the new day. The logic is checked against both 2026 DST seams, including the 01:30 that happens twice in November.

New accounts default to UTC, which is a placeholder rather than a real answer — nutrition tools return a timezone_warning until it is set, so the model knows to ask.

Connecting a client

The server speaks streamable HTTP and advertises its own OAuth metadata, so any MCP client that supports remote servers can discover the rest on its own. All you ever hand it is the /mcp URL.

Claude (web or desktop)Settings → Connectors → Add custom connector:

Field

Value

Name

Anything. Gym App

URL

https://gym-api.skylerx.ir/mcp — your HOSTED_API_URL with /mcp on the end

Hit Continue. Claude calls the server, gets a 401 pointing at the authorization metadata, registers itself, and opens a browser tab. Sign in with Discord, approve the connection on the consent screen, and the tools show up in the connector list.

Claude Code

claude mcp add --transport http gym https://gym-api.skylerx.ir/mcp

Same flow — the first call triggers the browser sign-in.

Notes:

  • The path matters. https://host alone will not work; it has to be https://host/mcp.

  • Hosted servers need real HTTPS. For local development http://localhost:8888/mcp is fine.

  • Nothing is granted until you click approve, and the consent screen shows which client asked and where its codes will be sent.

What happens under the hood

Two identities, never confused. A session cookie is the human, browser only. A bearer token is the client, API only — and it only exists after that human clicked approve.

sequenceDiagram
    participant C as MCP client
    participant A as API (Hono)
    participant B as Browser
    participant D as Discord

    C->>A: POST /mcp (no token)
    A-->>C: 401 + WWW-Authenticate → metadata
    C->>A: POST /oauth/register
    C->>B: open /oauth/authorize?…PKCE
    A-->>B: 302 /login?return_to=…
    B->>D: sign in
    D-->>A: callback → session cookie
    A-->>B: 302 back to /authorize
    A-->>B: 302 /consent?request_id=…
    B->>A: approve
    A-->>C: redirect_uri?code=…
    C->>A: POST /oauth/token (+ verifier)
    A-->>C: access_token

Details worth knowing:

  • PKCE (S256) throughout, with dynamic client registration, advertised at /.well-known/oauth-authorization-server.

  • Nothing is granted until consent. /authorize parks the request in Redis for ten minutes; only the approve route mints a code.

  • Consent is bound to the account that started it. Approve and deny are GET navigations, because a SameSite=Lax cookie will not travel on a cross-site POST — so each pending request records who created it and refuses a different session. An attacker can start a flow; only their own account can ever consent to it.

  • return_to is origin-checked on both sides, so an interrupted /authorize can resume without becoming an open redirect.

  • One MCP server per request. Tools close over the authenticated user, so nothing leaks between callers.

Stack

API

Hono 4 · @hono/mcp streamable HTTP · Node

MCP

@modelcontextprotocol/sdk 1.30

Data

Postgres 17 · Drizzle ORM 1.0-rc · Redis 7 for OAuth state, codes and pending consent

Web

Next.js 16 (App Router, RSC) · Tailwind 4 · Biome

Auth

OAuth 2.0 + PKCE for clients · Discord OAuth for humans

Quick start

pnpm install
docker compose up -d                  # postgres :5432, redis :6379

cp apps/backend/.env.example apps/backend/.env   # then fill in Discord creds
pnpm --filter backend db:push

pnpm --filter backend dev             # :8888
pnpm --filter www dev                 # :3000

Open http://localhost:3000, sign in, and point an MCP client at http://localhost:8888/mcp.

Uses pnpm throughout. pnpm dlx in place of npx.

Environment

apps/backend/.env

Variable

NODE_ENV

development | testing | production

PORT

Port to listen on

DATABASE_URL

Postgres connection string

REDIS_CONNECTION_URI

Redis connection string

HOSTED_API_URL

Public origin of this API. Every OAuth URL it advertises is built from this

FRONTEND_URL

Public origin of the web app. Login and consent redirects go here

COOKIE_DOMAIN

Optional. Scope for the session cookie, e.g. .example.com. Required when the API and the site are on different hostnames

DISCORD_CLIENT_ID / DISCORD_CLIENT_SECRET

From the Discord developer portal

DISCORD_REDIRECT_URI

${HOSTED_API_URL}/auth/discord/callback, and registered verbatim in that portal

apps/www/.env.local

Variable

NEXT_PUBLIC_API_URL

Must equal the backend's HOSTED_API_URL

NEXT_PUBLIC_SITE_HOST

Optional. Hostname this app is served from when it is not localhost — Next blocks cross-origin dev asset requests without it. Host only, no scheme

Layout

apps/
  backend/
    src/
      routes/       auth · oauth · well-known
      middleware/   is-authenticated (cookie) · mcp-gate (bearer)
      services/     auth · tool-manager · nutrition · ownership
      tools/        one file per MCP tool, registered in index.ts
      utils/        day · urls · cookies · env · mcp · format
      db/           schema + drizzle client
  www/
    src/app/        / · /login · /consent
    src/lib/        api · session

Adding a tool: write the file, export a defineTool({...}), add one line to tools/index.ts. The registry is explicit on purpose — the compiler checks every entry, and registration stays synchronous.

Deploying

The API and the site can live on separate hosts, but the session cookie has to reach both. Put them on subdomains of one domain you own and set COOKIE_DOMAIN to the shared parent:

gym-api.skylerx.ir   →  HOSTED_API_URL
gym.skylerx.ir       →  FRONTEND_URL
COOKIE_DOMAIN=.skylerx.ir

Two unrelated hostnames still complete the OAuth flow — every step that needs the cookie happens on the API host — but the site can never show a signed-in state, so its "already signed in, skip the login page" check goes dead.

Secure on the session cookie follows HOSTED_API_URL's scheme, so serving the API over https is enough; there is no separate flag to remember.

Not done yet

  • Refresh grant validates a differently-derived hash than the one it stored, so token renewal fails. Access tokens last an hour.

  • No sign-out route.

  • Google sign-in was removed; Discord is the only provider.

  • No editing of a logged set, and no personal-record tool.

  • The frontend never reports the browser's timezone at sign-up — set_timezone is the only path.

F
license - not found
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    A
    quality
    B
    maintenance
    Personal workout coach MCP server that logs exercises in natural language, tracks progress with SQLite, and provides coaching signals like estimated 1RM and volume trends.
    6
  • A
    license
    Not graded
    quality
    B
    maintenance
    Self-hosted fitness tracking MCP server that gives AI assistants access to your nutrition, training, weight, sleep, and accomplishment data via 77 tools and 5 resources. Enables natural-language logging and querying of personal health metrics through Claude or ChatGPT.
    1
    BSD 2-Clause "Simplified"
  • F
    license
    Not graded
    quality
    C
    maintenance
    Offline MCP server for logging workouts, tracking dietary macros, and retrieving daily summaries, all stored in local SQLite. Enables fitness tracking via natural language in MCP-compatible clients.

View all related MCP servers

Related MCP Connectors

  • Create Hevy routines and analyze your training from chat. Unofficial; BYO Hevy PRO API key.

  • Manage clients, plans, sessions, habits, and billing on Trainzilla via one-click OAuth.

  • Log, query, and edit expenses, budgets, and accounts in Manilo from any MCP-compatible AI assistant.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SklyerX/gym-app-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server