Skip to content

Ramir0/workout-mcp

Repository files navigation

Workout MCP Server

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.

Overview

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.

Features

REST API

  • 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-ID propagation and structured JSON/console logging via structlog

MCP Tools for AI Agents

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".

Database Schema

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
Loading

Schema Description

  • ROUTINE: Represents a predefined workout template (e.g., "Push Day", "Leg Day")

    • id: Primary key
    • name: Routine name
  • WORKOUT: Represents an individual workout session

    • id: Primary key
    • start: Session start timestamp
    • end: Session end timestamp
    • routine_id: Foreign key to ROUTINE
  • EXERCISE: Master list of exercises

    • id: Primary key
    • name: Exercise name (e.g., "Bench Press", "Squat")
  • WORKOUT_EXERCISE: Junction table linking workouts to exercises with ordering

    • id: Primary key
    • workout_id: Foreign key to WORKOUT
    • exercise_id: Foreign key to EXERCISE
    • exercise_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 key
    • workout_exercise_id: Foreign key to WORKOUT_EXERCISE
    • set_index: Order of the set within the exercise
    • reps: Number of repetitions performed
    • weight: Weight used (in kg or lbs, based on Hevy export settings)
    • rpe: Rate of Perceived Exertion (1-10 scale)

Architecture

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
Loading

Requirements

  • Python >= 3.13
  • Dependencies managed via uv

Installation

  1. Clone the repository:
git clone https://github.com/Ramir0/workout-mcp.git
cd workout-mcp
  1. Install dependencies using uv:
uv sync
  1. Install pre-commit hooks:
uv run pre-commit install

Local Development Database

  1. Copy the example environment file and adjust values for your machine:
cp .env.example .env
  1. Start PostgreSQL using Docker Compose:
docker-compose up -d

This starts a PostgreSQL 16 container with:

  • Database: workout_mcp
  • User: postgres
  • Password: postgres
  • Port: 5432
  1. To run migrations:
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/workout_mcp uv run alembic upgrade head

Usage

Starting the Server

# REST API (port 9090)
python main.py

# MCP Server (port 9091)
python mcp_server_main.py

Importing Hevy CSV Data

Export 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.csv

Hevy Webhook Integration

Receive real-time workout updates from Hevy via webhooks instead of manually importing CSV files.

1. Configure Environment Variables

Add the following to your .env file:

HEVY_API_KEY=your-hevy-api-key
  • HEVY_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.

2. Configure the Webhook URL in Hevy

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).

3. Webhook Request Format

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"
}

4. Webhook Processing Flow

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]
Loading
  • 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_KEY is not configured, the endpoint returns 503 Service Unavailable.

Hevy Sync Endpoint

Trigger a Hevy API sync on demand via the REST API. The sync runs as a background task.

POST /sync/hevy

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/hevy

Response:

{
  "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.

MCP Client Configuration

Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "workout": {
      "url": "http://localhost:9091"
    }
  }
}

Continue

Add to .continue/config.yaml:

mcpServers:
  - name: workout
    url: http://localhost:9091

Generic MCP Client

Any MCP-compatible client can connect to http://localhost:9091 using streamable HTTP transport.

Example Queries

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 Details

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

Query Capabilities

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?"

Environment Variables

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.

Project Structure

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

Data Flow

  1. Import: CSV data from Hevy is parsed and validated by workout_mcp/parser.py
  2. 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)
  3. Storage: Data is persisted in the relational schema (Routine -> Workout -> Exercise -> Set) via the REST API
  4. Query: MCP tools provide filtered access to workout history
  5. Analysis: AI agents can calculate trends, PRs, volume, and training patterns

Docker Deployment

Production

  1. Set environment variables:
export DATABASE_USER=postgres
export DATABASE_PASSWORD=<secure-password>
  1. Run migrations:
docker compose -f docker-compose.prod.yml run --rm api uv run alembic upgrade head
  1. Build and start all services:
docker compose -f docker-compose.prod.yml up -d --build

To build without cache (clean rebuild):

docker compose -f docker-compose.prod.yml build --no-cache
docker compose -f docker-compose.prod.yml up -d

Or in one command:

docker compose -f docker-compose.prod.yml up -d --build --no-cache
  1. View logs:
docker compose -f docker-compose.prod.yml logs -f
  1. Stop services:
docker compose -f docker-compose.prod.yml down
  1. Verify REST API: curl -X POST http://localhost:9090/import/csv -H "Content-Type: text/csv" --data-binary @hevy_export.csv
  2. Verify MCP Server: curl http://localhost:9091

The host's HTTP server should reverse proxy to localhost:8000.

Local Development

docker-compose up -d          # Start PostgreSQL
python main.py                # Start REST API (port 9090)
python mcp_server_main.py     # Start MCP Server (port 9091)

Development

Tooling

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

Dependencies

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

About

MCP server designed to maintain a comprehensive workout registry

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages