A Model Context Protocol (MCP) server designed to maintain a comprehensive workout registry and provide powerful query capabilities for AI agents. This server enables structured storage and retrieval of workout data imported from CSV files generated by the Hevy workout tracking application.
The Workout MCP Server acts as a centralized data hub for fitness tracking, allowing AI agents to query workout history, analyze training patterns, track personal records, and provide intelligent insights based on structured exercise data. The system combines a REST API for data ingestion with MCP tools for programmatic access.
- CSV Import: Import workout data from CSV files exported from the Hevy application
- On-Demand Sync: Trigger Hevy synchronization on demand — see Sync Endpoint
- Standardized data ingestion pipeline that normalizes and validates workout records
- Structured exception handling with field-level validation errors (422), constraint violations (409), and safe internal error responses (500)
- Request logging middleware with
X-Request-IDpropagation and structured JSON/console logging viastructlog
The server exposes the following query tools to enable AI agents to interact with workout data:
| Tool | Title | Description |
|---|---|---|
get_workout_by_date_range |
Get Workouts by Date Range | Retrieve all workout sessions that occurred between two dates. Use this when the user asks about workouts during a specific time period, such as "last week" or "this month". |
get_workout_by_routine |
Get Workouts by Routine | Retrieve all workouts that follow a specific routine template. Use this when the user asks about a named workout plan like "Push Day" or "Upper Body". |
get_workout_by_exercise |
Get Workouts by Exercise | Retrieve all workout sessions that include a specific exercise. Use this when the user asks about training history for a particular movement like "Bench Press" or "Squat". |
get_workout_count |
Count Workouts | Count the total number of workouts, optionally filtered by date range or routine name. Use this when the user asks "how many workouts" or wants to check training frequency. |
get_min_pr_by_exercise |
Get Min PR by Exercise | Find the lightest best-set weight recorded for an exercise. Use this when the user asks for their lightest PR or wants to see early performance data. |
get_max_pr_by_exercise |
Get Max PR by Exercise | Find the heaviest single set ever recorded for an exercise. Use this when the user asks for their personal record, max weight, or heaviest lift. |
get_last_workout |
Get Last Workout | Retrieve the most recent workout session. Optionally filter by a specific exercise to find when it was last performed. Use this when the user asks "what was my last workout" or "when did I last do X". |
The application uses a relational database schema designed to capture the hierarchical nature of workout tracking:
erDiagram
ROUTINE {
int id
string name
}
WORKOUT {
int id
datetime start
datetime end
int routine_id
}
EXERCISE {
int id
string name
}
WORKOUT_EXERCISE {
int id
int workout_id
int exercise_id
int exercise_index
}
SET {
int id
int workout_exercise_id
int set_index
int reps
float weight
float rpe
}
ROUTINE ||--o{ WORKOUT : contains
WORKOUT ||--o{ WORKOUT_EXERCISE : includes
EXERCISE ||--o{ WORKOUT_EXERCISE : appears_in
WORKOUT_EXERCISE ||--o{ SET : has
-
ROUTINE: Represents a predefined workout template (e.g., "Push Day", "Leg Day")
id: Primary keyname: Routine name
-
WORKOUT: Represents an individual workout session
id: Primary keystart: Session start timestampend: Session end timestamproutine_id: Foreign key to ROUTINE
-
EXERCISE: Master list of exercises
id: Primary keyname: Exercise name (e.g., "Bench Press", "Squat")
-
WORKOUT_EXERCISE: Junction table linking workouts to exercises with ordering
id: Primary keyworkout_id: Foreign key to WORKOUTexercise_id: Foreign key to EXERCISEexercise_index: Order of the exercise within the workout. A(workout_id, exercise_id, exercise_index)tuple is unique, so the same exercise can legitimately appear at multiple positions in a routine (e.g., a warm-up set and a working set of the same lift).
-
SET: Individual sets performed within an exercise
id: Primary keyworkout_exercise_id: Foreign key to WORKOUT_EXERCISEset_index: Order of the set within the exercisereps: Number of repetitions performedweight: Weight used (in kg or lbs, based on Hevy export settings)rpe: Rate of Perceived Exertion (1-10 scale)
flowchart TB
subgraph Clients["Clients"]
AI[AI Agent / Client]
REST[REST API Client]
end
subgraph MCP["MCP Server — port 9091"]
MCP_APP[FastMCP<br/>Query tools, PR calculations]
end
subgraph API["REST API Server — port 9090"]
API_APP[FastAPI<br/>POST /import/csv<br/>POST /sync/hevy<br/>POST /webhooks/hevy<br/>Hevy API client & sync]
end
subgraph DB["Relational Database — PostgreSQL"]
TABLES[ROUTINE, WORKOUT, EXERCISE, SET]
end
AI -->|"MCP Protocol<br/>(Streamable HTTP)"| MCP_APP
REST -->|"HTTP/JSON"| API_APP
MCP_APP -->|SQLAlchemy| TABLES
API_APP -->|SQLAlchemy| TABLES
- Python >= 3.13
- Dependencies managed via
uv
- Clone the repository:
git clone https://github.com/Ramir0/workout-mcp.git
cd workout-mcp- Install dependencies using uv:
uv sync- Install pre-commit hooks:
uv run pre-commit install- Copy the example environment file and adjust values for your machine:
cp .env.example .env- Start PostgreSQL using Docker Compose:
docker-compose up -dThis starts a PostgreSQL 16 container with:
- Database:
workout_mcp - User:
postgres - Password:
postgres - Port:
5432
- To run migrations:
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/workout_mcp uv run alembic upgrade head# REST API (port 9090)
python main.py
# MCP Server (port 9091)
python mcp_server_main.pyExport your workout data from the Hevy app and use the REST API endpoint to import:
curl -X POST http://localhost:9090/import/csv \
-H "Content-Type: text/csv" \
--data-binary @hevy_export.csvReceive real-time workout updates from Hevy via webhooks instead of manually importing CSV files.
Add the following to your .env file:
HEVY_API_KEY=your-hevy-api-keyHEVY_API_KEY: Your personal Hevy API key (available in Hevy app settings). Required for the server to fetch workout details when a webhook is received.
In the Hevy app or developer portal, set your webhook URL to:
https://your-server-domain.com/webhooks/hevy
Make sure this URL is publicly accessible (use a reverse proxy or tunnel like ngrok for local development).
When a workout is created or updated, Hevy sends a POST request to /webhooks/hevy with:
Headers:
| Header | Value | Description |
|---|---|---|
Content-Type |
application/json |
JSON payload |
Body:
{
"workoutId": "abc123"
}flowchart LR
H[Hevy App] -->|POST /webhooks/hevy| W[Workout MCP Server]
W --> P[Queue Background Task]
P --> F[Fetch Workout from Hevy API]
F --> U[Upsert into Database]
- The endpoint responds immediately with
{"status": "ok"}(HTTP 200). - The actual workout fetch and database upsert happen asynchronously in a background task.
- If
HEVY_API_KEYis not configured, the endpoint returns503 Service Unavailable.
Trigger a Hevy API sync on demand via the REST API. The sync runs as a background task.
Polls /v1/workouts/events for any workouts updated since the last sync and upserts them. Tracks progress via the SyncState watermark.
curl -X POST http://localhost:9090/sync/hevyResponse:
{
"status": "sync_started"
}The endpoint returns immediately (HTTP 202 Accepted). The actual sync runs in a background task. If HEVY_API_KEY is not configured, the background task logs a warning and skips the sync.
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"workout": {
"url": "http://localhost:9091"
}
}
}Add to .continue/config.yaml:
mcpServers:
- name: workout
url: http://localhost:9091Any MCP-compatible client can connect to http://localhost:9091 using streamable HTTP transport.
Once connected via MCP, you can ask questions like:
| Natural Language Query | MCP Tool Called |
|---|---|
| "Show me all chest workouts from last month" | get_workout_by_exercise("Chest Press") |
| "What's my heaviest squat this year?" | get_max_pr_by_exercise("Squat") |
| "How many workouts did I do this week?" | get_workout_count(start_date="2026-05-12", end_date="2026-05-19") |
| "What was my last workout?" | get_last_workout() |
| "Show me all PPL routine workouts" | get_workout_by_routine("PPL") |
| "What's my lightest bench press PR?" | get_min_pr_by_exercise("Bench Press") |
| Tool | Parameters | Returns |
|---|---|---|
get_workout_by_date_range |
start_date: str, end_date: str (ISO format) |
List of workouts with exercises and sets |
get_workout_by_routine |
routine_name: str |
List of workouts for that routine |
get_workout_by_exercise |
exercise_name: str |
List of workouts containing that exercise |
get_workout_count |
start_date?, end_date?, routine_name? |
Integer count |
get_last_workout |
exercise_name? |
Most recent workout (or empty dict) |
get_max_pr_by_exercise |
exercise_name: str |
{date, weight, reps} or empty dict |
get_min_pr_by_exercise |
exercise_name: str |
{date, weight, reps} or empty dict |
The MCP tools enable rich querying scenarios such as:
- Training History: "Show me all chest workouts from last month"
- Progress Tracking: "What's my heaviest squat this year?"
- Volume Analysis: "How many sets did I do for bench press in January?"
- Routine Analysis: "When was the last time I did the 'Upper Body' routine?"
- RPE Tracking: "What's my average RPE for deadlifts?"
All settings are managed via pydantic-settings with .env file auto-loading.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgresql://postgres:postgres@localhost:5432/workout_mcp |
PostgreSQL connection string |
TEST_DATABASE_URL |
postgresql://postgres:postgres@localhost:5432/workout_mcp_test |
Test database connection string |
APP_PORT |
9090 |
Port for the REST API server |
MCP_PORT |
9091 |
Port for the MCP server |
HEVY_API_KEY |
None |
Hevy API key (required for webhook sync and automatic fetching) |
HEVY_BASE_URL |
https://api.hevyapp.com |
Hevy API base URL |
LOG_LEVEL |
INFO |
Logging level (DEBUG, INFO, WARNING, ERROR) |
LOG_FORMAT |
console |
Log output format (console for dev, json for production) |
Copy .env.example to .env and adjust values for your environment.
workout-mcp/
├── .github/
│ └── workflows/
│ └── ci.yml # GitHub Actions CI (lint, typecheck, test)
├── alembic/
│ ├── env.py # Alembic environment
│ ├── script.py.mako # Alembic template
│ └── versions/ # Migration scripts
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Pytest fixtures with transaction isolation
│ ├── test_models.py # Model unit tests
│ ├── test_database.py # Database integration tests
│ ├── test_api.py # API integration tests (incl. error paths)
│ ├── test_parser.py # Parser unit tests
│ ├── test_config.py # Config module tests
│ ├── test_logging.py # Logging module tests
│ ├── test_main.py # Main/app structure tests
│ ├── test_mcp_tools.py # MCP tool integration tests (incl. error paths)
│ └── fixtures/
│ ├── sample_hevy.csv # Valid multi-routine fixture
│ ├── empty.csv # Header-only fixture
│ ├── missing_columns.csv # Missing required column
│ ├── malformed_date.csv # Bad date format
│ ├── invalid_weight.csv # Negative weight
│ ├── weight_without_reps.csv # Weight without reps
│ └── cardio.csv # Empty weight & reps (cardio)
├── workout_mcp/
│ ├── __init__.py
│ ├── config.py # pydantic-settings configuration
│ ├── database.py # Engine & session factory
│ ├── logging.py # structlog logging configuration
│ ├── models.py # SQLAlchemy ORM models
│ ├── api.py # FastAPI app, REST endpoints, exception handlers, middleware
│ ├── mcp_server.py # FastMCP server & tools with error handling
│ ├── parser.py # Hevy CSV export parser
│ ├── hevy_client.py # Async HTTP client for Hevy REST API
│ ├── hevy_mapper.py # Maps Hevy API responses to ORM models
│ ├── sync.py # Sync operations (incremental events + full sync)
│ └── sync_service.py # DB upsert logic for Hevy workouts
├── .dockerignore # Docker build exclusions
├── Dockerfile # Production container build
├── docker-compose.yml # PostgreSQL container for local development
├── docker-compose.prod.yml # Production: app + postgres
├── main.py # FastAPI server entry point
├── mcp_server_main.py # MCP server entry point
├── pyproject.toml # Project configuration
├── uv.lock # Locked dependencies
├── .env.example # Environment template
├── .pre-commit-config.yaml # Pre-commit hook configuration
├── alembic.ini # Alembic configuration
└── README.md # This file
- Import: CSV data from Hevy is parsed and validated by
workout_mcp/parser.py - Sync: On-demand endpoint (
POST /sync/hevy) or webhooks (POST /webhooks/hevy) poll recent events via the Hevy REST API (workout_mcp/hevy_client.py), map to ORM models (workout_mcp/hevy_mapper.py), and upsert into the database (workout_mcp/sync_service.py) - Storage: Data is persisted in the relational schema (Routine -> Workout -> Exercise -> Set) via the REST API
- Query: MCP tools provide filtered access to workout history
- Analysis: AI agents can calculate trends, PRs, volume, and training patterns
- Set environment variables:
export DATABASE_USER=postgres
export DATABASE_PASSWORD=<secure-password>- Run migrations:
docker compose -f docker-compose.prod.yml run --rm api uv run alembic upgrade head- Build and start all services:
docker compose -f docker-compose.prod.yml up -d --buildTo build without cache (clean rebuild):
docker compose -f docker-compose.prod.yml build --no-cache
docker compose -f docker-compose.prod.yml up -dOr in one command:
docker compose -f docker-compose.prod.yml up -d --build --no-cache- View logs:
docker compose -f docker-compose.prod.yml logs -f- Stop services:
docker compose -f docker-compose.prod.yml down- Verify REST API:
curl -X POST http://localhost:9090/import/csv -H "Content-Type: text/csv" --data-binary @hevy_export.csv - Verify MCP Server:
curl http://localhost:9091
The host's HTTP server should reverse proxy to localhost:8000.
docker-compose up -d # Start PostgreSQL
python main.py # Start REST API (port 9090)
python mcp_server_main.py # Start MCP Server (port 9091)This project uses:
| Tool | Purpose |
|---|---|
ruff |
Linting and formatting |
mypy |
Static type checking (strict mode) |
pytest |
Test framework |
pre-commit |
Git hooks (auto-runs ruff, mypy on commit) |
GitHub Actions |
CI pipeline (lint, typecheck, test on push/PR) |
Run tooling checks:
uv run ruff check . # Lint
uv run mypy . # Type check
uv run pytest # Run tests
uv run pytest --cov --cov-report=term-missing --cov-fail-under=90 # Tests + coverage
uv run pre-commit run --all-files # Run all hooks| Package | Purpose |
|---|---|
mcp[cli]>=1.27.1 |
Model Context Protocol implementation |
httpx>=0.28.1 |
HTTP client for API interactions |
sqlalchemy>=2.0 |
ORM for database access |
psycopg2-binary>=2.9 |
PostgreSQL driver |
alembic>=1.13 |
Database migrations |
python-dotenv>=1.0 |
Environment variable management |
fastapi>=0.115 |
REST API framework |
uvicorn>=0.32 |
ASGI server |
python-multipart>=0.0.12 |
Multipart form parsing for file uploads |
structlog>=24.1.0 |
Structured logging |
pydantic-settings>=2.0.0 |
Typed settings management |
Dev dependencies: pytest>=8.0, pytest-cov>=5.0, ruff>=0.6, mypy>=1.11, pre-commit>=3.8